The Timeline
Schedule events across channels for playback, pausing, and seeking
Events as a Movie
Add a channel (a lane) and a timing block to an emitted event and it becomes a scheduled item on a timeline. The portable Rust player releases items over simulated time, so the session can be played, paused, and seeked — as much a YouTube player as an append-only chat. The same core runs natively and as browser WASM.
// A scene that plays out over time, not all at once.
emitEvent(
eventType: "scene.background.set",
data: {url: "library.jpg"},
channel: "env.background",
timing: {mode: "replace"} // the latest background replaces the prior one
)
emitEvent(
eventType: "actors.narrator.speech",
data: {text: "The room grows dark."},
channel: "actors.narrator.speech",
timing: {key: "openLine", duration: 2}
)channel is the where (the lane / constraint group); eventType is the what. The say() helper is sugar for the common "a character speaks" case.
An event without a
channelstays untimed and is dispatched immediately, exactly as before — the timeline is strictly opt-in per event.
Scheduling: key, anchors, and the timing options
The timing block is how an item takes its place on the timeline. The runtime carries it verbatim; the timeline player interprets it, so it shapes when an item appears and how long it stays when the session is played or seeked.
key names an item so other items can anchor to it. A key is an optional, author-given handle. On its own it changes nothing visible — its only job is to be a reference target:
emitEvent(
eventType: "actors.narrator.speech",
data: {text: "The room grows dark."},
channel: "actors.narrator.speech",
timing: {key: "openLine", duration: 2} // this line is named "openLine"
)
emitEvent(
eventType: "env.sfx",
data: {sound: "thunder"},
channel: "env.sfx",
timing: {after: {ref: "key:openLine", edge: "start", offset: 1}, duration: 0.5}
)
// → the thunder fires 1s after the narrator's line *starts*.after (a start gate) and until (an end cap) reference another item by key:<name> (that exact item) or channel:<glob> (the most recent prior item on a matching channel), at its start or end edge, with an optional offset in seconds (negative = before the edge). Keys need not be unique — a duplicate just re-points future anchors while already-bound ones stay pinned — and an item with no key can still be targeted via channel:.
mode decides what happens to the other items already on the same channel:
| mode | effect |
|---|---|
replace | clears every active item on the channel — latest wins (the current background / board / line) |
queue | waits for the channel to be free, then plays — same-channel items serialize back-to-back |
layer | coexists with the others on the channel (stack several, e.g. multiple captions) |
Full timing reference. Every field is optional; all times are in seconds. Unknown keys are ignored, never rejected.
| field | meaning |
|---|---|
key | a name for this item, so after/until can target it |
mode | replace | queue | layer (see above) |
duration | how long the content lasts (> 0); overrides a media clip's intrinsic length |
after | start gate {ref, edge, offset} — begin at another item's start/end edge (+ offset) |
until | end cap {ref, edge, offset} — end no later than another item's edge |
paddingStart / paddingEnd | silent lead / tail around the content |
minDuration | readability floor — until won't shorten the item below this |
maxDuration | hard ceiling — the item ends by start + maxDuration (0 = kill immediately) |
defaultDuration | fallback length, used only when there's no duration and no media-reported length |
temporary + restoreMode | interrupt the channel's current item, then restore it (resume / restart / none) when this one ends |
wait | hold this item until the named channel glob(s) are clear |
parallel | let this item start even if the channel is busy (skip the implicit queue) |
idle | lowest-priority filler that shows only while the channel is otherwise empty |
releaseMode + releaseDuration | hard (instant) or soft (fade over N seconds) when the item is cleared |
persistence | replay/scrub behavior: state (latest-wins), transient (skip on scrub), fact (apply once, in order) |
The first six rows cover almost everything you'll write; the rest are for fine-grained staging.
Next Steps
- Events - Emit and await structured events
- emitEvent() - Full event API reference
- say() - Put character dialogue on the timeline