Tool: render-demo
render-demo is the tool that builds every gif on this docs site. It takes a led-ticker config.toml, runs the real ticker engine against the test stub canvas, captures every SwapOnVSync frame, and encodes the result to a gif at panel resolution. The script lives at tools/render_demo/render.py.
It exists so the docs can show what a config actually looks like on a panel without anyone needing real hardware on hand. The same engine that drives the LEDs drives the renderer, so the output matches what you’d see on the sign frame-for-frame — minus the physical brightness and viewing-angle differences of real LEDs.
Two pipelines
Section titled “Two pipelines”The docs site has two demo pipelines, each with its own source directory and output directory:
- Auto-rendered — TOML lives in
docs/site/demos/, output gif indocs/site/public/demos/(gitignored). Regenerated on every Cloudflare build bydocs/site/scripts/build-demos.mjs. - Long-running — TOML lives in
docs/site/demos-long/, output gif indocs/site/public/demos-long/(committed to git). Rendered manually viamake render-long-demos.
The auto-rendered path is for static demos that finish in five seconds; the long-running path is for data-fetch widgets (RSS, weather) where five seconds isn’t enough and Cloudflare can’t make the live HTTP calls anyway. Full setup details live in the docs/site README.
Quick start
Section titled “Quick start”Render any TOML to a gif from the repo root:
make render-demo CONFIG=path/to/config.toml OUT=out.gifFor a minimal demo TOML — the same shape the auto-rendered pipeline expects:
[display]rows = 16cols = 32chain_length = 5default_scale = 1brightness = 60
[[playlist.section]]mode = "slideshow"loop_count = 1hold_time = 4.0
[[playlist.section.widget]]type = "message"text = "Hello, panel"font_color = "rainbow"make render-demo CONFIG=demo.toml OUT=demo.gifThe output gif is upscaled 4× by default so individual LED pixels read clearly on retina displays.
Planning before rendering
Section titled “Planning before rendering”If you’re not sure what --duration to use, run gif-plan first — it estimates the render duration from your config and suggests the right # render-duration: header value before you commit to a 10-second render.
CLI flags
Section titled “CLI flags”The Makefile target wraps uv run python tools/render_demo/render.py. The user-facing flags:
| Flag | Default | Purpose |
|---|---|---|
--output / -o | (required) | Output gif path. |
--duration N | 5 | Capture duration in seconds. Longer means a bigger gif. |
--upscale N | 4 | Pixel upscale factor. The gif is N× panel resolution per side. |
--start-section N | 0 | Start at this section index. Useful for jumping past intros on long configs. |
There’s no fps flag — the engine runs at a fixed ~20 fps (50 ms per tick) and the gif is encoded at that cadence to match wall-clock playback.
For per-demo overrides without touching the build script, the auto-render pipeline parses an optional # render-duration: N comment from the top of each TOML. A wide marquee that needs more than five seconds to complete one full traversal can declare it inline:
# render-duration: 12[[playlist.section.widget]]type = "message"text = "A long marquee that needs more time to scroll all the way across..."The parser lives in readRenderDuration in build-demos.mjs. Malformed values fall back to the five-second default.
API-key-required widgets
Section titled “API-key-required widgets”Long-running demos sometimes need API keys — the weather.current plugin widget needs WEATHERAPI_KEY, and so on. To keep contributors without those keys from hitting render failures, the long-demo pipeline reads an optional # requires-env: VAR comment from the top of each TOML:
# requires-env: WEATHERAPI_KEY[[playlist.section.widget]]type = "weather.current"location = "Brooklyn, NY"When make render-long-demos walks docs/site/demos-long/*.toml, any TOML whose declared env var isn’t set in the current shell (or in .env) is skipped with a [render-long-demos] SKIP … log line. Everyone else’s demos still render.
Live-API widgets in a five-second window often show cached or placeholder data
Section titled “Live-API widgets in a five-second window often show cached or placeholder data”The renderer captures every SwapOnVSync so durations match wall-clock playback exactly, but five seconds usually isn’t enough for a weather.current widget to complete a network fetch. Use the long-running pipeline (# render-duration: 30) for those, not the auto-rendered one.
The test stub canvas does not paint hi-res TTF fonts identically to real hardware
Section titled “The test stub canvas does not paint hi-res TTF fonts identically to real hardware”The threshold-based rasterization path runs the same way, but subtle pixel-level differences can show up at small font sizes or with thin-stroked typefaces. If a demo gif looks slightly off relative to the real panel, the panel is the source of truth — tune font_threshold against hardware, not the gif.
Output gif files are larger than panel resolution would suggest
Section titled “Output gif files are larger than panel resolution would suggest”The renderer upscales gifs --upscale × before encoding (default 4), so a 256×64 bigsign panel becomes a 1024×256 gif on disk. That’s deliberate — the upscaling makes individual LED pixels read clearly on retina displays — but it does mean file sizes grow with the square of the upscale factor. Drop --upscale 2 if you need a smaller artifact.