SnapDOMGitHub8K
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 page stores that vector URL and derives a PNG preview from the same capture so every browser scales the filmstrip consistently.

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, with 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 stores the URL and a preview

    The callback provides the vector URL. The page then calls result.toPng() on that same capture and stores both URLs before appending the PNG to the filmstrip. There is no second DOM capture.

  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 = []
let capturedFrame

const collector = frameCollector(frame => {
  capturedFrame = frame
})

// runs once per "Capture pose" click, against whatever
// pose the sliders are showing at that moment
const result = await snapdom(poseScene, { plugins: [collector], cache: 'full', fast: true })
const preview = await result.toPng()
capturedFrames.push({ url: capturedFrame.url, preview: preview.src })
slider input live style change (no capture) Capture pose afterRender (ctx.dataURL) PNG preview

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