Type here
This text is real DOM, but you're seeing it as a WebGL texture kept in sync by SnapDOM.
Click into the box below and type. Every glyph you "see" is a WebGL texture rendered from the last SnapDOM capture — the real, editable DOM is invisible on purpose, and updates cross-fade in so nothing ever pops.
Two DOM elements do the work: a transparent contenteditable proxy that exists only to give the browser a real caret and real keystroke handling, and a hidden source element that mirrors the typed text and is what SnapDOM actually captures. A defineExports plugin feeds each capture into a WebGL renderer that double-buffers two textures and cross-fades between them, so updates never visibly pop — you always see the previous frame smoothly blend into the new one.
Clicking into the box and typing feels like a normal text field — you get a blinking caret, text selection, the works. None of that text is being rendered by the browser's text engine, though. Every glyph on screen is a pixel in a WebGL texture, produced by capturing a completely different, hidden DOM element with SnapDOM and mixing the result into the canvas underneath your cursor. Pick a shader from the dropdown to see the same texture pipeline reused for very different visual effects — the capture and the compositing don't change, only what the fragment shader does with the finished frame.
Type, paste, or hit Enter. The caret and selection are real; the letters are not.
This text is real DOM, but you're seeing it as a WebGL texture kept in sync by SnapDOM.
The .input-layer sits visually on top of the WebGL canvas with fully transparent text and background. It exists only to give the browser something to place a real caret on and to receive real keyboard and paste events — it never renders anything visible itself.
On every input event — plus paste, plus Enter (a beforeinput override inserts a literal newline instead of letting contenteditable create a new <div>) — the proxy's text is copied into the hidden #dom-source element and refresh() runs.
refresh() coalesces burstsSame dirty/working loop as the live-mirror demo: if a capture is already in flight when new input arrives, it's flagged dirty and picked up by the loop that's already running, instead of stacking up parallel captures. It captures #dom-source with the seamlessWebgl plugin attached.
The plugin's defineExports returns a webgl export. Calling it hands the capture's URL to renderer.update(ctx.export.url) instead of returning an image to the page.
SeamlessRenderer.update() loads the new capture into whichever of its two textures — current or next — isn't currently on screen, then starts a roughly 110ms blend toward it.
The render loop's fragment shader mixes beforeImage and afterImage by a blend uniform that ramps 0→1 over that window. Whatever shader is selected in the dropdown runs on top of that already-blended result.
Same argument as the plain live-mirror demo: SnapDOM's core exports return a finished image and stop — they have no concept of "push this into one of two alternating GPU textures for a crossfade." defineExports is the seam SnapDOM exposes for exactly that: a plugin can register a named export — here, webgl — that does whatever it wants with the render instead of handing back a PNG.
What's different here versus the plain live-mirror demo is architectural, not plugin-API related. Splitting the caret-owning element from the capture-source element is what makes the transition look seamless: the visible surface — the canvas — is never waiting on a capture to know what to show. It's always mid-blend between two textures it already has, while the hidden source quietly gets captured again in the background.
The plugin factory — it just forwards a capture's URL to whatever renderer you hand it:
function seamlessWebgl (renderer) {
let started = 0
let elapsed = 0
return {
name: 'seamless-webgl',
beforeSnap () { started = performance.now() },
afterRender () { elapsed = performance.now() - started },
defineExports () {
return {
webgl: async ctx => {
const canvas = await renderer.update(ctx.export.url)
perfLabel.textContent = `${elapsed.toFixed(0)} ms · texture swapped`
return canvas
}
}
}
}
}
And the usage — note this calls the plugin's export through the generic capture.to('webgl'), not a generated capture.toWebgl() helper. Both work; .to(name) is what lets a plugin-defined export be invoked without SnapDOM knowing its name ahead of time:
const plugin = seamlessWebgl(renderer)
let dirty = false
let working = false
async function refresh () {
dirty = true
if (working) return
working = true
while (dirty) {
dirty = false
const capture = await snapdom(source, { plugins: [plugin], cache: 'full', fast: true })
await capture.to('webgl')
}
working = false
}
The transparent-proxy trick only works cleanly for plain text editing. Anything that needs real DOM selection or formatting UI — rich text, IME composition edge cases — needs more care than this demo attempts; the proxy here deliberately strips formatting down to a literal newline on Enter and plain text on paste. Double-buffering also costs one extra WebGL texture's worth of GPU memory compared to the single-texture live-mirror demo — a deliberate trade for the flicker-free feel, and one that only makes sense when the crossfade is actually the point.
Every hook this demo uses — beforeSnap, afterRender, and defineExports for adding new export formats — is documented in the plugin spec.