π§ Utilities for managing WebGL resources: uniforms, (interleaved) attributes and buffers.
- schema-based resource management
view-gl - compose schema and shader simultaneously via a tag template literal
view-gl/tag
- π¦ Install
- ποΈ view.gl
- π Basic Usage
- ποΈ view
- π ViewSchema
- π Resource Views
- π― uniformView
- π UniformSchema
- π attributeView
- π AttributeSchema
- π interleavedAttributeView
- ποΈ bufferView
- π BufferSchema
- π― uniformView
- π·οΈ view.gl/tag
- π Basic Usage
- π glsl
- π·οΈ Resource Tokens
- βοΈ compile(gl, vertex, fragment)
- π οΈ Utils
- ποΈ createProgram
- πΌοΈ createTexture
- π₯οΈ createFramebuffer
- π¨ Errors
- π WebGL Type Compatibility
- π― Uniform Types
- π Attribute Types
npm install @bigmistqke/view.glpnpm add @bigmistqke/view.glyarn add @bigmistqke/view.glbun add @bigmistqke/view.glThe view system provides type-safe WebGL resource management for uniforms and attributes
const { uniforms, attributes } = view(gl, program, {
uniforms: {
time: { kind: 'float' },
resolution: { kind: 'vec2' },
},
attributes: {
position: { kind: 'vec3' },
uv: { kind: 'vec2' },
},
})
// Type-safe uniform setting
uniforms.time.set(performance.now())
uniforms.resolution.set(canvas.width, canvas.height)
// Attribute management
attributes.position.set(positionData)
attributes.position.bind()The view() function creates type-safe WebGL resource managers from a schema.
const { uniforms, attributes } = view(gl, program, schema)Parameters:
gl: WebGL rendering contextprogram: Compiled WebGL programschema: seeViewSchema
Returns:
uniforms: Type-safe uniform settersattributes: Attribute managers with buffer handlingdispose: Deletes everything the view made β see Disposal
The complete schema object that defines all WebGL resources. Contains mappings for:
UniformSchema- uniform variable definitionsAttributeSchema- vertex attribute definitionsInterleavedAttributeSchema- interleaved vertex data definitionsBufferSchema- buffers the view owns
A buffer is the one schema member with no name in the source, so it is not resolved against the program the way a uniform or an attribute is β a buffer key is a label for the reader. What putting it in the schema decides is its lifetime, and that is the question worth answering at the declaration:
| the buffer | declare it | disposed by |
|---|---|---|
| lives and dies with the view | buffers: in the schema |
the view |
| outlives the view, or is shared between views | a standalone bufferView |
you |
| belongs to somebody else | name it on an attribute | nobody β it is borrowed |
An index buffer whose lifetime is its program's belongs in the schema, beside the attributes it is drawn with, on the one signal. A quad cached per context, or a buffer handed from one program's view to another's, is its own thing.
TypeScript Types
interface ViewSchema {
uniforms: UniformSchema
attributes: AttributeSchema
interleavedAttributes: InterleavedAttributeSchema
}Everything that creates a WebGL resource takes an AbortSignal and offers a
dispose(), and the two are the same teardown reached two ways:
const controller = new AbortController()
const { view, dispose } = compile(gl, vertex, fragment, { signal: controller.signal })
const buffers = bufferView(gl, { indices: {} }, { signal: controller.signal })
const target = createFramebuffer(gl, definition, { signal: controller.signal })
controller.abort() // deletes the program, the buffers, the vertex arrays, the framebufferTwo rules make this safe to reach for.
Only what it made. A resource handed in from outside is borrowed, never
deleted: a buffer named by an attribute schema, a texture passed to
createFramebuffer. Those belong to whoever created them, and the same buffer
often feeds several views. Delete one out from under its owner and WebGL does
not fault where it happened β the object survives while it is bound, so the
handle is invalidated and the other view fails later, on its next upload.
Once. Every dispose() is idempotent. Disposing by hand and then aborting,
or aborting twice, deletes each resource exactly once.
| creates | disposed by |
|---|---|
view() |
its own attribute, layout and schema buffers, and vertex arrays still held from vao() |
compile() |
the program, and its view |
attributeView / interleavedAttributeView |
the buffer, unless the schema named one |
bufferView |
its buffers |
vaoView |
the vertex array, never its participants |
createProgram |
the program |
createTexture |
the texture |
createFramebuffer |
the framebuffer, and the texture unless one was passed in |
uniformView takes no signal: a uniform location is not a resource, so there is
nothing to delete.
Each view type can be imported individually.
Manages shader uniform variables.
const uniforms = uniformView(gl, program, {
time: { kind: 'float' },
lights: { kind: 'vec3', size: 8 }, // Array uniform: vec3[8]
transform: { kind: 'mat4' },
})
uniforms.time.set(performance.now())
uniforms.lights.set(lightData) // Takes Float32Array for array uniformsA mapping of uniform names to their configuration.
kind: GLSL type (see Uniform Types for full list and WebGL compatibility)size: Array size (optional) - converts uniform to array type
TypeScript Types
type UniformKind =
| 'float'
| 'int'
| 'bool'
| 'vec2'
| 'vec3'
| 'vec4'
| 'ivec2'
| 'ivec3'
| 'ivec4'
| 'bvec2'
| 'bvec3'
| 'bvec4'
| 'mat2'
| 'mat3'
| 'mat4'
| 'sampler2D'
| 'samplerCube'
interface UniformDefinition {
kind: UniformKind
size?: number // Creates array uniform with Float32Array setter
}
type UniformSchema = Record<string | symbol, UniformDefinition>Manages vertex attributes with automatic buffer creation and binding.
const attributes = attributeView(gl, program, {
position: { kind: 'vec3' },
instanceOffset: { kind: 'vec2', instanced: true },
})
attributes.position.set(positionData).bind()
attributes.instanceOffset.set(instanceData).bind()
gl.drawArraysInstanced(gl.TRIANGLES, 0, 3, 100)bind() returns a disposer that restores the state it changed β here the
instancing divisor, which belongs to the attribute location rather than to the
program, and so is inherited by whatever binds there next. Ignore it when the
view owns its locations for the lifetime of the program; call it when they are
shared:
const unbind = attributes.instanceOffset.set(instanceData).bind()
gl.drawArraysInstanced(gl.TRIANGLES, 0, 3, 100)
unbind()A large buffer can be sized first and filled in parts, counting in elements of the attribute's format:
attributes.position.allocate(totalElements) // sizes the buffer, contents undefined
attributes.position.write(chunk, offset) // overwrites a range with bufferSubDataA mapping of attribute names to their configuration.
kind: GLSL type (see Attribute Types for full list and WebGL compatibility)instanced: Boolean (optional) - enables instanced renderingbuffer: Custom WebGLBuffer (optional) - by default it gets created automatically during compilation
TypeScript Types
type AttributeKind =
| 'float'
| 'vec2'
| 'vec3'
| 'vec4'
| 'mat2'
| 'mat3'
| 'mat4'
| 'int'
| 'ivec2'
| 'ivec3'
| 'ivec4'
interface AttributeDefinition {
kind: AttributeKind
instanced?: boolean // Enables vertexAttribDivisor
buffer?: WebGLBuffer // Custom buffer, auto-created if not provided
}
type AttributeSchema = Record<string | symbol, AttributeDefinition>Manages interleaved vertex data with automatic stride/offset calculation.
const interleavedAttributes = interleavedAttributeView(gl, program, {
vertexData: {
layout: [
{ key: 'position', kind: 'vec3' },
{ key: 'normal', kind: 'vec3' },
{ key: 'uv', kind: 'vec2' },
],
},
})
interleavedAttributes.vertexData.buffer.set(interleavedVertexData)Where vertex array objects are available bind() binds one, and it stays
selected for every later draw β which would then write its own attributes into
it. The disposer returned by bind() selects the previous array again:
const unbind = interleavedAttributes.vertexData.buffer.bind()
gl.drawElements(gl.TRIANGLES, count, gl.UNSIGNED_SHORT, 0)
unbind()Whether the buffer steps once per vertex or once per instance is chosen per
draw, not in the schema: buffer steps per vertex, buffer.perInstance per
instance. constant sets the same layout as constant attribute values, for a
draw that needs one set of values without a buffer.
A large buffer can be sized first and filled in parts, so it never has to exist whole in JavaScript. Both count in floats:
const { buffer } = interleavedAttributes.vertexData
buffer.allocate(totalFloats) // sizes the buffer, contents undefined
buffer.write(chunk, offsetFloats) // overwrites a range with bufferSubDataA mapping of interleaved buffer names to their layout configuration. Each layout defines multiple attributes packed into a single buffer.
layout: Array of attribute definitions withkeyandkind(see Attribute Types)
TypeScript Types
interface InterleavedAttributeDefinition {
layout: Array<{
key: string | symbol
kind: AttributeKind
}>
}
type InterleavedAttributeSchema = Record<string | symbol, InterleavedAttributeDefinition>Manages generic WebGL buffers.
const buffers = bufferView(gl, {
indices: { target: 'ELEMENT_ARRAY_BUFFER' },
data: { target: 'ARRAY_BUFFER', usage: 'DYNAMIC_DRAW' },
})
buffers.indices.set(indexData)
buffers.data.set(dynamicData)
const unbind = buffers.indices.bind()
// bind() returns a disposer restoring the previous binding for that target.
// Worth calling for ELEMENT_ARRAY_BUFFER, whose binding is recorded in the
// bound vertex array object.A large buffer can be sized first and filled in parts. A buffer has no element type of its own, so these count in bytes:
buffers.data.allocate(byteLength) // sizes the buffer, contents undefined
buffers.data.write(chunk, byteOffset) // overwrites a range with bufferSubDataA mapping of buffer names to their configuration. Each buffer has a target type and optional usage pattern.
target: Buffer target ('ARRAY_BUFFER','ELEMENT_ARRAY_BUFFER')usage: Usage pattern ('STATIC_DRAW','DYNAMIC_DRAW','STREAM_DRAW')
TypeScript Types
interface BufferDefinition {
target: 'ARRAY_BUFFER' | 'ELEMENT_ARRAY_BUFFER'
usage?: 'STATIC_DRAW' | 'DYNAMIC_DRAW' | 'STREAM_DRAW' // Defaults to 'STATIC_DRAW'
}
type BufferSchema = Record<string | symbol, BufferDefinition>Type-safe GLSL template literals with automatic schema extraction and view creation.
- Embedded Resources: Define uniforms, attributes, and interleaved layouts directly in GLSL
- Type Inference: Automatically infers schema types and creates type-safe view
- Unique Variables: Prevent naming collisions using symbols for unique shader variables
- GLSL Composition: Compose reusable GLSL fragments with automatic dependency resolution
const vertexShader = glsl`
${attribute.vec3('position')}
${uniform.mat4('model')}
varying vec2 vUv;
void main() {
vUv = uv;
gl_Position = model * vec4(position, 1.0);
}
`
const fragmentShader = glsl`
${uniform.sampler2D('texture')}
varying vec2 vUv;
void main() {
gl_FragColor = texture2D(texture, vUv);
}
`
const { program, schema } = compile(gl, vertexShader, fragmentShader)Template literal processor that handles GLSL code and embedded resources. Supports interpolation of:
- Resource tags:
uniform.*(),attribute.*(),interleave() - GLSL fragments: Reusable shader code snippets
- Symbol Variables: Unique variable names to prevent collisions
- Strings: Interpolated as-is into the shader code
- Arrays: Arrays of any supported interpolation types
const precision = 'precision mediump float;'
const functionName = Symbol('function')
const shader = glsl`
${precision} // String interpolated as-is
${uniform.vec2('resolution')}
${[attribute.vec3('position'), attribute.vec2('uv')]} // Array interpolation
vec3 ${functionName}(vec2 uv) { // Symbol interpolated to unique identifier
return vec3(uv, 0.5);
}
void main() {
gl_Position = vec4(position, 1.0);
}
`Compose reusable GLSL code fragments to build complex shaders:
const lighting = glsl`
vec3 calculateLighting(vec3 normal, vec3 lightDir) {
float diff = max(dot(normal, lightDir), 0.0);
return vec3(diff);
}
`
const vertexShader = glsl`
${attribute.vec3('direction')}
${attribute.vec3('normal')}
${lighting} // Include the lighting fragment
varying vec3 vLighting;
void main() {
vLighting = calculateLighting(normal, direction);
}
`Use JavaScript symbols to prevent naming collisions:
const sum = Symbol('sum')
const sumFragment = glsl`
float ${sum}(float a, float b){
return a + b;
}`
const shader = glsl`
${sumFragment}
void main(){
float result = ${sum}(1.0, 2.0);
}
`Symbols are converted to unique identifiers during the compilation of the shader.
The glsl-function supports both WebGL1 and WebGL2 syntax, automatically using the correct keywords for resource tags:
// WebGL1 (default)
const shader = glsl`
${attribute.vec3('position')} // β attribute vec3 position;
varying vec2 vUv;
`If the shader starts with #version 300 es, resource tags generate WebGL2 syntax:
// WebGL2
const shader = glsl`#version 300 es
${attribute.vec3('position')} // β in vec3 position;
out vec2 vUv;
`Utilities for defining WebGL resources directly in GLSL templates. These create metadata that the compile consumes to generate the typesafe schema and view.
Define uniform variables in GLSL templates (see Uniform Types).
const uniqueTime = Symbol('time')
const shader = glsl`
${uniform.float('time')} // String key
${uniform.float(uniqueTime)} // Symbol key
${uniform.vec3('lights', { size: 8 })} // Array uniform: vec3[8]
void main() {
float wave = sin(time * 2.0 + ${uniqueTime});
vec3 totalLight = vec3(0.0);
for(int i = 0; i < 8; i++) {
totalLight += lights[i] * wave;
}
gl_FragColor = vec4(totalLight, 1.0);
}
`Parameters:
name: Uniform name (string or symbol for unique variables)options: Optional configuration objectsize: Array size (creates array uniform with Float32Array setter)
Define vertex attributes in GLSL templates (see Attribute Types).
const uniquePosition = Symbol('position')
const vertexShader = glsl`
${attribute.vec3('position')} // String key
${attribute.vec3(uniquePosition)} // Symbol key
${attribute.vec2('offset', { instanced: true })}
void main() {
gl_Position = vec4(position + vec3(offset, 0.0), 1.0);
}
`Parameters:
name: Attribute name (string or symbol for unique variables)options: Optional configuration objectinstanced: Boolean - enables instanced rendering withvertexAttribDivisorbuffer: Custom WebGLBuffer (optional)
Define interleaved attribute layouts for efficient vertex data.
const uniqueVertexData = Symbol('vertexData')
const uniquePosition = Symbol('position')
const vertexShader = glsl`
${interleave('vertexData', [
// String key
{ key: 'position', kind: 'vec3' },
{ key: 'uv', kind: 'vec2' },
])}
${interleave(uniqueVertexData, [
// Symbol key
{ key: uniquePosition, kind: 'vec3' }, // Symbol keys in layout
{ key: 'uv', kind: 'vec2' },
])}
void main() {
gl_Position = vec4(position + vec3(uv, 0.0), 1.0);
}
`Parameters:
name: Buffer name (string or symbol for unique variables)layout: Array of attribute definitionskey: Attribute name (string or symbol for unique variables)kind: GLSL type (see Attribute Types)
options: Optional configuration objectinstanced: Boolean - appliesvertexAttribDivisorto all attributes
Compiles shaders to a WebGLProgram and extracts typesafe schema and view.
Convenient helper for fullscreen quad rendering with fragment shaders. Automatically creates a vertex shader with a quad geometry and handles vertex buffer setup.
const { program, schema, view } = compile.toQuad(gl, fragmentShader)
// Ready to render - no vertex setup required
view.uniforms.time.set(performance.now())
gl.drawArrays(gl.TRIANGLES, 0, 6)Perfect for:
- Fragment shader effects (ray-marching, post-processing, etc.)
- Fullscreen compute-style shaders
- Quick prototyping and experimentation
Generated vertex shader:
- Creates
a_quadattribute automatically - Outputs
uvvarying (same as vertex position:[-1,1]range) - Sets up clip-space quad covering the entire screen
Parameters:
gl: WebGL rendering contextfragment: Fragment shader with embedded resourcesoptions: Optional compilation options (same ascompile())
Returns: Same as compile() with pre-configured quad rendering
const { program, schema, view, vertex, fragment } = compile(gl, vertexShader, fragmentShader)
// Use the view directly
view.uniforms.time.set(performance.now())
view.attributes.position.set(vertexData).bind()
// Access the compiled shader strings
console.log(vertex) // Compiled vertex shader GLSL
console.log(fragment) // Compiled fragment shader GLSL
// Or access the extracted schema
console.log(schema.uniforms) // { time: { kind: 'float' }, ... }
console.log(schema.attributes) // { position: { kind: 'vec3' }, ... }Override Schema:
You can provide an optional override schema to enhance or override the automatically extracted schema:
const { program, schema, view } = compile(gl, vertexShader, fragmentShader, {
schema: {
uniforms: {
// Add additional uniforms not automatically inferred
customTime: { kind: 'float' },
},
},
})
// The override schema is merged with the extracted schema
view.uniforms.customTime.set(123.45)
// An index buffer that outlives this program β shared between views, or cached
// per context β is its own thing. One that dies with the view goes in the
// schema instead, and is disposed with it.
const buffers = bufferView(gl, { indices: { target: 'ELEMENT_ARRAY_BUFFER' } })
buffers.indices.set(indexData)
const vao = view.vao([view.attributes.position, buffers.indices])Returns:
program: Compiled WebGL programschema: Merged schema (extracted + override)view: Ready-to-use view with type-safe resource accessvertex: Compiled vertex shader GLSL stringfragment: Compiled fragment shader GLSL string
Converts a GLSL tagged template to a shader string without compilation of the WebGLProgram. Useful for debugging or when you need the raw shader code.
const vertexShader = glsl`
${attribute.vec3('position')}
${uniform.mat4('mvpMatrix')}
void main() {
gl_Position = mvpMatrix * vec4(position, 1.0);
}
`
const shaderString = compile.toString(vertexShader)
console.log(shaderString)
// Output:
// attribute vec3 position;
// uniform mat4 mvpMatrix;
//
// void main() {
// gl_Position = mvpMatrix * vec4(position, 1.0);
// }Features:
- Converts resource tags to GLSL declarations
- Handles WebGL1/WebGL2 syntax automatically
- Resolves symbol variables to unique identifiers
- Processes nested GLSL fragments and arrays
Extracts the ViewSchema from a GLSL tagged template without compilation of the WebGLProgram. Returns the type-safe schema that could be used by view().
const shader = glsl`
${uniform.float('time')}
${uniform.vec3('lightPos', { size: 4 })}
${attribute.vec3('position')}
${interleave('vertexData', [
attribute.vec2('uv'),
attribute.vec4('color')
])}
`
const schema = compile.toSchema(shader)
console.log(schema)
// Output:
// {
// uniforms: {
// time: { kind: 'float' },
// lightPos: { kind: 'vec3', size: 4 }
// },
// attributes: {
// position: { kind: 'vec3' }
// },
// interleavedAttributes: {
// vertexData: {
// layout: [
// { key: 'uv', kind: 'vec2' },
// { key: 'color', kind: 'vec4' }
// ],
// instanced: false
// }
// }
// }Use Cases:
- Analyze shader resources without GL context
- Generate TypeScript types from shaders
- Validate shader compatibility before compilation
- Build tooling around shader resources
Usage with vanilla view.gl
When manually constructing GLSL strings, use toID() to convert symbols to valid, unique identifiers:
const u_time = Symbol('time')
const a_position = Symbol('position')
const vertex = `
attribute vec3 ${toID(a_position)};
uniform float ${toID(u_time)};
void main() {
gl_Position = vec4(${toID(a_position)}, ${toID(u_time)});
}
`
const fragment = `
precision mediump float;
uniform float ${toID(u_time)};
void main() {
gl_FragColor = vec4(1.0, 1.0, 1.0, sin(${toID(u_time)}));
}
`
const program = createProgram(gl, vertex, fragment)
const { attributes, uniforms } = view(gl, program, {
uniforms: { [u_time]: { kind: 'float' } },
attributes: { [a_position]: { kind: 'vec3' } },
})
attributes[a_position].set(vertexData)
uniforms[u_time].set(performance.now())Creates and links a WebGL program from vertex and fragment shader sources.
const program = createProgram(gl, vertexShaderSource, fragmentShaderSource, { signal })Creates a WebGL texture with specified parameters.
const texture = createTexture(
gl,
{
width: 512,
height: 512,
internalFormat: 'RGBA',
format: 'RGBA',
type: 'UNSIGNED_BYTE',
minFilter: 'LINEAR',
magFilter: 'LINEAR',
wrapS: 'CLAMP_TO_EDGE',
wrapT: 'CLAMP_TO_EDGE',
},
data,
{ signal },
)Automatically validates WebGL2-only formats and provides fallbacks for WebGL1.
Creates a framebuffer with attached texture for render-to-texture operations.
const { framebuffer, texture, dispose } = createFramebuffer(
gl,
{
width: 512,
height: 512,
attachment: 'color',
internalFormat: 'RGBA',
format: 'RGBA',
type: 'UNSIGNED_BYTE',
},
{ signal },
)Supports color, depth, stencil, and combined depth-stencil attachments with completeness validation.
Pass a texture to attach one you already have β ping-ponging a pair of
framebuffers over shared textures, say. dispose() then deletes the framebuffer
and leaves that texture alone, because it is not the framebuffer's to delete.
Every error view.gl throws for a GL call that failed is a class exported from @bigmistqke/view.gl, carrying what the GL answered:
| Error | Thrown when | Carries |
|---|---|---|
ShaderCompileError |
a shader does not compile | stage, infoLog (string | null, as the driver answered it), glError |
ProgramLinkError |
a program does not link | infoLog (string | null), glError |
GLCreateError |
a create* call answers null |
resource: 'shader', 'program', 'buffer', 'vertexArray', 'texture' or 'framebuffer' |
AttributeNotFoundError |
getAttribLocation answers -1 |
attribute |
A lost WebGL context answers every call with a default: null from a create*, -1 for an attribute, false with an empty or null log for a compile or link. So an empty infoLog or a GLCreateError carries no cause, where a non-empty infoLog is the driver's own reason.
import { createProgram } from '@bigmistqke/view.gl/gl'
import { ShaderCompileError } from '@bigmistqke/view.gl'
try {
createProgram(gl, vertex, fragment)
} catch (error) {
if (error instanceof ShaderCompileError && error.infoLog?.trim()) {
console.error(`${error.stage} shader rejected:`, error.infoLog)
}
}| Type | WebGL 1 | WebGL 2 |
|---|---|---|
float |
β | β |
int |
β | β |
bool |
β | β |
vec2 |
β | β |
vec3 |
β | β |
vec4 |
β | β |
ivec2 |
β | β |
ivec3 |
β | β |
ivec4 |
β | β |
bvec2 |
β | β |
bvec3 |
β | β |
bvec4 |
β | β |
mat2 |
β | β |
mat3 |
β | β |
mat4 |
β | β |
sampler2D |
β | β |
samplerCube |
β | β |
uint |
β | β |
uvec2 |
β | β |
uvec3 |
β | β |
uvec4 |
β | β |
mat2x3 |
β | β |
mat2x4 |
β | β |
mat3x2 |
β | β |
mat3x4 |
β | β |
mat4x2 |
β | β |
mat4x3 |
β | β |
sampler3D |
β | β |
sampler2DArray |
β | β |
sampler2DShadow |
β | β |
samplerCubeShadow |
β | β |
sampler2DArrayShadow |
β | β |
isampler2D |
β | β |
isampler3D |
β | β |
isamplerCube |
β | β |
isampler2DArray |
β | β |
usampler2D |
β | β |
usampler3D |
β | β |
usamplerCube |
β | β |
usampler2DArray |
β | β |
| Type | WebGL 1 | WebGL 2 |
|---|---|---|
float |
β | β |
vec2 |
β | β |
vec3 |
β | β |
vec4 |
β | β |
mat2 |
β | β |
mat3 |
β | β |
mat4 |
β | β |
int |
β | β |
ivec2 |
β | β |
ivec3 |
β | β |
ivec4 |
β | β |
uint |
β | β |
uvec2 |
β | β |
uvec3 |
β | β |
uvec4 |
β | β |
mat2x3 |
β | β |
mat2x4 |
β | β |
mat3x2 |
β | β |
mat3x4 |
β | β |
mat4x2 |
β | β |
mat4x3 |
β | β |