Labs Experiment

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.

TL;DR

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.

Pose playback
Capture at least two poses.

How it works

  1. The plugin factory closes over a counter

    frameCollector(onFrame) returns a plugin holding a private index variable — nothing else. No clone, no DOM reference, no persisted state.

  2. Moving a slider never captures anything

    Dragging Horizontal, Vertical, or Rotation just updates inline styles and a CSS custom property on the live #actor element. SnapDOM is not involved yet.

  3. 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.

  4. afterRender fires with the finished context

    Once SnapDOM renders the SVG, the plugin's afterRender(ctx) hook runs, increments its counter, and calls back with { index, url: ctx.dataURL }.

  5. 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.

  6. Play iterates the array on a timer

    Pressing Play walks capturedFrames for a few loops, swapping <img id="playback">.src on 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 })
slider input live style change (no capture) Capture pose snapdom() afterRender (ctx.dataURL) capturedFrames.push()

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.

Explore pluginsRead PLUGIN_SPEC.md