Turn a paginated HTML deck into an element-level motion video. The HTML stays the only visual source — no template, no front-end framework.
Most "HTML → video" tools either flip through whole pages like a slideshow, or lock you into a template system whose look isn't your HTML. scenecast animates your poster, element by element — the HTML you wrote is the only visual source, so the video looks exactly like your page, just moving.
- Your HTML is the source of truth — palette, layout, fonts are all inherited.
- Declarative — describe motion with
data-animattributes; write zero JS. - Deterministic — seek-based rendering, frame-accurate and reproducible.
- No front-end framework — no GSAP, no React; just your HTML and a tiny engine.
scenecast opens your HTML in headless Chromium (Playwright), injects a dependency-free, seek-based animation engine that reads your data-anim attributes, steps a virtual clock frame by frame while screenshotting each scene, then encodes the frames to MP4, WebM, or GIF with ffmpeg (chosen by the output file extension).
git clone https://github.com/AliceLJY/scenecast.git && cd scenecast
npm install playwright # the only runtime dependency (peer)
npx playwright install chromium # one-time browser downloadRun it locally — clone and run. The npm name
scenecastbelongs to an unrelated package, so don'tnpm i scenecast. Want a shortscenecastcommand? Runnpm linkonce inside the repo (otherwise usenode src/cli.mjs).
scenecast also needs ffmpeg on your PATH:
- macOS:
brew install ffmpeg - Debian/Ubuntu:
sudo apt install ffmpeg - Windows: https://ffmpeg.org/download.html
Playwright is a peer dependency, so you pick the version. By default scenecast tries Playwright's bundled Chromium and falls back to system Chrome when the bundled browser is not installed; pass --channel chrome to require system Chrome explicitly.
Mark each page with data-scene, and add data-anim to the elements you want to move:
<section data-scene data-scene-duration="4">
<h1 data-anim="fade-left" data-anim-delay="0.1">Hello</h1>
<span data-anim="count-up" data-anim-delay="0.6">128</span>
</section>
<section data-scene>
<p data-anim="fade-up">The HTML you wrote is the only visual source.</p>
</section>Render it (from the cloned repo, or scenecast render … after npm link):
node src/cli.mjs render deck.html -o out.mp4Or from Node:
import { renderHtmlToVideo } from "./src/index.mjs"; // path within the cloned repo
await renderHtmlToVideo({ input: "deck.html", output: "out.mp4" });| attribute | meaning |
|---|---|
data-scene |
marks an element as one scene (a page / shot) |
data-scene-duration="5" |
seconds this scene lasts (overrides --scene-duration) |
Add data-anim to any element inside a scene:
data-anim |
effect |
|---|---|
fade |
fade in |
fade-up / fade-down |
fade + slide vertically |
fade-left / fade-right |
fade + slide horizontally |
zoom-in |
fade + scale up |
count-up |
count a number from 0 up to the element's final value |
draw |
draw on an SVG line / path (stroke draw-on) |
highlight |
tween text color toward data-anim-color |
Modifiers (all optional):
| attribute | default | meaning |
|---|---|---|
data-anim-delay |
0 |
start time within the scene (seconds) |
data-anim-duration |
0.6 |
animation length (seconds) |
data-anim-ease |
outCubic |
linear / outQuad / outCubic / outQuint / outBack |
data-anim-distance |
preset | slide distance in px for fade-* |
data-anim-rotate |
— | extra rotation in deg, "from to" e.g. "9 -1.6" (works on any preset) |
data-anim-scale |
— | scale endpoints, "from" (to 1) or "from to" |
data-anim-color |
— | target color for highlight (hex or rgb()) |
data-anim-base |
— | a transform to keep, e.g. translateX(-50%) |
Put data-anim-stagger on a container; its direct children enter in sequence, each offset by the step (seconds). A child and any animated elements inside it move together as one unit:
<div class="cards" data-anim-stagger="0.15">
<div class="card" data-anim="fade-up">…</div>
<div class="card" data-anim="fade-up">…</div>
<div class="card" data-anim="fade-up">…</div>
</div>scenecast render <input.html> -o <output.mp4|.webm|.gif> [options]
| option | default | |
|---|---|---|
-o, --output |
— | output file; codec by extension: .mp4 / .webm / .gif (required) |
--scene-selector |
[data-scene] |
which elements are scenes |
--scene-duration |
5 |
seconds per scene |
--fps |
30 |
frames per second |
--width / --height |
1920 / 1080 |
output size |
--deck-width / --deck-height |
1200 / 675 |
your HTML's per-scene design size |
--scale |
2 |
capture device scale factor (sharper text) |
--channel |
bundled Chromium | e.g. chrome for system Chrome |
--voiceover |
— | audio file; also writes *-voiceover.mp4 (mp4 output only) |
--keep-frames |
off | keep the intermediate PNG frames |
A runnable example lives in demo/demo.html; npm run demo renders it.
- Only render HTML you trust. scenecast opens your HTML in a real browser and executes its scripts with network access. Don't feed it untrusted pages.
- Your existing CSS animations / transitions are not captured. Frames are seek-driven and grabbed with
animations: "disabled"; motion comes fromdata-anim, not from CSS keyframes. transformanimations on inline elements (fade-*,zoom-in) promote them toinline-block, which can change wrapping and baseline. Prefer animating block / inline-block elements, or usehighlight(color only) for inline text.- Nested
data-anim-staggeris not supported — the outermost container wins. data-anim-baseis manual. If an element already has a CSS transform you want to keep, repeat it there.- count-up reads the element's final text. It handles integers, decimals, and a leading unit/sign, but not arbitrary formats; thousands separators are dropped while counting.
- Node ≥ 18
- Playwright (peer dependency) + a browser (
npx playwright install chromium) - ffmpeg on
PATH
MIT © AliceLJY
