Expand description
§broadsheet
A 2D animation engine for algorithm & data-structure explainer videos, in Rust on macroquad. Newspaper-styled, deterministic, code-driven.
Every render looks like a page from the same broadsheet: off-white paper,
ink strokes, one spot color, serif headlines, mono data. The visual identity
is defined once in src/style.rs.
§Run the examples
cargo run --example features_demo # live preview window
cargo run --example lsm_treeLive transport controls (for lining narration up with beats):
| key | action |
|---|---|
Space | pause / play |
← → | step one frame |
, . | jump ±1 s |
1–9 | jump to section markers |
F / Ctrl+Cmd+F | toggle fullscreen (fit-to-screen, letterboxed) |
R | restart |
| drag bottom bar | scrub |
The HUD shows exact t and frame number; it is never present in recordings.
§Record a video
cargo run --example features_demo -- --record out/showcase --fps 60Renders at a fixed timestep (t = frame / fps, wall clock ignored → output
is deterministic), then pipes raw RGBA frames straight into ffmpeg when it is
installed:
out/showcase/out.mp4If ffmpeg is missing, or if you pass --png, it writes
out/showcase/frame_00000.png … and prints the exact stitch command.
Recording supersamples at --scale 1.5 by default, so the 1280×720 logical
canvas comes out as true 1920×1080 with fonts rasterized at full resolution
(pass --scale 2 for 1440p). Everything is drawn with 4× MSAA.
Tip: --fps 2 sparsely samples the whole movie in a few dozen frames —
a fast visual proof-read of a full video. --frames N caps the frame count.
Useful export flags: --still S writes one PNG at timestamp S,
--from S --to S records a range, --alpha writes transparent PNG frames,
--gif pipes a recording to out.gif, and --grain applies the newsprint
grain/vignette pass.
§Writing a movie
use broadsheet::prelude::*;
fn main() {
let mut m = Movie::new("Skip Lists", 1280, 720);
// 1. declare the cast (state at t = 0)
m.scene()
.circle("A", v(300., 400.), 40.).label("A")
.circle("B", v(900., 400.), 40.).label("B").hidden()
.arrow("e", v(340., 400.), v(340., 400.)).hidden()
.text("cap", v(640., 620.), "").size(22.).color(FADED).hidden();
// 2. script the beats (cursor advances with each play)
m.play(act().set_text("cap", "two nodes, one pointer"));
m.play(act().fade_in("B").dur(0.4));
m.play(par![ // simultaneous
act().fade_in("e").dur(0.15),
act().grow_to("e", v(860., 400.)).dur(0.5).ease(InOutCubic),
]);
m.play(seq![ // sequential
act().highlight("B", ACCENT),
act().pulse("B"),
wait(0.5), // narration beat
]);
m.section("Level 2"); // newspaper section card + jump marker
m.play(act().move_to("A", v(300., 250.)).dur(0.6).ease(OutBack));
broadsheet::run(m);
}Verbs: move_to move_by fade_in fade_out color_to highlight
(auto-reverts) scale_to pulse shake grow_to/retarget (line & arrow
endpoints — draws edges, rewires pointers) set_text (crossfade).
Tune any act with .dur(secs) and .ease(...).
Scene niceties: .label("A") puts a mono label riding on a shape
(addressable as "A.label"); .follow(id, offset) pins any entity to
another; .wrap(px) word-wraps long text (captions) into centred lines;
.hidden() starts invisible for a later fade_in; m.wait(s)
leaves silence; m.at(t, clip) places a clip at an absolute timestamp;
m.now() tells you the cursor time for narration notes; m.mark("name")
exports a beat marker to markers.json during recording; .sticky() keeps
an entity in screen coordinates during camera pan/zoom for HUD-style overlays.
Palette: INK, PAPER, ACCENT (newsprint red), BLUE, FADED,
PAPER_SHADE.
§Extending it
See ARCHITECTURE.md — module map, the statelessness invariant, and step-by-step recipes for adding a primitive or a verb (the two extension points most movies eventually need).
§Crate API
broadsheet is organized around a small, scriptable pipeline:
- Build a
movie::Moviewith a base scene. - Declare visual entities with
scene::SceneBuilder. - Add animation clips with
animate::act,seq!,par!, andstagger!. - Hand the movie to
runfor live preview or deterministic recording.
The easiest entry point is prelude, which re-exports the types and
helpers used by movie scripts.
§Minimal Example
use broadsheet::prelude::*;
fn main() {
let mut m = Movie::new("Hello", 1280, 720);
m.scene()
.circle("A", v(300., 400.), 40.).label("A")
.circle("B", v(900., 400.), 40.).label("B");
m.play(seq![
act().move_to("A", v(900., 500.)).dur(0.6).ease(InOutCubic),
act().highlight("B", ACCENT),
wait(0.5),
]);
broadsheet::run(m);
}§Core Concepts
movie::Moviestores the base scene, timeline clips, section jumps, and beat marks.sceneowns entity declaration: circles, rectangles, lines, arrows, text, cells, code blocks, labels, tags, and follow relationships.animateprovides the fluent verb DSL: move, fade, highlight, pulse, trace, type, retarget, and camera moves.timelineresolves clips into absolute tracks. Its evaluation is a pure function of time, so pause, scrub, frame stepping, and offline recording are deterministic.renderturns a scene snapshot into macroquad draw calls with the built-in broadsheet style fromstyle.layoutcontains small coordinate helpers for rows, grids, trees, and rings.
§Recording
Run live with cargo run --example NAME, or record with:
cargo run --example NAME -- --record out/name --fps 60Useful recording flags include --still S, --from S --to S, --alpha,
--gif, --png, --grain, --scale F, and --frames N.
Modules§
- animate
- The animation DSL: verbs,
seq!/par!, and beats. - easing
- Easing curves. To add one: new variant + match arm in
Easing::apply. - layout
- Position helpers: compute
Vec2s, feed them to the scene builder. Pure functions — no engine state involved. - movie
- The
Movie: top-level container tying a scene to a timeline, with a cursor-based sequencing model. - player
- The runtime: live preview window with transport controls, or offline
recording, both driving the same pure
Timeline::apply(base, t). - prelude
- Everything a movie script needs:
use broadsheet::prelude::*; - primitives
- Drawable primitives. One entity type; its look is data (
Shape). New primitive = newShapevariant + match arm inrender::draw_entity. - record
- Deterministic offline output. Frame
fis rendered att = f / fps, wall clock ignored, so output is bit-identical across runs. - render
- The macroquad draw pass: scene → pixels, plus the newspaper page chrome.
- scene
- The
Scene: an id-addressed store of entities, plus the chainableSceneBuilderused to declare the time-zero state of a movie. - style
- The house visual style: newsprint / editorial.
- timeline
- Keyframe timeline: tracks, clips, and stateless evaluation.
Macros§
- par
- Run clips at the same time (duration = longest member).
- seq
- Run clips one after another. Accepts anything
Into<Clip>(acts,wait(s), nestedseq!/par!). - stagger
- Cascade: each clip starts
delayafter the previous.stagger![0.05; a, b, c].