Export
A scene (or any canvas) to PNG, WebM and animated GIF.
Open the live demo
npx susegad add export
A scene (or any canvas) to PNG, WebM and animated GIF, for social posts and
for the sample video packages/player's demo and checks use — the library
dogfoods itself; nothing here fetches anything from the web.
import { canvasToPng, recordSceneToWebm, recordSceneToGif } from './index.js';
import kolam from '../scenes/kolam/index.js'; // the scene's own default export, from defineScene()
const png = await canvasToPng(myCanvas);
const webm = await recordSceneToWebm(kolam, { register: 'warm', seed: 7, durationSec: 3, fps: 24 });
const gif = await recordSceneToGif(kolam, { register: 'warm', seed: 7, durationSec: 3, fps: 10 });
PNG (png.js)
canvasToPng(canvas) wraps canvas.toBlob. svgToPng(svg, { width, height, scale, background })
rasterises an inline <svg> (serialised, loaded as an Image, drawn to a
canvas) — for the SVG scenes and skins that have no canvas of their own.
blobToDataUrl(blob) inlines a PNG for a Folio document or a poster.
WebM (webm.js)
recordCanvasToWebm(canvas, { fps, durationSec, draw }) opens
canvas.captureStream(0) — a stream that takes no automatic frames —
and calls draw(t, i, frames) once per frame, requesting exactly that frame
from the track afterwards. The recording is therefore exactly the frames
asked for, at exactly the times asked for, never whatever the wall clock
managed while MediaRecorder was running: the same seed and frame count
give the same video, run to run, however loaded the machine is.
recordSceneToWebm(def, { seed, register, params, fps, durationSec }) drives
a scene definition's own model() and createRenderer() directly at fixed,
evenly spaced times (never the scene's wall-clock loop), so a scene export
is reproducible the same way.
Known limitation: the WebM this writes is not scrubbable in Chromium.
MediaRecorder does not write a Cues (seek index) element, so Chromium
reports the file's video.seekable as [0, 0] however much is buffered —
confirmed directly against a sample clip (packages/player/fixtures,
readyState === 4, fully buffered, seekable still [0, 0]). The clip
plays straight through correctly and its duration reads correctly once
seeked once (the standard workaround), but a person cannot drag a scrubber
to an arbitrary point without an external remux (for example
ffmpeg -c copy to add Cues). See packages/player/fixtures/README.md for
how this shaped that package's own checks. Worth a follow-up if an exported
clip ever needs to be scrubbed outside this library.
GIF (gif.js, gif.core.js)
gif.core.js is a small, dependency-free GIF89a encoder: median-cut palette
quantisation, nearest-colour indexing, and an LZW core matching GIF's own
variable-code-size variant, all pure and tested in Node against a
from-scratch reference decoder (not a claim that any particular GIF viewer
accepts the output, only that the bytes follow the format's own rules).
gif.js is the browser edge: paint a frame, read its pixels with
getImageData, repeat, encode. recordSceneToGif mirrors
recordSceneToWebm's fixed-time driving of a scene.
Budget
18 KB of behaviour source for the whole package; see registry.json for the
breakdown. gif.core.js alone is the biggest part (7.9 KB) and is the one
piece with real algorithmic weight (median cut, LZW), which is why it is
separated out and tested on its own, the way a component's *.core.js is
(decision 0010).