A custom defineExports plugin (shatterExport) adds a genuinely new export type — result.toShatter() — that isn't a thin wrapper around toPng/toCanvas. It reuses the core canvas exporter internally via the "silent" ctx.exports.canvas() facade (no re-triggered beforeExport/afterExport hooks), then slices that canvas into a grid of small canvases for the physics simulation to grab as WebGL textures. The other four effects — ripple, glitch, dissolve, metal — don't need the plugin at all: they just take the whole card as one texture via plain snapdom.toCanvas().
What you're looking at
The card's heading and paragraph are contenteditable — genuinely editable HTML, not a canned image behind a fake cursor. Whatever you type is what ends up baked into the WebGL texture, because SnapDOM captures the card exactly as it currently renders: your text, your font metrics, the gradient, the shadow. Edit the copy, then apply an effect, and you're watching your own words get shattered, rippled, or dissolved.
Try it
Edit the card, pick an effect, hit Shatter (the button relabels itself per effect). Ripple, Glitch, and Dissolve settle or complete on their own — well, Dissolve needs a manual Reset since it ends empty. Metal stays interactive, tracking your pointer, until you exit it.
Try me!
This text is genuinely editable HTML. SnapDOM captures it exactly as it renders — font, gradient, shadow — and turns it into a WebGL texture.
Edit the card's text before applying an effect — you'll see your own text, not a canned image.
How it works
Ripple, Glitch, Dissolve, Metal: one texture, one shader
snapdom.toCanvas(card, { scale: 2 })captures the card once as a single canvas. That canvas becomes a WebGL texture viatexImage2D, and a per-effect fragment shader distorts or reveals it over a fixed duration — or, for Metal, indefinitely while tracking pointer position.The card hides while the effect plays
The card's CSS
visibilityis set tohiddenright after the texture is captured, and restored once the effect finishes — except Metal, which stays hidden and interactive until you explicitly exit it.Shatter needs more than one texture
The
shatterExportplugin'sdefineExports(ctx)returns ashatterexport function. Callingresult.toShatter({ cols, rows })first gets a canvas viactx.exports.canvas(opts)— the same core canvas exporter, called directly, bypassing the plugin pipeline's own hook re-runs.That canvas is sliced into a grid
The canvas is cut into
cols × rowssmall canvases withdrawImage, each one becoming its own WebGL texture positioned at its origin cell.Physics takes over
Each piece gets a random launch velocity and rotation on spawn; gravity is applied every frame, and pieces fade out until none are left.
Why this needed a plugin
SnapDOM's built-in exports — toPng, toCanvas, toSvg, toBlob — all return one complete image. There's no option to get back "the same capture, pre-sliced into N pieces," because slicing into tiles is a shatter-effect concern, not a general capture concern.
defineExports is exactly the seam SnapDOM exposes for adding an export shape that doesn't exist in core. And because the new shatter export still needs ordinary canvas pixels underneath it, it calls back into the "silent" ctx.exports.{img,svg,canvas,blob,png,jpeg,webp} facade, which reuses the core exporter functions directly without re-running the plugin's own beforeExport/afterExport hooks — avoiding infinite recursion and duplicate hook firing.
The other four effects deliberately don't use a plugin at all, and that's the point: most creative uses of a SnapDOM capture don't need one. defineExports is for the narrower case of inventing a new output shape, not a substitute for reaching for toCanvas() first.
Copy-paste example
The plugin factory — cols/rows control how fine the grid is:
function shatterExport (options = {}) {
const { cols: defaultCols = 14, rows: defaultRows = 9 } = options
return {
name: 'shatter',
defineExports (ctx) {
return {
shatter: async (_ctx2, opts = {}) => {
const cols = opts.cols || defaultCols
const rows = opts.rows || defaultRows
// Reuse the core canvas exporter directly — this does NOT
// re-run beforeExport/afterExport, so it's safe to call from
// inside another export.
const canvas = await ctx.exports.canvas(opts)
const pw = canvas.width / cols
const ph = canvas.height / rows
const pieces = []
for (let ry = 0; ry < rows; ry++) {
for (let rx = 0; rx < cols; rx++) {
const piece = document.createElement('canvas')
piece.width = pw
piece.height = ph
piece.getContext('2d').drawImage(canvas, rx * pw, ry * ph, pw, ph, 0, 0, pw, ph)
pieces.push({ u: rx / cols, v: ry / rows, du: 1 / cols, dv: 1 / rows, canvas: piece })
}
}
return { pieces }
}
}
}
}
}
Usage — the plugin is local to this one capture, and toShatter() shows up as a method on the result alongside the built-in toPng/toCanvas:
const result = await snapdom(card, { plugins: [shatterExport({ cols: 16, rows: 11 })] })
const { pieces } = await result.toShatter()
// pieces[i] = { u, v, du, dv, canvas } — grid position (0..1) plus a
// small
The other four effects skip the plugin entirely — one capture, one texture:
const canvas = await snapdom.toCanvas(card, { scale: 2 })
const tex = makeTexture(canvas) // gl.texImage2D(..., canvas)
Limits and things to watch
ctx.exports.canvas() is an internal facade — not yet part of the public PLUGIN_SPEC.md hook table, but a real, stable pattern in the codebase, meant specifically for plugins that need to build on a core export without re-triggering the whole export pipeline. Slicing one large canvas into many small ones has a real per-piece drawImage cost: this demo's 16×11 grid is already 176 pieces, and pushing much finer than that will start to show on lower-end GPUs.
Build your own plugin
defineExports — plus afterClone, afterRender, and the rest of the hook chain this site's other labs use — is documented in the plugin spec.