preview-surface.ts
PURPOSE
Self-contained WebGL2 surface that compiles a fragment shader (either a named template from the shader-template registry or raw GLSL) and renders it to a fullscreen quad. Used by the VFX workbench to preview shader templates with uniform controls and capture pixels for tests/thumbnails. Lifecycle: construct sized canvas, useTemplate or useRawFragment, setUniforms, render(t), optionally readPixels, then destroy.
OWNS
canvas: HTMLCanvasElement | OffscreenCanvas— backing surface, sized from constructorwidth/height.gl: WebGL2RenderingContext— context acquired from canvas; throwsWebGL2 unavailableifgetContext('webgl2')returns null.program: WebGLProgram | null— currently linked program; replaced on everyuseTemplate/useRawFragmentcall (old program deleted first).currentTemplate: TemplateId | null— id of the active registry template, ornullwhen running viauseRawFragment.uniformLocs: Record<string, WebGLUniformLocation | null>— cached locations for shared uniforms (u_time,u_resolution,u_palette,u_intensity) plus template-declared uniforms.vao: WebGLVertexArrayObject | null— single VAO covering the fullscreen quad attributes.vbo: WebGLBuffer | null— vertex buffer holdingQUAD_VERTICES(4 NDC corners asFloat32Array).pendingUniforms: Record<string, number | number[]>— staged uniform values; flushed to the program byflushUniforms.destroyed: boolean— guard flag; methods (other thandestroyitself andrender) throw'Surface destroyed'after teardown;renderbecomes a no-op.
READS FROM
./shader-templates/registry—TEMPLATESmap andTemplateIdtype.useTemplate(id)looks uptpl.fragGlslandtpl.uniformSchema(each entry hasnameanddefault)../component-schema—UniformDecltype used byuseRawFragment’suniformSchemaargument.globalThis.document/OffscreenCanvas— environment probing in the constructor selects a backend.
PUSHES TO
- WebGL2 GPU state on its own context: shader compile, program link, attribute bind, uniform upload,
drawArrays(TRIANGLE_STRIP, 0, 4). Uint8Arrayreturned fromreadPixels()—width * height * 4bytes, RGBA8, top-down GL convention.- Throws
Erroron: missing canvas backend, no WebGL2, unknown template id, shader compile failure (with formatted source context), program link failure, render before any shader compiled, calls after destroy.
DOES NOT
- Does not own a render loop — caller supplies
tand decides when to callrender. - Does not handle resize —
width/heightare fixed at construction;gl.viewportis set once in the constructor and not re-applied per frame. - Does not manage textures, framebuffers, depth, or blending state beyond the implicit defaults (clears to transparent black, no depth test enabled).
- Does not validate uniform types against the schema —
setUniformsaccepts any numbers or number arrays andflushUniformsdispatches bytypeof/lengthheuristics. - Does not retain compiled shaders —
vs/fsshader objects are deleted immediately afterlinkProgram. - Does not coalesce uniform updates — every
setUniformscall triggers an immediateflushUniforms. - Does not deduplicate
programdeletion betweenuseTemplate(which nulls the field) anduseRawFragment(which deletes without nulling before reassign). - Does not export
formatShaderCompileError— it’s a module-private helper.
Signals
'Surface destroyed'— thrown byuseTemplate,useRawFragment,setUniforms(indirectly viaflushUniformsno-op),readPixelsif called afterdestroy().'No shader compiled — call useTemplate or useRawFragment first'— thrown byrenderwhenprogramis null.'Unknown template: <id>'— thrown byuseTemplatewhenTEMPLATES[id]is missing.'WebGL2 unavailable'— thrown by constructor when the context cannot be acquired.'PreviewSurface: no canvas backend available (need document or OffscreenCanvas)'— thrown when neitherdocumentnorOffscreenCanvasexist.'Shader compile failed (vertex|fragment)\n<info>\n--- source context around line N ---\n<annotated source>'— thrown bycompileShaderviaformatShaderCompileError, with>>marker on the offending line and ±2 lines of context.'Program link failed'/'Live shader program link failed'— thrown byuseTemplate/useRawFragmentrespectively onLINK_STATUS === false.
Entry points
constructor(width: number, height: number)— picksOffscreenCanvasin worker-like environments,document.createElement('canvas')when a DOM exists; acquires WebGL2, sets viewport, creates VAO/VBO, uploadsQUAD_VERTICES.useTemplate(id: TemplateId): void— compile + link the named template’s fragment shader againstQUAD_VS, cache shared + schema uniform locations, seedpendingUniformsfromtpl.uniformSchema[i].default, flush.useRawFragment(fragGlsl: string, uniformSchema: UniformDecl[]): void— same flow asuseTemplatebut with caller-provided GLSL and schema; clearscurrentTemplatetonull; does not seed defaults from the schema (caller is responsible).setUniforms(values: Record<string, number | number[]>): void— merges intopendingUniformsand immediately flushes.render(t: number): void— uploadsu_time = t,u_resolution = [width, height],u_intensity = 1.0, defaultu_palette(vec3[4] flattened to 12 floats) if caller hasn’t supplied one, clears to(0,0,0,0), draws the quad.readPixels(): Uint8Array— RGBA8 readback,width * height * 4bytes.destroy(): void— deletes program, VBO, VAO; setsdestroyed = true; idempotent.
Pattern notes
- Shared uniform contract. Every shader is expected to accept (or ignore)
u_time(float),u_resolution(vec2),u_palette(vec3[4] / 12 floats),u_intensity(float). These are always looked up; missing-uniform locations (null) are skipped silently inrenderandflushUniforms. - Default palette. When
u_paletteis declared but never set by the caller,renderuploads the IQ-style cosine palette base[0.5, 0.5, 0.5, 0.5, 0.5, 0.5, 1, 1, 1, 0, 0.33, 0.67]. Despite the comment “vec3[4] flattened (12 elements)”, the code uploads it viauniform3fvrather thanuniform3fvagainst an array — relies on driver accepting a 12-float payload for avec3[4]location. - Uniform value dispatch in
flushUniforms. Scalar numbers go touniform1f; arrays dispatch by length: 2 →uniform2fv, 3 →uniform3fv, 4 →uniform4fv, anything else →uniform3fv(intended for the 12-float palette). - Shader compile error formatting.
formatShaderCompileErrorparses driver logs likeERROR: 0:14: 'foo' : undeclared identifier, extracts the line number with regex/(?:^|[:\s])(?:0:)?(\d+):/m, and splices inlines[lineNum-3 .. lineNum+2]annotated with>>on the offending line. Improves error legibility when shaders are authored in templates or the live workbench. - VAO is bound twice. Constructor binds and unbinds the VAO.
useTemplate/useRawFragmentrebind it to set up thea_posvertex attribute pointer;renderrebinds it again beforedrawArrays. The VAO retains attribute state oncevertexAttribPointeris called, so subsequent renders don’t re-issue pointer setup. useRawFragmentdoesn’t seed defaults. UnlikeuseTemplate, raw-fragment mode leavespendingUniformsuntouched — caller mustsetUniformsbeforerenderor accept whatever the previous program’s stale state was. (Note:pendingUniformsis not cleared between template switches either, so values can carry across template changes if names collide.)- Canvas-backend probing. The constructor prefers
OffscreenCanvasonly whendocumentis undefined; in a real browser the DOM path always wins even ifOffscreenCanvasis available. Tests in jsdom hit the DOM path. - No texture support. This file does not bind, allocate, or sample any textures — templates that need samplers cannot run here without extension.
EXTRACT-CANDIDATE
formatShaderCompileErroris generic — could move to a sharedengine/vfx-workbench/shader-error.tsso non-preview compilers (e.g., production shader build pipeline, live-edit overlay) can reuse the line-annotated error format.- The fullscreen-quad scaffold (VAO + VBO +
QUAD_VS+QUAD_VERTICES) is duplicated across any other WebGL2 surface in the project; if a second user appears, extract aFullscreenQuadhelper. - The shared-uniform contract (
u_time/u_resolution/u_palette/u_intensity) is implicit; consider hoisting the list and the default palette literal into the shader-templates registry so the preview surface and shipping VFX runtime stay in sync. - The
uniform3fv-of-12-floats palette upload should be replaced with the explicitvec3[4]upload pattern (uniform3fv(loc, flatArray)does work in WebGL2 against array uniforms, but the default-length fallthrough influshUniformspapers over the intent — name and document it). setUniformsflushes synchronously per call; if a UI binds many sliders, a deferred-flush mode (set during the frame, flush once inrender) would avoid redundantglUniform*calls.