generateAnimatedSvg()
Compose multiple captured frames into one animated SVG.
Stitches a list of captured frames into a single SVG with CSS
keyframes between them. The output loops infinitely and runs without
JavaScript — drop it into <img src="demo.svg">.
Signature
function generateAnimatedSvg(config: AnimationConfig): string
See AnimationConfig for the
full input shape. The minimum viable call is:
const svg = generateAnimatedSvg({
width: 800,
height: 400,
frames: [
{ svgContent: frame0Svg, duration: 2000 },
{ svgContent: frame1Svg, duration: 2000 },
{ svgContent: frame2Svg, duration: 2000 },
],
});
What it does
- Sums up frame durations and per-frame transition durations into a total animation length.
- If every transition is
crossfade(or unset) and there are no overlays, takes the merge fast path — de-duplicates elements across frames and emits per-element visibility timelines (smaller, no flicker). - Otherwise, emits each frame as an
opacity-animated<g>with the right transition keyframes (push-leftusestranslateX;scrollstays visible, fades only at the end). - Renders every overlay (typing reveals, tap ripples) as additional
<g>+@keyframesblocks on top of the relevant frame. - Wraps everything in a viewport-clipped SVG with a dark background fill.
Returns
An SVG document string starting with the XML declaration. Pipe it through
optimizeSvg() for production.
Example: three-frame crossfade
import { chromium } from "@playwright/test";
import {
captureElementTree, elementTreeToSvg, generateAnimatedSvg, optimizeSvg,
} from "domotion";
import { writeFileSync } from "node:fs";
const WIDTH = 600, HEIGHT = 300;
const stages = [
{ html: "<div style='padding:24px'>Step 1: install</div>" },
{ html: "<div style='padding:24px'>Step 2: configure</div>" },
{ html: "<div style='padding:24px'>Step 3: ship 🚀</div>" },
];
const browser = await chromium.launch();
const page = await browser.newPage();
const frames = [];
for (let i = 0; i < stages.length; i++) {
await page.setContent(stages[i].html);
const tree = await captureElementTree(page, "body", {
x: 0, y: 0, width: WIDTH, height: HEIGHT,
});
frames.push({
svgContent: elementTreeToSvg(tree, WIDTH, HEIGHT, `f${i}-`),
duration: 1500,
transition: { type: "crossfade", duration: 300 },
});
}
await browser.close();
const svg = optimizeSvg(generateAnimatedSvg({ width: WIDTH, height: HEIGHT, frames }));
writeFileSync("steps.svg", svg);
Pitfalls
- Forgetting unique
idPrefixes per frame. Frames re-use clip / gradient IDs by default; without distinct prefixes you'll get the wrong shapes after frame 0. - Overlays disable the merge fast path. If you want the smallest possible output and no flicker, capture overlay-style content (typed text, ripples) into the captured frames themselves rather than as overlays. Overlays are best for content that needs to span a transition or that wasn't in the captured DOM at all.
- Transitions outside
crossfadecan't merge. A singlepush-lefttransition forces every frame onto the slower per-frame-atomic path. Keep all-crossfade sequences when you can.