The text you see isn't text
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 the previous frame can blend into the new one.
What you're looking at
Clicking into the box and typing feels like a normal text field — you get a blinking caret, text selection, the works. The visible text is displayed through WebGL after the browser renders the captured source. 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.
Try it
Type, paste, or hit Enter. The caret and selection are real; the letters are not.
Type here
This text is real DOM, but you're seeing it as a WebGL texture kept in sync by SnapDOM.
How it works
A transparent proxy owns the caret
The
.input-layersits 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 — its text is transparent while the caret and selection remain visible.Every edit is mirrored to a hidden element
On every
inputevent — plus paste, plus Enter (abeforeinputoverride inserts a literal newline instead of lettingcontenteditablecreate a new<div>) — the proxy's text is copied into the hidden#dom-sourceelement andrefresh()runs.refresh()coalesces burstsSame
dirty/workingloop 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-sourcewith theseamlessWebglplugin attached.The plugin exports into the renderer
The plugin's
defineExportsreturns awebglexport. Calling it hands the capture's URL torenderer.update(ctx.export.url)instead of returning an image to the page.The idle texture is loaded, not the visible one
SeamlessRenderer.update()loads the new capture into whichever of its two textures —currentornext— isn't currently on screen, then starts a roughly 110ms blend toward it.The shader mixes, then the effect applies
The render loop's fragment shader mixes
beforeImageandafterImageby ablenduniform that ramps 0 1 over that window. Whatever shader is selected in the dropdown runs on top of that already-blended result.
Why this needed a plugin
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.
Copy-paste example
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] })
await capture.to('webgl')
}
working = false
}
Limits and things to watch
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.
Build your own plugin
Every hook this demo uses — beforeSnap, afterRender, and defineExports for adding new export formats — is documented in the plugin spec.