DOM, live on the GPU
Type in the card on the left. On every keystroke, SnapDOM re-captures it and a plugin uploads the result straight into a WebGL texture on the right — not a screenshot you take once, a texture that keeps up with you.
A defineExports plugin adds a webgl export that feeds a captured SVG straight into an existing WebGL texture, and a small dirty/working coalescing loop makes sure fast typing never queues up stale captures — a trailing capture picks up edits made while the current capture runs.
What you're looking at
Type in the card on the left and the panel on the right updates within a frame or two — that's a real SnapDOM capture of the live DOM element, decoded into an <img>, and uploaded as a WebGL TEXTURE_2D. The color-theme buttons change a CSS custom property read by the captured element itself, so switching themes is just another DOM mutation flowing through the same capture path. The shader dropdown is different — it never touches SnapDOM at all, it only changes how the fragment shader samples the texture that's already sitting on the GPU.
Try it
Edit the text, switch the accent color, or swap shaders while you type. The right pane is a live WebGL canvas with its own independent render loop; SnapDOM only ever refreshes what texture that loop is drawing.
Type here
Every edit becomes a WebGL texture through SnapDOM.
How it works
Typing fires an
inputlistenerEvery keystroke in the
contenteditableelement dispatches a nativeinputevent. The only listener attached to it callsupdateMirror().updateMirror()sets adirtyflagIf a capture is already running, it just marks the work as
dirtyand returns — it never queues a second capture. If nothing is running, it enters awhile (dirty)loop and captures the editable element with{ plugins: [mirrorPlugin] }.The plugin's
defineExports(ctx)adds awebglexportSnapDOM wires that up automatically as
capture.toWebgl(). Calling it hands the export function the capture's own context — includingctx.export.url, the rendered SVG data URL for this specific capture.The export loads that SVG into the existing texture
The export function creates an
Image, points it atctx.export.url, and once it decodes, hands it to theWebGLMirrorinstance'supdate()method, which uploads it as a newTEXTURE_2Dand returns the canvas.An independent render loop keeps drawing it
The mirror's own
requestAnimationFrameloop — started once, completely outside of SnapDOM — redraws that texture every frame, applying whichever shader is currently selected. SnapDOM never drives the render loop; it only refreshes what's on the texture.The
dirtyre-check collapses bursts of keystrokesBecause
dirtyis checked again right after each capture finishes, typing a whole sentence quickly produces exactly one trailing capture instead of one per keystroke — the loop always converges on the latest DOM state.
Why this needed a plugin
SnapDOM's core exports — toPng, toCanvas, and the rest — hand you a finished image or canvas and stop there. They have no concept of "also push this into an existing WebGL texture and keep it around for a render loop," because that's not a rendering concern, it's a destination the caller invented.
defineExports is the seam SnapDOM exposes for inventing that new output shape: a plugin can return named async functions that receive the capture's context and get wired up as capture.to<Name>() automatically. The beforeSnap/afterRender hooks are used here too, but only to time each round trip for the on-screen readout — they're informational and have nothing to do with the texture update itself.
Copy-paste example
The plugin factory — it doesn't know or care what a "capture" is beyond ctx.export.url, so it drops into any project that already has a WebGL renderer with an update(url) method:
function webglMirror (renderer) {
let started = 0
let elapsed = 0
return {
name: 'webgl-live-mirror',
beforeSnap () { started = performance.now() },
afterRender () { elapsed = performance.now() - started },
defineExports () {
return {
webgl: async ctx => {
const canvas = await renderer.update(ctx.export.url)
statsEl.textContent = `${elapsed.toFixed(0)} ms · GPU updated`
return canvas
}
}
}
}
}
And the usage — a dirty/working loop guarantees a burst of edits collapses into one trailing capture instead of one per keystroke:
const mirrorPlugin = webglMirror(renderer)
let dirty = false
let working = false
async function updateMirror () {
dirty = true
if (working) return
working = true
while (dirty) {
dirty = false
const capture = await snapdom(editable, { plugins: [mirrorPlugin] })
await capture.toWebgl()
}
working = false
}
editable.addEventListener('input', updateMirror)
Limits and things to watch
A keystroke can trigger a fresh capture and texture upload. Keep the captured element small or debounce the input handler for heavier content. SnapDOM v3.x.x handles eligible reuse automatically; the timing hooks in this demo are stateful and force fresh capture work. Resource caches can still be reused.
Build your own plugin
Every hook this demo uses — beforeSnap, afterRender, plus defineExports for adding new export formats — is documented in the plugin spec.