JavaScript API
The runtime is a set of tree-shakeable ESM functions: an instance handle comes
out of pngine(), and every other function takes it as its first argument.
All GPU work runs in a WebWorker over an OffscreenCanvas.
What it plays is a payload the CLI compiled: PNGB, PNGine’s compact bytecode,
normally travelling inside a PNG’s pNGb chunk together with the executor, the
small WASM interpreter that turns that bytecode into GPU commands.
Profiles
Section titled “Profiles”The npm package ships six player profiles (its other subpaths are the compiler, the raw worker and wasm files, and the schema). Sizes are production measurements (2026-08-18, gzip in parentheses), each gated by a hard budget in the bundler:
| Import | Size | What it is |
|---|---|---|
pngine |
49.6 KB (17.0) | The production viewer. Worker inlined; accepts payloads with an embedded executor only |
pngine/dev |
54.1 KB (18.5) | The viewer plus what it deliberately rejects: selector and <img> sources, a pngine.wasm fetch fallback, prefetch, executor helpers |
pngine/mini |
7.1 KB (3.3) | Main-thread player for flat pNGf payloads (pre-decoded command buffers written by --flat, no executor), audio included |
pngine/mini-no-audio |
6.4 KB (3.0) | mini with the audio path compiled out |
pngine/core |
26.6 KB (9.0) | GPU dispatcher and loader only: no worker, no animation loop |
pngine/executor |
1.9 KB (0.9) | Payload splitting and executor instantiation |
Node resolution gets stubs that throw: the runtime is browser-only. This page
covers pngine and pngine/dev; pngine/core and pngine/executor are
documented in the WASM API, and choosing
between the tiers in Shipping a Player.
Initialization
Section titled “Initialization”pngine(source, options)
Section titled “pngine(source, options)”import { pngine, play } from "pngine";
const p = await pngine("shader.png", { canvas: document.getElementById("canvas"),});play(p);Sources: a URL string, ArrayBuffer, Uint8Array, or Blob. The bytes
may be a PNG with a pNGb chunk, raw PNGB, or a ZIP bundle; the loader
detects the container.
Options:
| Option | Type | Default | Description |
|---|---|---|---|
canvas |
HTMLCanvasElement | required | Target canvas; a missing one throws Error("viewer pngine() requires options.canvas") |
debug |
boolean | false |
[Worker] / [GPU] console logging |
dpr |
number | device | Device-pixel-ratio override for the backing store |
autoResize |
boolean | false |
Leave the canvas CSS size to the page and keep the backing store synced to its rendered size |
onError |
function | - | Receives worker errors, shader errors and GPU errors after init (see Error handling) |
The default profile is strict, and the rejections are part of its contract;
each one throws a plain Error whose message points at pngine/dev:
- CSS-selector and
HTMLImageElementsources - the
wasmUrloption - payloads without an embedded executor
pngine/dev
Section titled “pngine/dev”Same API, looser contract, for development and tooling:
import { pngine, play } from "pngine/dev";
const p = await pngine("#shader-img"); // selector or <img> element:play(p); // canvas created over the image
const q = await pngine("legacy.png", { // payload without an embedded canvas, // executor: fetch the shared wasmUrl: "/pngine.wasm", // runtime instead});pngine/dev additionally re-exports the executor helpers (parsePayload,
createExecutor, getExecutorImports, getExecutorVariantName).
Playback
Section titled “Playback”| Function | Effect |
|---|---|
play(p) |
Start the animation loop (requestAnimationFrame) |
pause(p) |
Stop the loop, keep the current time |
stop(p) |
Stop the loop, reset time to 0 |
seek(p, time) |
Jump to a time in seconds (audio seeks with it) and draw |
draw(p, opts?) |
Render one frame |
setFrame(p, frame) |
Pin which (frame …) renders; null releases the pin |
restart(p) |
Rebuild the GPU device and replay the loaded bytecode (see Device loss) |
destroy(p) |
Tear down: listeners, worker, GPU device |
Every function is a no-op on a destroyed instance except draw, which throws.
draw options:
| Option | Type | Description |
|---|---|---|
time |
number | Time in seconds (default: current playback time) |
frame |
string | null | Frame name; null lets the animation timeline resolve it |
uniforms |
object | Uniform values written for this draw |
draw(p); // current timedraw(p, { time: 1.5 }); // explicit timedraw(p, { frame: "intro" }); // one named framedraw(p, { uniforms: { brightness: 0.8 } }); // with uniform valuesCall destroy(p) when removing an instance: the worker, the GPU device and
seven input listeners (five on the canvas, two on the document, for the pointer
and keyboard state) outlive garbage collection otherwise.
Uniforms
Section titled “Uniforms”Uniform names come from the WGSL struct fields of buffers the document binds; the compiler reflects them into the payload’s uniform table.
setUniform(p, name, value) / setUniforms(p, uniforms)
Section titled “setUniform(p, name, value) / setUniforms(p, uniforms)”setUniform(p, "brightness", 0.5);setUniforms(p, { brightness: 0.8, color: [1.0, 0.5, 0.0] });Values ride a draw: each call renders one frame with the values written into
the uniform buffer, where they persist for subsequent frames. Both take a
trailing redraw boolean that defaults to true; passing false skips the
draw and therefore discards the values; there is no deferred store.
A caveat that looks like a runtime bug but is a compile-time one: if
reflection failed for a module (reported as a validate warning), its fields
are missing from the uniform table and setUniform on them does nothing.
getUniforms(p)
Section titled “getUniforms(p)”Resolves to the uniform metadata, {} before init or after 2 s without a
worker reply:
const uniforms = await getUniforms(p);// { time: { type: "f32", size: 4, bufferId: 0, offset: 0 }, … }getStats(p)
Section titled “getStats(p)”Live resource counts, the instrument for long-running sessions:
const { gpu, wasmBytes, frameCount, moduleLoaded, deviceLost } = await getStats(p);gpu.live counts the GPU objects held right now, by kind; gpu.executed
counts command buffers run; wasmBytes is the executor’s memory size. Over a
soak, gpu.live stays flat while gpu.executed climbs, and wasmBytes never
changes: the embedded executor has no allocator, so growth there is itself a
bug. Resolves to { gpu: null, … } on a destroyed instance.
Device loss
Section titled “Device loss”The GPU device can be lost under a running instance (driver reset, GPU
eviction). onError receives a PngineGPUError whose source is
"device-lost". The pngine viewer then recovers on its own: the worker
acquires a fresh device and replays the loaded bytecode. pngine/dev stays
passive (draws become no-ops) until the host calls restart(p).
Properties
Section titled “Properties”Read-only, safe on a destroyed instance:
| Property | Description |
|---|---|
p.width / p.height |
Canvas size in logical pixels |
p.time |
Current playback time in seconds |
p.isPlaying |
Loop state |
p.frameCount |
Number of (frame …) definitions in the payload |
p.duration |
Animation-table duration in seconds, 0 without one |
p.animation |
Animation-table metadata, null without one |
p.currentScene / p.currentFrame |
Timeline position, null without one |
p.audio |
The audio player when the payload carries a pNGa chunk (an audio WASM module) |
Animation timeline
Section titled “Animation timeline”The runtime carries a timeline reader (p.animation, p.currentScene,
p.duration), but no form of SJON, the S-expression source language, emits
a timeline today. There is no
(animation …) or scene-scheduling form in the schema, so on any payload
compiled from .sjon these read empty: p.animation is null and
p.duration is 0.
To sequence scenes, declare several
(frame …) forms and switch between them from
JavaScript:
const scenes = [ { name: "introFrame", until: 10 }, { name: "mainFrame", until: 25 }, { name: "outroFrame", until: 30 },];
function tick() { const t = p.time % 30; setFrame(p, scenes.find((s) => t < s.until).name); requestAnimationFrame(tick);}tick();Mini player
Section titled “Mini player”pngine/mini plays flat pNGf payloads (produced with pngine … --flat) on
the main thread, with no worker and no WASM executor:
import { miniPngine } from "pngine/mini";
const player = await miniPngine(canvas, "art.png", { autoplay: true, onError: (err) => console.error(err),});Setup failures reject the promise: no WebGPU adapter, no pNGf chunk, or a
chunk whose version byte is not 2. Version 2 is the current one; a --flat PNG
exported before clear values became one f32 per channel encodes its pass
commands differently, and the player refuses it rather than walking a stream it
would misread. Re-export it with the current CLI.
Runtime failures stop the loop and go to onError. The mini player implements
a subset of the command set by design; the flat writer refuses to emit a
payload it cannot play, so an unsupported-opcode error means a corrupt or
hand-built payload; re-export without --flat and use the full viewer.
Error handling
Section titled “Error handling”const p = await pngine("shader.png", { canvas, onError: (err) => { if (err.name === "PngineShaderError") { // WGSL compilation: err.lineNum, err.linePos, err.context } else if (err.name === "PngineGPUError") { // WebGPU uncaptured error or device loss (err.source === "device-lost") } },});Init failures reject the pngine() promise; everything after init routes to
onError.
Exports
Section titled “Exports”// pngine: the production viewerexport { pngine, destroy } from "./viewer-init.js";export { draw, play, pause, stop, seek, setFrame, setUniform, setUniforms, getUniforms, getStats, restart,} from "./anim.js";export { extractBytecode, detectFormat, isPng, isZip, isPngb } from "./extract.js";
// pngine/dev: all of the above, plusexport { parsePayload, createExecutor, getExecutorImports, getExecutorVariantName } from "./loader.js";
// pngine/miniexport { miniPngine };Related
Section titled “Related”- Getting Started - First program, browser setup
- Shipping a Player - Choosing a profile and payload format
- WASM API -
pngine/core,pngine/executor, the compiler (frame …)- Frame definitionssetFrameselects(queue …)- The runtime data sources behind the built-in uniforms