# π¬ Deterministic HTML/CSS/JS Animation Rendering
### *Frame-by-Frame Puppeteer β FFmpeg Pipeline with Transparency*
This document describes a workflow for rendering **web-based animations** (HTML/CSS/JS, Canvas, WebGL) into **smooth, high-quality videos with alpha transparency**.
The goal is to replicate βMotion-styleβ reusable templates (title cards, lower thirds, bumpers) using web technologies.
## π₯ Why Frame-by-Frame?
Running the animation at real-time inside Chrome and screen-recording leads to:
* stutter, dropped frames
* inconsistent timing
* low-quality capture
* no alpha channel
**Frame-by-frame rendering** creates:
* perfectly stable FPS (30, 60, 120, etc.)
* deterministic animation timing
* super-smooth easing curves
* slower-than-real-time rendering for complex effects
* transparent PNG β alpha video output
This is required for templated, repeatable graphics.
---
# π§± Overall Architecture
```
ββββββββββββββββββββ
β HTML/CSS/JS page β β Your animation template
ββββββββββ¬ββββββββββ
β Expose a controllable renderFrame(t)
βΌ
ββββββββββββββββββββ Puppeteer
β Virtual Browser β β headless Chrome
βββββββββ¬βββββββββββ
β Step time t = frameIndex / fps
β Screenshot each frame as PNG (with alpha)
βΌ
ββββββββββββββββββββ FFmpeg
β frame_0000.png β β encode to ProRes 4444 or WebM VP9 alpha
β frame_0001.png β
β ... β
ββββββββββββββββββββ
```
---
# π§© Step 1 β Build a Time-Controlled Animation Template
Create a web page that renders animations based on a **virtual time variable** instead of `performance.now()` or normal `requestAnimationFrame`.
### `renderFrame(t)`
Define a global function in JavaScript:
```js
window.renderFrame = function(t) {
// t = time in seconds
// Update CSS variables
document.documentElement.style.setProperty("--t", t);
// Or animate DOM directly
const el = document.querySelector("#title");
el.style.transform = `translateX(${Math.min(0, -200 + t * 400)}px)`;
// If using Canvas/WebGL, re-render scene at time t
// shader.uniforms.time = t;
// renderer.render(scene, camera);
};
```
### Optional: Live mode
So it still animates normally in a browser:
```js
if (!window.IS_PUPPETEER) {
const start = performance.now();
function tick() {
const t = (performance.now() - start) / 1000;
window.renderFrame(t);
requestAnimationFrame(tick);
}
tick();
}
```
---
# π§© Step 2 β Launch Puppeteer and Step Through Frames
Use a Node script to render frames deterministically:
### `render.js`
```js
import puppeteer from "puppeteer";
import fs from "fs";
import path from "path";
const FPS = 60;
const DURATION = 5; // seconds
const TOTAL_FRAMES = FPS * DURATION;
const WIDTH = 1920;
const HEIGHT = 1080;
async function main() {
const browser = await puppeteer.launch({
headless: "new",
defaultViewport: { width: WIDTH, height: HEIGHT },
args: ["--disable-gpu", "--enable-blink-features=CSSColorSchemeUARendering"]
});
const page = await browser.newPage();
await page.goto("http://localhost:3000/template?name=Alice&subtitle=Fellow");
await page.addScriptTag({ content: `window.IS_PUPPETEER = true;` });
// Ensure transparent background
await page.evaluate(() => {
document.body.style.background = "transparent";
});
for (let i = 0; i < TOTAL_FRAMES; i++) {
const t = i / FPS;
// Advance virtual time
await page.evaluate((t) => window.renderFrame(t), t);
// Capture frame
const buffer = await page.screenshot({
omitBackground: true, // <-- transparency enabled
type: "png"
});
fs.writeFileSync(
path.join("frames", `frame_${String(i).padStart(4, "0")}.png`),
buffer
);
console.log(`Rendered frame ${i + 1}/${TOTAL_FRAMES}`);
}
await browser.close();
}
main();
```
---
# π§© Step 3 β Convert PNG Frames to a Video with Alpha
Use FFmpeg to encode transparency.
## Option A: ProRes 4444 (studio/NLE-friendly)
Great for DaVinci Resolve, FCP, HyperDeck, ATEM:
```bash
ffmpeg -framerate 60 -i frames/frame_%04d.png \
-c:v prores_ks -profile:v 4 -pix_fmt yuva444p10le \
titlecard.mov
```
## Option B: WebM VP9 with alpha
Great for web compositing:
```bash
ffmpeg -framerate 60 -i frames/frame_%04d.png \
-c:v libvpx-vp9 -pix_fmt yuva420p \
titlecard.webm
```
---
# π§© Step 4 β Parameterize Templates (Names, Roles, etc.)
Your template page can use query parameters:
```
/template?name=Alice%20Zhang&subtitle=Expos%20Studio%20Fellow
```
Or you can inject data directly:
```js
await page.evaluate(({ name, subtitle }) => {
document.querySelector("#name").textContent = name;
document.querySelector("#subtitle").textContent = subtitle;
}, studentData);
```
Then loop over your class roster:
```js
for (const student of students) {
await renderStudentLowerThird(student);
}
```
This is the web-based equivalent of **Final Cut/Motion templates**.
---
# π§© Step 5 β Folder Structure (Recommended)
```
templates/
lower-third/
index.html
style.css
script.js // contains renderFrame(t)
scripts/
render.js // Puppeteer frame renderer
batch.js // optional, loop over roster
frames/
...output PNGs...
output/
lower-third_Alice.mov
lower-third_Benito.mov
```
---
# π§© Step 6 β Animation Tips for Web β Video Rendering
### Use CSS Variables for Time
```css
#title {
transform: translateX(calc( var(--t) * 200px ));
opacity: calc( min(1, var(--t) * 2) );
}
```
### Use deterministic easing
Provide your easing function in JS:
```js
function easeOut(t) {
return 1 - Math.pow(1 - t, 3);
}
```
### Avoid built-in `transition` or `animation` timing
They're real-time and nondeterministic.
Drive everything with **virtual time** instead.
### Canvas/WebGL also works
Just re-render your scene at each `t`.
---
# π§© Optional: Generate 30 Title Cards Automatically
If you have a CSV:
```csv
name,subtitle
Alice Zhang,Expos Fellow
Benito Cruz,Vibe Coding
Chloe Kim,Harvard '27
```
Use:
```bash
node batch.js students.csv --template lower-third
```
The script:
* loads each student
* opens the template with params
* renders frames for 5 seconds
* encodes to transparent video
---
# π This Pipeline Gives You:
### β Perfectly smooth Motion-quality graphics
### β Deterministic timing
### β Transparency (alpha)
### β Reusable templates with βslotsβ
### β HTML/CSS/JS as your graphics engine
### β FFmpeg as your compositor
### β Batch export of dozens of variants
### β Easy integration with Resolve/ATEM/HyperDeck or web apps
Itβs the cleanest way to produce **next-generation academic/teaching graphics** without touching native GPU frameworks.
---
If you want, I can also produce:
* A working **Next.js page template** with a sample animated lower third
* A full **CLI tool** for your studio that reads Canvas/Airtable roster β exports videos
* A **Makefile** or `npm run render` pipeline
* A **Dockerized** version so LLUFs can use it anywhere
Just say the word.