The Synod player: a TypeScript canvas library that plays scene.json and draws a Synod program as an animation.
Status: v0.2.0, implements Synod 0.2. The player is milestone M7 in the roadmap. It reads scene.json from the scene builder (M5) and draws it on a canvas, in a browser, in a WKWebView or offscreen. It is a TypeScript library with no runtime dependencies, released as GitHub release assets (npm later). Numbers for Safari on Apple silicon at 120 Hz are still to be measured by hand.
Synod programs are simulated by the compiler, which writes a trace. The scene builder turns the trace and the program's looks into scene.json. The player draws that file. Neither the app nor the site interprets Synod; both draw with the player.
.synod --> synod (compiler) --> trace.jsonl --> scene builder --> scene.json --> player
| Repository | What it is | Status |
|---|---|---|
| dsl | The language specification and implementation guides | Spec v0.2 |
| compiler | The synod command: fmt, check, run |
v0.2.2 released |
| std | The standard library packages | In use by the compiler |
| examples | Sample programs and acceptance tests | Available |
| player (this repository) | Canvas library that plays scene.json |
v0.2.0 |
| mac-app | The Mac app | Planned (M8, M9, M10) |
| agent-skills | Skills for coding agents | Planned (M10) |
| synodlang.github.io | The website and documentation | Install page live; rest planned (M11) |
| homebrew-tap | Homebrew formula for synod |
Updated by compiler releases |
| scoop-bucket | Scoop manifest for synod |
Updated by compiler releases |
| .github | Organization profile and shared workflows | Available |
See 00-architecture.md for the full data flow.
The player reads synod-scene/1 files. The format is defined in 04-formats.md and versioned in dsl. In short:
- The file has
duration(milliseconds of presentation time), acanvassize, athemeof named colors, a list ofshapes, a list ofmarkersand atracemap. - A shape has an
id, akind(circle,rect,label,lineorgroup) andprops. Alinepoints at two shape ids withfromandto. - A shape has
keys: keyframes with anattime. Each keyframe sets only the properties that change. - Colors are theme names such as
AccentorCritical. They are never raw hex values. tracemaps each traceseqto its slot in presentation time, so the player can show simulated time and jump to a fact.- Readers reject unknown major versions and ignore keys they do not know. Within a major version, keys are only added.
Download the release assets from the releases page. Check them with SHA256SUMS.
| Asset | Use |
|---|---|
synod-player-<tag>.js |
ES module (import { createPlayer } from "./synod-player-v0.2.0.js") |
synod-player-<tag>.iife.js |
Classic script that defines window.SynodPlayer |
synod-player-<tag>.d.ts |
TypeScript declarations for the module |
SHA256SUMS |
Checksums of the three files above |
<canvas id="stage" style="width: 100%; height: 100%"></canvas>
<script type="module">
import { createPlayer } from "./synod-player-v0.2.0.js";
const scene = await (await fetch("scene.json")).text();
const player = createPlayer(document.getElementById("stage"), scene, { autoplay: true });
player.on("marker", ({ marker }) => console.log(marker.kind, marker.at));
</script>loadScene(input)parses and checks ascene.json(text or object) and returns{ scene, warnings }. It throws aSceneErrorwith acode(bad-json,unsupported-format,bad-scene). Unknown fields are ignored, as the format requires.compileScene(scene)andrenderToContext(ctx, compiled, at, { width, height, pixelRatio })draw one frame into any 2D context, such as anOffscreenCanvasfor video export. They allocate nothing per frame.createPlayer(canvas, scene, options)returns a player. Options:autoplay,loop,rate,stopAtPause(Predict mode),autoResize,pixelRatio,fonts,timeEventHz.- A player has
duration,time,playing,rate,markers,traceandwarnings, and the methodsplay,pause,toggle,seek,setRate(0.25 to 4),setLoop,setStopAtPause,nextFact,prevFact,factAt,timeOfSeq,setScene,resize,renderFrame(at),on(event, callback)anddestroy. player.hitTest(x, y)takes CSS pixels of the canvas and returns{ id, kind, node, msg }for the topmost drawn circle or rect that has anodeormsg, ornull.player.nodeBounds()returns{ node, x, y, w, h }per node. Both answer for the frame last drawn and follow the hit-testing rules of the formats guide (4 px slop, labels and lines never hit). The same rules are exported ashitTestScene(compiled, at, view, x, y)andnodeBoundsScene(compiled, at, view). Scenes withoutnodeormsg(older compilers) never hit.- Events:
time,marker(fired when playback crosses a marker, never on seek; a pause marker carrieschoiceswhen the question has them),fact,play,pause(with a reason ofuser,markerorend) andscene. attachWebKitBridge(player)posts these events towindow.webkit.messageHandlers.synodPlayer. In the IIFE build,SynodPlayer.mount("#stage", options)creates a player and attaches the bridge; Swift then callsplayer.setScene(jsonString).
The player bundles no fonts. Labels use the system sans font at 16 px, weight 500.
Node 22.18 or newer is required.
npm ci
npm run check # lint, typecheck, tests (including display-list goldens), build
npm run bench # frame cost in Node with a no-op context (CI gates it)
npm run demo # build and serve the demo at http://127.0.0.1:5173/
npm run test:pixel # Playwright: WebKit pixel goldens, Chromium smoke, WebKit bench
npm run build writes dist/synod-player.js, dist/synod-player.iife.js, dist/synod-player.d.ts and dist/demo/, and fails when a bundle is over its size budget. Demo: open ?scene=<url> to load a scene, ?bench=1 to measure frame timing.
The golden scenes in test/scenes are copied byte for byte from the compiler; node scripts/sync-scenes.ts ../compiler --check verifies them. Pixel goldens depend on the exact Playwright container, so they are generated only there (npm run test:pixel:docker -- --update-snapshots, or the update input of the ci workflow).
A release is made by pushing a tag vX.Y.Z that equals the version in package.json and has a matching section in CHANGELOG.md.
- Interpolation. Between two keyframes the player uses the smootherstep curve for every property.
- Color. Colors interpolate in OKLab, not in RGB.
- Style. Shapes have rounded corners and no soft shadows. The ground is a dark, 3Blue1Brown-style background taken from the theme.
- Markers. A
pausemarker comes fromlesson.Askand stops playback so the viewer can predict. Acaptionmarker comes fromlesson.Note. Abrokenmarker shows an invariant that failed, at the moment it broke. - Determinism. The same scene always draws the same frames.
- The Mac app hosts the player in a
WKWebView, offline, and renders frames from it for video export. - The site runs
synod.wasmin the page and plays the result with the player, so every example is editable and re-runnable.
From the roadmap: every golden scene plays at 120fps in Safari and WKWebView on Apple silicon, with 50 nodes and 200 messages in flight, and frame-render golden images match.
Changes to the scene format start in dsl. Changes to the player go through pull requests here; run npm run check first. CI checks line endings, lint, types, tests, the bench, vendored scenes and the browser tests.
Every synodlang repository shares MAJOR.MINOR with the language version, so this one releases as v0.2.x.
Synod is named after the Synod protocol, the core consensus algorithm in Leslie Lamport's paper "The Part-Time Parliament" (ACM Transactions on Computer Systems, 1998; first submitted in 1990). The paper tells it as the story of the parliament of the ancient Greek island of Paxos, whose part-time legislators could agree on decrees although they came and went; today the algorithm is known as Paxos. Lamport later explained it plainly in "Paxos Made Simple" (2001), and in 2013 he received the ACM A.M. Turing Award for fundamental contributions to the theory and practice of distributed and concurrent systems. His "Time, Clocks, and the Ordering of Events in a Distributed System" (1978) and his TLA+ specification language shaped how we reason about such systems. We are grateful for his work, and we hope Synod honours it by making these ideas easier to watch and understand. The player is how you watch such protocols unfold.
Implements Synod 0.2 (specification: https://github.com/synodlang/dsl).
Licensed under the Apache License 2.0.