A dependency-free, tree-shakable TypeScript toolkit for building portable WebGPU and WebGL2 render pipelines without hiding the underlying graphics API.
WGX provides one backend-neutral resource and render-pass model for application code while keeping the existing low-level WebGL2 helpers available for direct control.
bun add @obvia/wgximport { createWGX } from "@obvia/wgx"
const canvas = document.querySelector("canvas")!
const wgx = await createWGX({
canvas,
backend: "auto",
powerPreference: "high-performance"
})
const vertices = wgx.createBuffer({
data: new Float32Array([
-1, -1,
1, -1,
0, 1
]),
usage: ["vertex"]
})
const shader = wgx.createShader({
webgpu: `
struct VertexInput {
@location(0) position: vec2f,
}
@vertex
fn vertexMain(input: VertexInput) -> @builtin(position) vec4f {
return vec4f(input.position, 0.0, 1.0);
}
@fragment
fn fragmentMain() -> @location(0) vec4f {
return vec4f(0.12, 0.55, 1.0, 1.0);
}
`,
webgl2: {
vertex: `#version 300 es
layout(location = 0) in vec2 aPosition;
void main() {
gl_Position = vec4(aPosition, 0.0, 1.0);
}`,
fragment: `#version 300 es
precision mediump float;
out vec4 fragmentColor;
void main() {
fragmentColor = vec4(0.12, 0.55, 1.0, 1.0);
}`
}
})
const pipeline = wgx.createRenderPipeline({
shader,
vertex: {
buffers: [{
arrayStride: 8,
attributes: [{
shaderLocation: 0,
offset: 0,
format: "float32x2"
}]
}]
}
})
wgx.render({ clearColor: [0.02, 0.03, 0.05, 1] }, pass => {
pass.setPipeline(pipeline)
pass.setVertexBuffer(0, vertices)
pass.draw(3)
})
wgx.dispose()With backend: "auto", WGX attempts WebGPU first and falls back to WebGL2. Explicit webgpu or webgl2 selection never silently switches backends.
- Portable: application code uses one
Buffer → Shader → RenderPipeline → RenderPassmodel across WebGPU and WebGL2. - Safe: resources are device-owned, destroyed resources are rejected, cross-device usage fails early, and buffer writes are bounds-checked.
- Performant: the WebGL2 backend caches program, viewport, VAO and upload bindings instead of querying driver state on the render path.
- Direct: every high-level resource exposes its native backend representation through
nativewhen lower-level integration is required. - Capability-aware: backend differences such as compute and timestamp-query support are exposed through normalized device capabilities.
- Modular: the root package exposes WGX and existing low-level WebGL2 helpers; focused subpaths remain tree-shakable.
- Dependency-free: the runtime does not add a graphics engine, scene graph, renderer framework, or shader transpiler.
const automatic = await createWGX({ canvas })
const webgpu = await createWGX({ canvas, backend: "webgpu" })
const webgl2 = await createWGX({ canvas, backend: "webgl2" })createWGX is always asynchronous so backend selection has one stable call shape even though WebGPU adapter and device acquisition is asynchronous.
Every device exposes a normalized capability snapshot:
console.log(wgx.backend)
console.log(wgx.capabilities.depth)
console.log(wgx.capabilities.stencil)
console.log(wgx.capabilities.compute)
console.log(wgx.capabilities.timestampQuery)
console.log(wgx.capabilities.maxTextureSize)
console.log(wgx.capabilities.maxColorAttachments)
console.log(wgx.capabilities.uniformBufferOffsetAlignment)Capabilities describe what the selected backend can support. They do not emulate unsupported features; for example, WebGL2 reports compute: false rather than translating compute work into a hidden fallback.
Buffers declare their intended usage at creation time and remain owned by the device that created them.
const vertexBuffer = wgx.createBuffer({
data: vertices,
usage: ["vertex"]
})
const indexBuffer = wgx.createBuffer({
data: indices,
usage: ["index"]
})
vertexBuffer.write(updatedVertices)Writes are validated against the allocated byte length and the four-byte alignment required by the portable upload path. A buffer created for one device cannot be submitted through another device.
WGX does not perform hidden runtime shader transpilation. Portable applications provide WGSL for WebGPU and GLSL ES 3.00 for WebGL2:
const shader = wgx.createShader({
webgpu: wgslSource,
webgl2: {
vertex: vertexGLSL,
fragment: fragmentGLSL,
attributes: { aPosition: 0 }
}
})When a backend is explicitly selected, only that backend's source is required.
const pipeline = wgx.createRenderPipeline({
shader,
vertex: {
entryPoint: "vertexMain",
buffers: [{
arrayStride: 12,
attributes: [{ shaderLocation: 0, offset: 0, format: "float32x3" }]
}]
},
fragment: {
entryPoint: "fragmentMain",
blend: {
color: { srcFactor: "src-alpha", dstFactor: "one-minus-src-alpha", operation: "add" },
alpha: { srcFactor: "one", dstFactor: "one-minus-src-alpha", operation: "add" }
},
writeMask: ["red", "green", "blue", "alpha"]
},
depth: {
depthWriteEnabled: true,
depthCompare: "less"
},
stencil: {
front: {
compare: "equal",
failOp: "keep",
depthFailOp: "keep",
passOp: "replace"
},
readMask: 0xff,
writeMask: 0xff
},
primitive: {
topology: "triangle-list",
frontFace: "ccw",
cullMode: "back"
}
})The WebGPU backend creates a native render pipeline. The WebGL2 backend links a program once and lazily caches VAOs by pipeline and buffer binding combination. Optional depth and stencil state is translated to a managed WebGPU depth24plus-stencil8 attachment and the matching WebGL2 depth/stencil state without changing the application-facing descriptor. Fragment blend state and color write masks are also portable; WebGL2 caches the matching blend equations, factors, enablement, and write masks instead of rebinding unchanged output state on each draw.
Indexed line-strip and triangle-strip pipelines must declare primitive.stripIndexFormat as uint16 or uint32. WebGPU fixes this value at pipeline creation time, so WGX validates the same rule on WebGL2 and rejects a bound index buffer whose format does not match.
WebGL2 stencil buffers are opt-in context state. Applications that use portable stencil pipelines on the fallback backend should request one while creating WGX:
const wgx = await createWGX({
canvas,
webgl2: { stencil: true }
})The actual context result is exposed through wgx.capabilities.stencil; WGX rejects stencil pipelines and clears when the selected WebGL2 context does not provide a stencil buffer.
WGX exposes portable RGBA8 textures, samplers, uniform-buffer bindings, sampled textures, and sampler bindings without hiding backend layout differences. With matching @group/@binding declarations in WGSL and GLSL uniform declarations, resources can be described once at the application layer:
const uniforms = wgx.createBuffer({
size: 256,
usage: ["uniform"]
})
const texture = wgx.createTexture({
width: 2,
height: 2,
data: new Uint8Array([
255, 255, 255, 255,
0, 0, 0, 255,
0, 0, 0, 255,
255, 255, 255, 255
])
})
const sampler = wgx.createSampler({
minFilter: "linear",
magFilter: "linear"
})
const bindings = wgx.createBindGroup({
pipeline,
index: 0,
entries: [
{
binding: 0,
buffer: uniforms,
webgl2: { uniformBlock: "Scene" }
},
{
binding: 1,
texture,
webgl2: { uniform: "uTexture", unit: 0 }
},
{
binding: 2,
sampler,
webgl2: { unit: 0 }
}
]
})The binding values map directly to WebGPU shader bindings. WebGL2 has no equivalent bind-group model, so entries that must run on WebGL2 provide explicit GLSL uniform-block, sampler-uniform, and texture-unit mappings instead of relying on naming conventions or shader reflection magic.
Uniform-buffer offsets and ranges are validated against wgx.capabilities.uniformBufferOffsetAlignment. Portable textures currently use rgba8unorm and accept typed RGBA8 pixel data.
A portable texture can also be used as a color render target. Pipelines that render offscreen declare the expected texture format explicitly, while presentation pipelines keep the default canvas target format:
const renderTexture = wgx.createTexture({
width: 1024,
height: 1024,
usage: ["render-attachment", "texture-binding"]
})
const offscreenPipeline = wgx.createRenderPipeline({
shader: sceneShader,
fragment: { targetFormat: "rgba8unorm" }
})
wgx.render({ target: renderTexture, clearColor: [0, 0, 0, 1] }, pass => {
pass.setPipeline(offscreenPipeline)
pass.draw(3)
})The target texture dimensions define the default viewport and the bounds used to validate explicit viewport and scissor rectangles. A pipeline created for canvas cannot be submitted to an rgba8unorm texture target, and vice versa. This keeps WebGPU attachment-format requirements visible in the portable contract instead of hiding them behind backend-specific behavior.
Textures intended for post-processing normally include both render-attachment and texture-binding usage. WGX rejects binding the active render target as a sampled texture in the same pass so WebGPU validation errors and WebGL2 feedback loops fail consistently before drawing. WebGL2 creates and caches the required framebuffer/depth-stencil resources lazily and releases them with the owning texture.
wgx.render({
clearColor: [0, 0, 0, 1],
clearDepth: 1,
clearStencil: 0,
scissor: [32, 32, 640, 360]
}, pass => {
pass.setPipeline(pipeline)
pass.setStencilReference(1)
pass.setBindGroup(0, bindings)
pass.setVertexBuffer(0, vertexBuffer)
pass.setIndexBuffer(indexBuffer, "uint16")
pass.drawIndexed(indices.length)
})WGX enforces a synchronous, scoped render callback. Returning a promise, re-entering render() on the same device, or using a pass after its callback has returned throws before additional backend work is submitted. Vertex and instance ranges are also validated against the bound portable buffers before draw submission.
When depth pipelines are used, clearDepth accepts values from 0 through 1. clearStencil and setStencilReference() accept unsigned 32-bit integer values. WebGPU keeps one managed depth24plus-stencil8 attachment and recreates it when the active canvas or texture target dimensions change. WebGL2 uses the presentation context depth/stencil buffers for canvas rendering and managed framebuffer attachments for texture targets.
scissor uses an integer [x, y, width, height] rectangle and is applied after attachment clears. This matches WebGPU render-pass clear behavior: a previous pipeline color write mask or WebGL2 scissor state cannot accidentally restrict the next pass clear.
All high-level resources are tracked by their creating device:
buffer.destroy()
shader.destroy()
pipeline.destroy()
wgx.dispose()Individual destroy() calls are idempotent. wgx.dispose() releases every still-owned resource and can also be called repeatedly.
The common device lifecycle is observable through state and lost:
console.log(wgx.state) // "ready"
wgx.lost.then(info => {
console.error(info.backend, info.reason, info.message)
})WebGPU device loss and WebGL2 context loss both move the WGX device to "lost". Resources owned by that device must not be reused; create a new WGX device and recreate application resources before rendering again. dispose() transitions the device to "disposed" and remains idempotent.
The native escape hatch is intentionally available, but direct WebGL2 mutations can make WGX's state cache stale. Call invalidateState() after changing WebGL2 bindings or render state through wgx.native so the next WGX render pass rebinds the required state. WebGPU uses explicit command state, so invalidateState() is a validated no-op there.
| API | Purpose |
|---|---|
createWGX |
Select WebGPU/WebGL2 and create one graphics device |
WGXCapabilities |
Normalize important backend limits and feature availability |
createBuffer |
Allocate, update and track portable GPU buffers |
createTexture |
Allocate, update and optionally render into portable RGBA8 textures |
createSampler |
Create portable texture sampling state |
createShader |
Hold backend-specific WGSL/GLSL shader sources/resources |
createRenderPipeline |
Build one portable draw pipeline, including cull/front-face, blend/write-mask and optional depth/stencil state |
createBindGroup |
Bind uniform buffers, textures and samplers to a pipeline |
render |
Encode one canvas or texture render pass with color/depth/stencil clear, viewport and scissor state |
WGXRenderPass |
Bind pipelines/resources, set stencil reference and issue indexed or non-indexed draws |
state / lost |
Observe the common graphics-device lifecycle |
invalidateState |
Reset cached WebGL2 state after direct native mutations |
dispose |
Release all resources owned by the WGX device |
| Capability | WebGPU | WebGL2 |
|---|---|---|
| Automatic backend selection | Yes | Yes, as fallback |
| Vertex/index buffers | Yes | Yes |
| Buffer updates | Yes | Yes |
| Render pipelines | Yes | Yes |
| Indexed drawing | Yes | Yes |
| Instanced drawing | Yes | Yes |
| Portable vertex layouts | Yes | Yes |
| Indexed strip format validation | Yes | Yes |
| Depth compare/write state | Yes, managed depth/stencil attachment | Yes, context depth buffer |
| Depth clearing | Yes | Yes |
| Stencil compare/operations/masks | Yes, managed depth/stencil attachment | Yes, when context stencil buffer is enabled |
| Stencil clearing/reference | Yes | Yes, when context stencil buffer is enabled |
| Color/alpha blending | Yes | Yes |
| Color write masks | Yes | Yes |
| Render-pass scissor rectangles | Yes | Yes |
| RGBA8 texture render targets | Yes | Yes, managed framebuffer |
| Render-target feedback-loop validation | Yes | Yes |
| RGBA8 textures and partial updates | Yes | Yes |
| Portable samplers | Yes | Yes |
| Uniform-buffer bind groups | Yes | Yes, with explicit GLSL block mapping |
| Texture/sampler bind groups | Yes | Yes, with explicit sampler/unit mapping |
| Device/context loss reporting | Yes | Yes |
| Native resource escape hatch | Yes | Yes |
| Native state invalidation | Validated no-op | Resets WGX state cache |
| Compute capability reporting | Yes | Reports unsupported |
| Timestamp-query capability reporting | Feature-dependent | Extension-dependent |
The original WebGL2 helper layer remains available for cases that need direct native constants, textures, framebuffers, uniforms, vertex arrays, context lifecycle handling, or manual resource ownership.
You can import these helpers from the package root or focused subpaths such as @obvia/wgx/framebuffer, @obvia/wgx/uniform, and @obvia/wgx/vertex-array.
| Module | Purpose |
|---|---|
| Attribute | Attribute lookup, binding, enable/disable and instancing |
| Buffer | Generic uploads, partial updates and primitive geometry |
| Canvas | DPR-aware resizing |
| Context | WebGL2 creation and context-loss lifecycle |
| Framebuffer | Texture-backed offscreen render targets |
| Program | Program creation, linking, validation and source pipeline |
| Render | Clear, viewport, indexed and instanced draw calls |
| Shader | Shader creation, compilation, validation and cleanup |
| Texture | 2D texture creation, updates and sampler parameters |
| Toolkit | High-level WebGL2 setup, resource tracking, rendering and cleanup |
| Uniform | Scalars, vectors, matrices, samplers and WebGL2 buffers |
| Vertex Array | Attribute layouts, index bindings and scoped VAO state |
The high-level toolkit removes repeated context arguments, buffer targets, strict flags, and manual cleanup while keeping native WebGL2 resources accessible.
Creates a high-level toolkit for an existing WebGL2 context.
context– Target WebGL2 rendering context
const toolkit = createToolkit(context)Creates a WebGL2 context and toolkit directly from a canvas element.
canvas– Target canvas elementoptions– Standard WebGL2 context attributesstrict– Throw error if WebGL2 cannot be created (default: false)
const toolkit = createToolkitFromCanvas(canvas, {
antialias: true,
powerPreference: "high-performance",
strict: true
})Creates and tracks an ARRAY_BUFFER without repeating the target or strict flag.
data– Vertex buffer dataoptions– Optional buffer configurationusage– Buffer usage hint (default:STATIC_DRAW)
const positionBuffer = toolkit.createVertexBuffer(positions)Creates and tracks an ELEMENT_ARRAY_BUFFER.
const indexBuffer = toolkit.createIndexBuffer(indices)Creates vertex buffers, an optional index buffer, and a vertex array as one drawable resource.
options– Mesh configurationattributes– Vertex attribute layouts and typed datalocation– Shader attribute locationsize– Number of components per vertextype– Component data typedata– Typed vertex datastride– Byte distance between vertices (default: 0)offset– First component byte offset (default: 0)integer– Use integer attribute binding (default: false)divisor– Instancing divisor (default: 0)
count– Number of vertices or indices to drawindices– Optional typed index dataindexType– Optional index component typeindexUsage– Index buffer usage hint (default:STATIC_DRAW)
const mesh = toolkit.createMesh({
attributes: [{
location: 0,
size: 3,
type: context.FLOAT,
data: positions
}],
indices,
indexType: context.UNSIGNED_SHORT,
count: indices.length
})Binds and draws a toolkit mesh without repeating its vertex array, count, or index type.
toolkit.drawMesh(mesh)
toolkit.drawMesh(mesh, { mode: context.LINES, instances: 10 })Compiles, links, validates, and tracks a program from vertex and fragment sources.
const program = toolkit.createProgram({
vertex: vertexSource,
fragment: fragmentSource,
attributes: { aPosition: 0 }
})Creates and tracks a configured 2D texture.
Creates and tracks a texture-backed framebuffer.
Creates and tracks a WebGL2 vertex array.
Executes a scoped framebuffer render pass and restores framebuffer and viewport state.
Executes work with a scoped vertex array binding.
Clears attachments without repeating the context argument.
Issues array, indexed, or instanced drawing without repeating the context argument.
Uses drawing-buffer dimensions by default or accepts explicit dimensions.
Deletes every buffer, program, texture, framebuffer, renderbuffer, and vertex array created by the toolkit. Calling dispose more than once is safe.
toolkit.dispose()This section documents the helper functions for managing vertex attributes, including binding, enabling, disabling, and validating
Binds a vertex attribute to the currently bound buffer and defines how data is read.
context– WebGL rendering context (WebGL2)program– Linked shader programoptions– Attribute binding configurationname– Attribute name in the shader (e.g."aPosition")size– Number of components per attribute (e.g. 2 for vec2, 3 for vec3)type– Data type of each component (e.g.context.FLOAT,context.UNSIGNED_BYTE)stride– Byte offset between consecutive attributes (default: 0)offset– Byte offset of the first component (default: 0)strict– Throw error if attribute is not found (default: false)normalize– Normalize integer data values to [0,1] or [-1,1] (default: false, WebGL2)divisor– Divisor for instanced rendering (default: 0, WebGL2 only)integer– Use integer attribute binding (vertexAttribIPointer) instead of float (default: false, WebGL2 only)
// Vertex shader example:
// in vec3 aPosition;
// Bind the "aPosition" attribute to a buffer (float attribute)
bindAttribute(context, program, { name: "aPosition", size: 3, type: context.FLOAT })
// Enforce strict mode: throw error if missing
bindAttribute(context, program, {
name: "aPosition",
size: 3,
type: context.FLOAT,
strict: true
})
// Normalize unsigned byte colors to [0,1]
bindAttribute(context, program, {
name: "aColor",
size: 4,
type: context.UNSIGNED_BYTE,
normalize: true
})
// Integer attribute binding (WebGL2 only)
// attribute ivec4 aBoneIDs;
bindAttribute(context, program, {
name: "aBoneIDs",
size: 4,
type: context.UNSIGNED_BYTE,
integer: true
})
// Instanced rendering (WebGL2 only)
bindAttribute(context, program, {
name: "aOffset",
size: 2,
type: context.FLOAT,
divisor: 1
})Disables a vertex attribute in the currently linked shader program.
context– WebGL rendering context (WebGL2)program– Linked shader programoptions– Attribute disabling configurationname– Attribute name in the shader (e.g."aTexCoord")strict– Throw error if attribute is not found (default: false)
// Disable the "aTexCoord" attribute when it's not needed
disableAttribute(context, program, { name: "aTexCoord" })
// Enforce strict mode: throw error if missing
disableAttribute(context, program, {
name: "aTexCoord",
strict: true
})Enables a vertex attribute in the currently linked shader program.
context– WebGL rendering context (WebGL2)program– Linked shader programoptions– Attribute enabling configurationname– Attribute name in the shader (e.g."aPosition")strict– Throw error if attribute is not found (default: false)
// Enable the "aPosition" attribute
enableAttribute(context, program, { name: "aPosition" })
// Enforce strict mode: throw error if missing
enableAttribute(context, program, {
name: "aPosition",
strict: true
})Validates a vertex attribute by checking its location in the linked shader program.
context– WebGL rendering context (WebGL2)program– Linked shader programoptions– Validation configurationname– Attribute name in the shader (e.g."aPosition")strict– Throw error if attribute is not found (default: false)
// Validate attribute existence
const location = validateAttribute(context, program, { name: "aPosition" })
// Enforce strict mode: throw error if missing
const location2 = validateAttribute(context, program, {
name: "aPosition",
strict: true
})
// Use location in subsequent binding
if (location !== -1) {
context.enableVertexAttribArray(location)
}By centralizing validation and using a consistent options‑object pattern, it reduces boilerplate, eliminates code duplication, and ensures a clean, predictable API for creating, updating, deleting, and binding buffers.
Creates a generic WebGL buffer and uploads data.
context– WebGL rendering contextoptions– Buffer configurationtarget– Buffer target (ARRAY_BUFFERorELEMENT_ARRAY_BUFFER)data– Vertex or index data (typed array)usage– Buffer usage hint (default:STATIC_DRAW)strict– Throw error if buffer cannot be created (default: false)
// Silent mode (default): returns null if buffer cannot be created
const vertices = new Float32Array([0,0, 1,0, 0,1])
const vboDefault = createBuffer(context, {
target: context.ARRAY_BUFFER,
data: vertices
})
// Strict mode: throws an error if buffer cannot be created
const indices = new Uint16Array([0,1,2, 2,1,3])
const iboStrict = createBuffer(context, {
target: context.ELEMENT_ARRAY_BUFFER,
data: indices,
strict: true
})
// Custom usage hint (dynamic draw)
const dynamicVertices = new Float32Array([0,0, 1,0, 0,1])
const vboDynamic = createBuffer(context, {
target: context.ARRAY_BUFFER,
data: dynamicVertices,
usage: context.DYNAMIC_DRAW
})Creates a buffer for a full‑screen quad.
context– WebGL rendering contextposition– Attribute location index for vertex positionsoptions– Optional configurationstrict– Throw error if buffer cannot be created (default: false)
// Silent mode (default): returns null if buffer cannot be created
const vboDefault = createQuadBuffer(context, positionLocation)
context.drawArrays(context.TRIANGLE_STRIP, 0, 4)
// Strict mode: throws an error if buffer cannot be created
const vboStrict = createQuadBuffer(context, positionLocation, { strict: true })
context.drawArrays(context.TRIANGLE_STRIP, 0, 4)
// With index buffer
const vbo = createQuadBuffer(context, positionLocation)
const ibo = createQuadIndexBuffer(context)
context.drawElements(context.TRIANGLES, 6, context.UNSIGNED_SHORT, 0)Creates an index buffer for a full‑screen quad.
context– WebGL rendering contextoptions– Optional configurationstrict– Throw error if buffer cannot be created (default: false)
// Silent mode (default): returns null if buffer cannot be created
const vbo = createQuadBuffer(context, positionLocation)
const iboDefault = createQuadIndexBuffer(context)
context.drawElements(context.TRIANGLES, 6, context.UNSIGNED_SHORT, 0)
// Strict mode: throws an error if buffer cannot be created
const iboStrict = createQuadIndexBuffer(context, { strict: true })
context.drawElements(context.TRIANGLES, 6, context.UNSIGNED_SHORT, 0)Creates a buffer for a full‑screen triangle.
context– WebGL rendering contextposition– Attribute location index for vertex positionsoptions– Optional configurationstrict– Throw error if buffer cannot be created (default: false)
// Silent mode (default): returns null if buffer cannot be created
const vboDefault = createTriangleBuffer(context, positionLocation)
context.drawArrays(context.TRIANGLES, 0, 3)
// Strict mode: throws an error if buffer cannot be created
const vboStrict = createTriangleBuffer(context, positionLocation, { strict: true })
context.drawArrays(context.TRIANGLES, 0, 3)Creates an index buffer for a full‑screen triangle.
context– WebGL rendering contextoptions– Optional configurationstrict– Throw error if buffer cannot be created (default: false)
// Silent mode (default): returns null if buffer cannot be created
const vbo = createTriangleBuffer(context, positionLocation)
const iboDefault = createTriangleIndexBuffer(context)
context.drawElements(context.TRIANGLES, 3, context.UNSIGNED_SHORT, 0)
// Strict mode: throws an error if buffer cannot be created
const iboStrict = createTriangleIndexBuffer(context, { strict: true })
context.drawElements(context.TRIANGLES, 3, context.UNSIGNED_SHORT, 0)Creates a buffer for a unit cube.
context– WebGL rendering contextposition– Attribute location index for vertex positionsoptions– Optional configurationstrict– Throw error if buffer cannot be created (default: false)
// Silent mode (default): returns null if buffer cannot be created
const vboDefault = createCubeBuffer(context, positionLocation)
const iboDefault = createCubeIndexBuffer(context)
context.drawElements(context.TRIANGLES, 36, context.UNSIGNED_SHORT, 0)
// Strict mode: throws an error if buffer cannot be created
const vboStrict = createCubeBuffer(context, positionLocation, { strict: true })
const iboStrict = createCubeIndexBuffer(context, { strict: true })
context.drawElements(context.TRIANGLES, 36, context.UNSIGNED_SHORT, 0)Creates an index buffer for a cube.
context– WebGL rendering contextoptions– Optional configurationstrict– Throw error if buffer cannot be created (default: false)
// Silent mode (default): returns null if buffer cannot be created
const vboDefault = createCubeBuffer(context, positionLocation)
const iboDefault = createCubeIndexBuffer(context)
context.drawElements(context.TRIANGLES, 36, context.UNSIGNED_SHORT, 0)
// Strict mode: throws an error if buffer cannot be created
const vboStrict = createCubeBuffer(context, positionLocation, { strict: true })
const iboStrict = createCubeIndexBuffer(context, { strict: true })
context.drawElements(context.TRIANGLES, 36, context.UNSIGNED_SHORT, 0)Updates an existing WebGL buffer with new data.
context– WebGL rendering contextbuffer– Existing buffer to updateoptions– Buffer configurationtarget– Buffer target (ARRAY_BUFFERorELEMENT_ARRAY_BUFFER)data– New typed array data to uploadusage– Buffer usage hint (default:STATIC_DRAW)strict– Throw error if buffer binding fails (default: false)
// Replace the entire buffer with new vertices
const newVertices = new Float32Array([0,0, 1,0, 1,1])
updateBuffer(context, vbo, {
target: context.ARRAY_BUFFER,
data: newVertices
})
// Resize the buffer with more vertices (old data discarded)
const largerVertices = new Float32Array([0,0, 1,0, 1,1, 0,1])
updateBuffer(context, vbo, {
target: context.ARRAY_BUFFER,
data: largerVertices,
usage: context.DYNAMIC_DRAW
})
// Strict mode: throws an error if buffer binding fails
updateBuffer(context, vbo, {
target: context.ARRAY_BUFFER,
data: newVertices,
strict: true
})Partially updates an existing WebGL buffer with new data.
context– WebGL rendering contextbuffer– Existing buffer to updateoptions– Buffer configurationtarget– Buffer target (ARRAY_BUFFERorELEMENT_ARRAY_BUFFER)data– Typed array containing new dataoffset– Byte offset in the buffer where data should be written (default: 0)strict– Throw error if buffer binding fails (default: false)
// Replace only the first vertex (two floats) in the buffer
const newVertex = new Float32Array([0.5, 0.5])
updateBufferPartial(context, vbo, {
target: context.ARRAY_BUFFER,
data: newVertex,
offset: 0
})
// Replace the 3rd vertex (offset = 2 * 4 bytes = 8)
const anotherVertex = new Float32Array([1.0, 1.0])
updateBufferPartial(context, vbo, {
target: context.ARRAY_BUFFER,
data: anotherVertex,
offset: 8,
strict: true
})Deletes a WebGL buffer and frees GPU memory.
context– WebGL rendering contextbuffer– Buffer object to deleteoptions– Optional configurationstrict– Throw error if buffer deletion fails (default: false)
// Silent mode (default): ignores if buffer is null
deleteBuffer(context, vbo)
// Strict mode: throws an error if buffer is null or deletion fails
deleteBuffer(context, vbo, { strict: true })
// Delete both vertex and index buffers when cleaning up
deleteBuffer(context, vbo)
deleteBuffer(context, ibo)By centralizing context creation and resize logic, it provides a unified, predictable API for obtaining WebGL contexts and managing canvas scaling. This reduces repetitive setup code, ensures consistent handling of device pixel ratios, and simplifies error management with optional strict mode.
Safely obtain a WebGL rendering context from a canvas element.
canvas– Target canvas element to initialize WebGL onoptions– Optional configuration (extends WebGL context attributes)strict– Throw error if WebGL cannot be created (default: false)*– Inherits all standard WebGL context attributes
// Silent mode (default): returns null if WebGL cannot be created
const contextDefault = canvasContext(canvas)
// Strict mode: throws an error if WebGL cannot be created
const contextStrict = canvasContext(canvas, { strict: true })
// Custom options (disable antialias, depth buffer)
const contextCustom = canvasContext(canvas, { antialias: false, depth: false })
// Create a WebGL2 context
const context = canvasContext(canvas)
if (!contextDefault) {
// Handle unavailable WebGL2 support
}Resize a canvas element based on its bounding box and device pixel ratio (DPR).
canvas– Target canvas element to resizeoptions– Optional configurationmax– Maximum constraintsdpr– Maximum device pixel ratio (default: 1.5)width– Maximum canvas width (default: Infinity)height– Maximum canvas height (default: Infinity)
min– Minimum constraintsdpr– Minimum device pixel ratio (default: 1)width– Minimum canvas width (default: 1)height– Minimum canvas height (default: 1)
source– Device pixel ratio source (default:window.devicePixelRatio || 1)autoResize– Automatically resize on window resize events (default: false)onResize– Callback invoked after resize with new width/heightrounding– Rounding strategy for DPR scaling ("floor" | "round" | "ceil")
// Default usage (uses window.devicePixelRatio)
resizeCanvas(canvas)
// Custom DPR source
resizeCanvas(canvas, { source: 2 })
// Custom min/max constraints
resizeCanvas(canvas, { max: { dpr: 2 }, min: { width: 10, height: 10 } })
// Auto resize with callback
resizeCanvas(canvas, {
autoResize: true,
onResize: (w, h) => console.log("Resized :", w, h),
rounding: "round"
})By centralizing uniform setting logic, these helpers provide a unified, predictable API for assigning values to shader uniforms. This reduces repetitive code, ensures consistent type handling, and simplifies error management with optional strict mode.
Sets a single‑component uniform (float, int, boolean, or array).
context– WebGL rendering context (WebGL2)program– Linked shader programoptions– Uniform configurationname– Uniform name in the shadervalue– Value to assign (number | boolean | Float32Array | Int32Array)strict– Throw error if uniform location is not found (default: false)
// Float uniform
setUniform1(context, program, { name: "uTime", value: performance.now() / 1000 })
// Integer uniform
setUniform1(context, program, { name: "uEnabled", value: 1 })
// Boolean uniform
setUniform1(context, program, { name: "uFlag", value: true })
// Float array uniform
setUniform1(context, program, { name: "uWeights", value: new Float32Array([0.1, 0.2, 0.3]) })
// Int array uniform
setUniform1(context, program, { name: "uIndices", value: new Int32Array([1, 2, 3]) })
// Strict mode
setUniform1(context, program, { name: "uMissing", value: 0, strict: true })Sets a two‑component uniform (vec2).
context– WebGL rendering context (WebGL2)program– Linked shader programoptions– Uniform configurationname– Uniform name in the shadervalue– Value to assign ([number, number] | boolean[] | Float32Array | Int32Array)strict– Throw error if uniform location is not found (default: false)
// Float vec2 uniform
setUniform2(context, program, {
name: "uOffset",
value: [0.5, 1.2]
})
// Integer vec2 uniform
setUniform2(context, program, {
name: "uCoords",
value: [3, 7]
})
// Boolean vec2 uniform
setUniform2(context, program, {
name: "uFlags",
value: [true, false]
})
// Float array uniform
setUniform2(context, program, {
name: "uWeights",
value: new Float32Array([0.1, 0.2])
})
// Int array uniform
setUniform2(context, program, {
name: "uIndices",
value: new Int32Array([1, 2])
})
// Strict mode example
setUniform2(context, program, {
name: "uMissing",
value: [0, 0],
strict: true
})Sets a three‑component uniform (vec3).
context– WebGL rendering context (WebGL2)program– Linked shader programoptions– Uniform configurationname– Uniform name in the shadervalue– Value to assign ([number, number, number] | boolean[] | Float32Array | Int32Array)strict– Throw error if uniform location is not found (default: false)
// Float vec3 uniform
setUniform3(context, program, {
name: "uColor",
value: [1.0, 0.5, 0.0]
})
// Integer vec3 uniform
setUniform3(context, program, {
name: "uCoords",
value: [10, 20, 30]
})
// Boolean vec3 uniform
setUniform3(context, program, {
name: "uFlags",
value: [true, false, true]
})
// Float array uniform
setUniform3(context, program, {
name: "uWeights",
value: new Float32Array([0.1, 0.2, 0.3])
})
// Int array uniform
setUniform3(context, program, {
name: "uIndices",
value: new Int32Array([1, 2, 3])
})
// Strict mode example
setUniform3(context, program, {
name: "uMissing",
value: [0, 0, 0],
strict: true
})Sets a four‑component uniform (vec4).
context– WebGL rendering context (WebGL2)program– Linked shader programoptions– Uniform configurationname– Uniform name in the shadervalue– Value to assign ([number, number, number, number] | boolean[] | Float32Array | Int32Array)strict– Throw error if uniform location is not found (default: false)
// Float vec4 uniform
setUniform4(context, program, {
name: "uColor",
value: [1.0, 0.5, 0.0, 1.0]
})
// Integer vec4 uniform
setUniform4(context, program, {
name: "uCoords",
value: [10, 20, 30, 40]
})
// Boolean vec4 uniform
setUniform4(context, program, {
name: "uFlags",
value: [true, false, true, false]
})
// Float array uniform
setUniform4(context, program, {
name: "uWeights",
value: new Float32Array([0.1, 0.2, 0.3, 0.4])
})
// Int array uniform
setUniform4(context, program, {
name: "uIndices",
value: new Int32Array([1, 2, 3, 4])
})
// Strict mode example
setUniform4(context, program, {
name: "uMissing",
value: [0, 0, 0, 0],
strict: true
})Binds a uniform block to a binding point in a WebGL2 shader program.
context– WebGL2 rendering contextprogram– Linked shader programoptions– Uniform block configurationname– Uniform block name in the shaderindex– Binding point indexstrict– Throw error if uniform block is not found (default: false)
// Bind uniform block "Matrices" to binding point 0
setUniformBlock(context, program, {
name: "Matrices",
index: 0
})
// With strict mode enabled
setUniformBlock(context, program, {
name: "Matrices",
index: 0,
strict: true
})Binds a buffer to a uniform binding point in a WebGL2 shader program.
context– WebGL2 rendering contextoptions– Uniform buffer configurationbuffer– WebGLBuffer to bindindex– Binding point indexstrict– Throw error if buffer binding fails (default: false)
// Create buffer and bind to binding point 0
const buffer = createBuffer(context, {
target: context.UNIFORM_BUFFER,
data: new Float32Array([0.1, 0.2, 0.3, 0.4])
})
setUniformBuffer(context, {
buffer,
index: 0
})
// With strict mode enabled
setUniformBuffer(context, {
buffer,
index: 0,
strict: true
})Sets matrix uniforms (mat2, mat3, mat4, and WebGL2 non‑square variants).
context– WebGL rendering context (WebGL2 required for non‑square matrices)program– Linked shader programoptions– Matrix uniform configurationname– Uniform name in the shadermatrix– Matrix values as aFloat32Arraytranspose– Whether to transpose the matrix before uploading (default: false)strict– Throw error if uniform location or type is not found (default: false)
// mat2 (2×2 identity matrix)
const mat2 = new Float32Array([1, 0, 0, 1])
setUniformMatrix(context, program, { name: "uMat2", matrix: mat2 })
// mat3 (3×3 identity matrix with strict mode)
const mat3 = new Float32Array([1,0,0, 0,1,0, 0,0,1])
setUniformMatrix(context, program, { name: "uMat3", matrix: mat3, strict: true })
// mat4 (4×4 identity matrix with transpose)
const mat4 = new Float32Array([
1,0,0,0,
0,1,0,0,
0,0,1,0,
0,0,0,1
])
setUniformMatrix(context, program, { name: "uMat4", matrix: mat4, transpose: true })
// WebGL2: mat2x3
const mat2x3 = new Float32Array([1,0,0, 0,1,0])
setUniformMatrix(gl2Context, program, { name: "uMat2x3", matrix: mat2x3 })
// WebGL2: mat3x2
const mat3x2 = new Float32Array([1,0,0, 0,1,0])
setUniformMatrix(gl2Context, program, { name: "uMat3x2", matrix: mat3x2 })
// WebGL2: mat2x4
const mat2x4 = new Float32Array([1,0,0,0, 0,1,0,0])
setUniformMatrix(gl2Context, program, { name: "uMat2x4", matrix: mat2x4 })
// WebGL2: mat4x2
const mat4x2 = new Float32Array([1,0, 0,1, 0,0, 0,0])
setUniformMatrix(gl2Context, program, { name: "uMat4x2", matrix: mat4x2 })
// WebGL2: mat3x4
const mat3x4 = new Float32Array([1,0,0,0, 0,1,0,0, 0,0,1,0])
setUniformMatrix(gl2Context, program, { name: "uMat3x4", matrix: mat3x4 })
// WebGL2: mat4x3
const mat4x3 = new Float32Array([1,0,0, 0,1,0, 0,0,1, 0,0,0])
setUniformMatrix(gl2Context, program, { name: "uMat4x3", matrix: mat4x3 })Sets a sampler uniform (texture unit binding).
context– WebGL rendering context (WebGL2)program– Linked shader programoptions– Sampler uniform configurationname– Sampler uniform name in the shader (e.g."uTexture")unit– Texture unit index (must be ≥ 0, e.g.0forTEXTURE0)strict– Throw error if uniform location is not found (default: false)
// Bind texture to unit 0 and assign sampler
context.activeTexture(context.TEXTURE0)
context.bindTexture(context.TEXTURE_2D, texture)
setUniformSampler(context, program, {
name: "uTexture",
unit: 0
})
// With strict mode enabled
setUniformSampler(context, program, {
name: "uTexture",
unit: 0,
strict: true
})Sets unsigned integer uniforms (1–4 components).
context– WebGL2 rendering contextprogram– Linked shader programoptions– Unsigned integer uniform configurationname– Uniform name in the shader (e.g."uCount")values– One to four unsigned integer values[number]→uniform1ui[number, number]→uniform2ui[number, number, number]→uniform3ui[number, number, number, number]→uniform4ui
strict– Throw error if uniform location is not found (default: false)
// Single unsigned int
setUniformUi(context, program, { name: "uCount", values: [5] })
// Two unsigned ints
setUniformUi(context, program, { name: "uCoords", values: [10, 20] })
// Three unsigned ints
setUniformUi(context, program, { name: "uTriple", values: [1, 2, 3] })
// Four unsigned ints
setUniformUi(context, program, { name: "uColor", values: [255, 128, 64, 32] })
// Strict mode example
setUniformUi(context, program, { name: "uMissing", values: [0], strict: true })Creates a WebGL2 rendering context from a canvas element.
canvas– Target canvas elementoptions– Optional WebGL2 context configuration*– All standardWebGLContextAttributesstrict– Throw error if WebGL2 cannot be created (default: false)
const context = canvasContext(canvas, {
antialias: true,
alpha: false,
strict: true
})Subscribes to webglcontextlost and webglcontextrestored and returns an unsubscribe function.
canvas– Canvas that owns the WebGL2 contextoptions– Context lifecycle configurationonLost– Callback invoked when the context is lostonRestored– Callback invoked when the context is restoredpreventDefault– Allow browser restoration after context loss (default: true)
const unsubscribe = observeContext(canvas, {
onLost: () => stopRendering(),
onRestored: () => rebuildGpuResources()
})
unsubscribe()Compiles and links a complete program, supports deterministic attribute locations and WebGL2 transform-feedback varyings, then releases intermediate shaders.
context– Target WebGL2 rendering contextoptions– Program source configurationvertex– Vertex shader GLSL sourcefragment– Fragment shader GLSL sourceattributes– Optional attribute name-to-location mappingtransformFeedbackVaryings– Optional transform feedback outputstransformFeedbackMode– Transform feedback storage mode (default:INTERLEAVED_ATTRIBS)strict– Throw error if compilation or linking fails (default: false)
const program = createProgramFromSources(context, {
vertex: vertexSource,
fragment: fragmentSource,
attributes: { aPosition: 0, aNormal: 1 },
strict: true
})Low-level helpers are also available: createProgram, linkProgram, validateProgram, useProgram, and deleteProgram.
Use createShader, compileShader, validateShader, attachShader, detachShader, and deleteShader when manual shader lifecycle control is required. Prefer createProgramFromSources for the common path.
Creates a configured 2D texture from any TexImageSource. Passing null creates a transparent 1×1 placeholder. Creation returns null by default or throws with strict: true.
context– Target WebGL2 rendering contextimage– Texture image source ornullfor a transparent placeholderoptions– Texture configurationinternalFormat– Internal texture format (default:RGBA)format– Source data format (default:RGBA)type– Source data type (default:UNSIGNED_BYTE)wrapS– Horizontal wrapping mode (default:CLAMP_TO_EDGE)wrapT– Vertical wrapping mode (default:CLAMP_TO_EDGE)minFilter– Minification filter (default:LINEAR)magFilter– Magnification filter (default:LINEAR)generateMipmap– Generate texture mipmaps (default: false)strict– Throw error if creation fails (default: false)
const texture = createTexture2D(context, image, {
minFilter: context.LINEAR_MIPMAP_LINEAR,
generateMipmap: true,
strict: true
})Use updateTexture for dynamic image/video frames and setTextureParameters for an already-bound texture.
Creates a texture-backed offscreen target with an optional depth renderbuffer. Use withFramebuffer to restore framebuffer and viewport state reliably.
context– Target WebGL2 rendering contextoptions– Framebuffer configurationwidth– Framebuffer width in pixelsheight– Framebuffer height in pixelsinternalFormat– Color texture internal format (default:RGBA)format– Color texture format (default:RGBA)type– Color texture data type (default:UNSIGNED_BYTE)depth– Create a depth renderbuffer (default: true)minFilter– Texture minification filter (default:LINEAR)magFilter– Texture magnification filter (default:LINEAR)strict– Throw error if creation fails (default: false)
const target = createFramebuffer(context, { width: 1024, height: 1024, depth: true, strict: true })!
withFramebuffer(context, target, () => {
clear(context, { color: [0, 0, 0, 0], depth: 1 })
renderScene()
})
deleteFramebuffer(context, target)Binds a framebuffer for a scoped render pass and always restores the previous framebuffer and viewport.
context– Target WebGL2 rendering contextresource– Framebuffer resource ornullfor the default framebufferrenderCallback– Callback executed while the framebuffer is active
Deletes the framebuffer, color texture, and optional depth renderbuffer.
context– Target WebGL2 rendering contextresource– Framebuffer resource to delete
Creates a WebGL2 vertex array and captures attribute and index buffer bindings.
context– Target WebGL2 rendering contextoptions– Vertex array configurationattributes– Array of vertex attribute descriptorslocation– Shader attribute locationsize– Number of components per vertextype– Component data typebuffer– Source array bufferstride– Byte distance between vertices (default: 0)offset– First component byte offset (default: 0)normalize– Normalize fixed-point values (default: false)integer– Use integer attribute binding (default: false)divisor– Instancing divisor (default: 0)
indexBuffer– Optional element array bufferstrict– Throw error if creation fails (default: false)
const vertexArray = createVertexArray(context, {
attributes: [{
location: 0,
size: 3,
type: context.FLOAT,
buffer: positionBuffer
}],
indexBuffer,
strict: true
})Binds a vertex array for a scoped operation and always restores the previous binding.
context– Target WebGL2 rendering contextvertexArray– Vertex array object to bindrenderCallback– Callback executed while the vertex array is active
Deletes a vertex array when it is no longer needed.
context– Target WebGL2 rendering contextvertexArray– Vertex array object to delete
Clears color and, when provided, depth and stencil attachments.
context– Target WebGL2 rendering contextoptions– Clear attachment valuescolor– RGBA clear color (default:[0, 0, 0, 0])depth– Optional depth clear valuestencil– Optional stencil clear value
clear(context, { color: [0.02, 0.03, 0.05, 1], depth: 1 })Sets the viewport to the drawing buffer or explicit dimensions.
context– Target WebGL2 rendering contextwidth– Viewport width (default: drawing buffer width)height– Viewport height (default: drawing buffer height)
setViewport(context)
setViewport(context, 1920, 1080)Dispatches array/indexed drawing and uses WebGL2 instanced drawing when instances is greater than one.
context– Target WebGL2 rendering contextoptions– Draw configurationmode– Primitive drawing modecount– Number of vertices or indicesfirst– First vertex for non-indexed drawing (default: 0)type– Optional index type for indexed drawingoffset– Index buffer byte offset (default: 0)instances– Number of instances (default: 1)
draw(context, { mode: context.TRIANGLES, count: 36, type: context.UNSIGNED_SHORT })
draw(context, { mode: context.TRIANGLES, count: 6, instances: 100 })Creates a controllable animation-frame render loop with clamped delta time.
renderCallback– Callback invoked for every animation frametime– Current high-resolution timestamp in millisecondsdelta– Time since the previous frame in seconds, clamped to 0.1frame– Number of frames rendered since the loop started
const renderLoop = createRenderLoop(({ time, delta, frame }) => {
updateScene(delta)
renderScene(time, frame)
})
renderLoop.start()
renderLoop.stop()Resource creators use a consistent two-mode contract:
- Default mode returns
null/falsewhen an operation cannot complete. strict: truethrows a typed or native error with operation details.
GPU resources remain caller-owned. Delete buffers, shaders, programs, textures and framebuffers when no longer needed.
- WebGPU is used when available and selected; WebGL2 is the portable rendering fallback.
- TypeScript declarations are bundled with the package.
- ESM and CommonJS builds are provided.
- Bun is the package, test, benchmark and build workflow runtime; rendering requires a browser graphics context.
bun install
bun run check
bun run test
bun run coverage
bun run build
bun run packTests run with bun:test; DOM and canvas globals are registered through very-happy-dom. Builds are produced with tsdown for ESM and CommonJS consumers.
- Run
bun run check,bun run test,bun run coverage, andbun run pack. - Confirm
package.jsonandchangelog.mdcontain the target version. - Commit, tag the release as
v0.8.1, and publish the GitHub release. - GitHub Actions re-runs Bun tests, TypeScript checks, and the tsdown build before publishing with npm provenance.
MIT © 2026 Selçuk Çukur. See license.md.