A tiny plugin's afterRender hook fires once per capture and hands back ctx.dataURL. Nothing accumulates onto a saved clone — unlike the paint-canvas and target-range labs, each capture here is fully independent. The plugin's only job is to collect the dataURLs you explicitly ask for into an ordered array; the player decides the playback rhythm afterward.
What you're looking at
The three sliders position and rotate an actor sprite on a small stage. Nothing you do with them triggers a capture — they just change CSS on a live element. Pressing Capture pose runs one SnapDOM capture of that stage and appends the result to a filmstrip. Pressing Play flips through the filmstrip at a fixed interval, like a flipbook.
Try it
Drag the sliders to pose the actor, capture a few poses from different positions, then hit play. Every thumbnail in the filmstrip below is a separate, independent PNG capture — there's no shared state between them.
How it works
The plugin factory closes over a counter
frameCollector(onFrame)returns a plugin holding a privateindexvariable — nothing else. No clone, no DOM reference, no persisted state.Moving a slider never captures anything
Dragging Horizontal, Vertical, or Rotation just updates inline styles and a CSS custom property on the live
#actorelement. SnapDOM is not involved yet.Capture pose runs one real capture
The button calls
snapdom(poseScene, { plugins: [collector] })against the current, live state of the stage — whatever pose the sliders are showing right now.afterRenderfires with the finished contextOnce SnapDOM renders the SVG, the plugin's
afterRender(ctx)hook runs, increments its counter, and calls back with{ index, url: ctx.dataURL }.The page pushes the URL into an array
capturedFrames.push(frame.url), plus a thumbnail appended to the filmstrip. The plugin doesn't know or care that a filmstrip exists — it just handed the URL to a callback.Play iterates the array on a timer
Pressing Play walks
capturedFramesfor a few loops, swapping<img id="playback">.srcon an interval. The plugin is done by this point — playback is pure application code.
Why this needed a plugin
Contrast this with the accumulation pattern in the paint-canvas and target-range labs: there, a plugin mutates ctx.clone and persists it across calls so the next capture builds on the last one. Here the plugin does neither. It never touches ctx.clone, and it doesn't need to remember anything about the DOM from one capture to the next — each pose is a fresh, self-contained render.
What it does need is a way to get data out of a capture that a plain snapdom() call, used only for its side effect of producing an image, wouldn't otherwise return to the caller. The afterRender hook is documented as firing "After SVG is rendered" to "Post-process rendered output," and by that point ctx.dataURL is already available on the context. This lab uses that hook purely as a tap on the pipeline — proof that plugin hooks aren't only for mutating the DOM before a render; afterRender and afterExport are just as much a seam for pulling values back out once a render has finished.
Copy-paste example
The plugin factory — it has no idea what a "pose" or a "filmstrip" is, it just collects dataURLs:
function frameCollector (onFrame) {
let index = 0
return {
name: 'frame-collector',
afterRender (ctx) {
index += 1
onFrame({ index, url: ctx.dataURL })
},
reset () { index = 0 }
}
}
And the usage — capture a pose, read the frame back through the callback:
const capturedFrames = []
const collector = frameCollector(frame => {
capturedFrames.push(frame.url)
})
// runs once per "Capture pose" click, against whatever
// pose the sliders are showing at that moment
await snapdom(poseScene, { plugins: [collector], cache: 'full', fast: true })
Limits and things to watch
Every pose is a full, independent SnapDOM capture — nothing is diffed or reused between frames — so a large filmstrip means proportionally more capture time, not a fixed cost. And because frames are plain dataURLs held in memory, footprint scales linearly with frame count and resolution. Fine for a dozen poses on a small stage; not a strategy for hundreds of frames or a full-resolution scene.
Build your own plugin
Every hook this demo uses — afterRender, plus the rest of the beforeSnap → afterSnap pipeline — is documented in the plugin spec.