You choose the frames
No continuous sampling here. You pose an actor, press the shutter, and move on — the sequence is exactly the poses you decided were worth keeping, like a physical stop-motion rig.
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 SVG snapshot — 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. It persists the frame count, but no clone or DOM reference.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.
The afterRender hook centralizes frame collection after capture, using the SVG data URL in ctx.dataURL. An application could also collect result.url after awaiting snapdom(); this demo uses a plugin callback to keep that bookkeeping in one place.
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] })
Limits and things to watch
The collector has a stateful afterRender hook, so these poses run fresh captures in v3. Resource caches can still be reused. Each SVG data URL stays in memory, so filmstrip size grows with the number and content of the frames.
Build your own plugin
Every hook this demo uses — afterRender, plus the rest of the beforeSnap → afterSnap pipeline — is documented in the plugin spec.