Aether Renderer

CLI · Rust crate · v0.2.0

aether-renderer-capture

Turn any HTML/CSS/JS animation into a deterministic, transparent PNG sequence or stream. Every frame is stepped, never sampled from a running clock: same page, same bytes, every time.

crates.io versiondownloadsdocs.rs

Install

cargo install aether-renderer-capture

# It drives chrome-headless-shell (BeginFrame doesn't exist in normal Chrome's --headless=new)
npx @puppeteer/browsers install chrome-headless-shell@stable
export AETHER_CHROME=/path/printed/above/chrome-headless-shell

Or download a binary (you still need chrome-headless-shell):

FFmpeg is only needed to turn the frames into video, directly or throughaether-renderer-core. Pages withhtml, body { background: transparent } give real alpha.

Usage

# a folder (or a ZIP) of frame_00000.png ... for aether-renderer-core
aether-renderer-capture -i anim.html -o frames --fps 30 -d 4 -s 1080x1080
aether-renderer-capture -i anim.html -o anim.zip --fps 30 -d 4 -s 1080x1080

# stream straight into aether-renderer-core: no files on disk
aether-renderer-capture -i anim.html --stdout --fps 30 -d 4 \
  | aether-renderer-core -i - -o anim.webm --fps 30 --frames 120

# ...or into plain ffmpeg
aether-renderer-capture -i anim.html --stdout --fps 30 -d 4 \
  | ffmpeg -f image2pipe -framerate 30 -c:v png -i - \
           -c:v libvpx-vp9 -pix_fmt yuva420p -b:v 0 -crf 30 anim.webm
FlagDefaultWhat it does
-i, --inputHTML file or URL
-o, --outframesOutput folder, or a .zip, of frame_00000.png …
--stdoutoffPNG files back to back on stdout
--fps30Frames per second
-d, --duration2.0Seconds
-s, --size512x512WIDTHxHEIGHT
--chrome / AETHER_CHROMEPath to chrome-headless-shell
--modeautobegin-frame (Linux), screenshot (macOS, Windows), or auto
--no-sandboxoffRun Chrome without its sandbox (Ubuntu 23.10+, trusted pages only)

How it works

⏸️ Virtual time

Chrome's clock starts paused. While the page loads it moves in 1 µs steps until fonts and network are ready; then it advances exactly one frame interval per frame.

🕰️ A frame-clock shim

Injected before any page code: performance.now(), rAF timestamps andDate return the exact frame time, and CSS animations, transitions and the Web Animations API are paused and seeked to it.

🖼️ Two ways to draw

begin-frame on Linux (Chrome draws only when asked and returns the PNG),screenshot on macOS and Windows. Both give the same bytes.

Design for it

Because the clock is exact, you can do the maths in your script and trust it: a loop that's a function of t % T ends exactly where it started. The animation onweb2vid.com is made like that, and rendered with capture.

// A seamless 6 s loop: everything is a function of t modulo the period.
const T = 6000;
function frame(t) {
  const phase = (t % T) / T;          // 0..1, exactly, on every frame
  draw(phase);
  requestAnimationFrame(frame);
}
requestAnimationFrame(frame);

As a library

[dependencies]
aether-renderer-capture = "0.2"
use aether_renderer_capture::{png_stream, CaptureOptions};

let opts = CaptureOptions::new("anim.html", "/path/to/chrome-headless-shell")
    .size(1080, 1080)
    .fps(30)
    .duration_secs(4.0);
let frames = opts.frame_count() as u32;

let (stream, capture) = png_stream(opts)?; // options are checked here
aether_renderer_core::render_stream(cfg, stream, Some(frames), |p| {
    println!("{:.0}%", p.percent);
})?;
capture.wait()?; // the capture's report, or why it failed

If the capture fails, the stream's next read returns the error. Dropping the stream stops the capture and closes Chrome. PngStream::on_frame reports progress as frames arrive. Full API on docs.rs.

Known limits

Source on GitHub · Changelog · Next: the desktop app →