Skip to main content

Timeline and steps

The engine compiles your timeline array into a pure chronology: at any given time t (ms), the visual state is determined by an evaluate(timeline, t) function. Backward seeking, step-by-step navigation, and SSR come for free from this design.

Logical steps

Each root action (at the first level of timeline) constitutes a logical step. The "Previous" / "Next" buttons of the player navigate from one step to another.

A short pause (STEP_GAP) separates two consecutive steps: the "Next" stop thus shows the "settled" step on its own, without overlapping the appearance of the next one.

In the example below, the five root actions form five steps: use "Previous" / "Next" to step forward.

Loading…
timeline: [
{ type: 'comment', object: 'browser', text: '1. …' }, // step 1
{ type: 'move', object: 'req', from: 'browser', to: 'api' }, // step 2
{ type: 'loading', id: 'work', object: 'api' }, // step 3
{ type: 'move', object: 'res', from: 'api', to: 'browser', wait_for: 'work' }, // step 4
{ type: 'comment', object: 'browser', text: '2. …', keep_until_end: true }, // step 5
],

Breakpoints

Actions produce breakpoints on which navigation can snap:

  • A move produces two points: when the object appears at its origin, then when it arrives at its destination.
  • arrow, loading, set_content, comment, highlight produce one point: their "settled" state at the end of the animation.

Synchronization between actions

Three mechanisms allow you to coordinate actions:

wait_for

The action starts at the end of another action referenced by its id:

{ type: 'loading', id: 'dbwork', object: 'db', duration: 900 },
{
type: 'move',
object: 'rows',
from: 'db',
to: 'api',
wait_for: 'dbwork', // starts when the loading finishes
},

parallel

Wraps multiple actions executed at the same time:

{
type: 'parallel',
actions: [
{ type: 'move', object: 'p1', from: 'a', to: 'b' },
{ type: 'move', object: 'p2', from: 'c', to: 'd' },
],
}

Here, a gateway broadcasts three requests to its services: the three packets depart simultaneously.

Loading…

Custom durations

Each action accepts a duration (ms). The defaults are:

Action typeDefault (ms)
comment, set_contentderived from the content — see below
movederived from the distance — see below
arrow500
loading1200
highlight600
set_visible300
wait1000

Reading time

An action that carries something to read — a comment's text, a set_content's panel — needs to stay on screen long enough for that text to actually be read. Write no duration and the engine works one out from the content itself: a fixed cost to notice what appeared, plus the length divided by a reading speed. Code is given more time than prose, a table more than a label, and the result is bounded so a two-word badge does not flash past and a long paragraph does not hold the animation hostage.

// No duration: each bubble stays for as long as its own text needs.
{ type: 'comment', object: 'lb', text: 'Request 1 → Backend 1' },
{ type: 'comment', text: 'The load balancer distributes requests in turn, to balance the load.' },

The second bubble above is three times longer than the first, so it stays roughly three times as long — without anyone counting characters.

A comment's time is counted once the bubble is fully there: it fades in first (over fade_in_ms, or 250 ms by default), and only then does the reading time start. So duration: 3000 means three seconds of readable text, not three seconds that include its own arrival.

An explicit duration always wins. It is a stated intent, not an estimate, so reach for it whenever the content's length is not what should decide the timing — a dramatic pause, a beat held for effect.

If the derived pace reads too fast or too slow for your audience, adjust it in one place with pace on the spec rather than action by action:

const spec = {
pace: 1.25, // a quarter more reading time everywhere it was derived
nodes: [...],
packets: [...],
timeline: [...],
};

pace scales only the durations the engine derived. A duration you wrote yourself is left exactly as written.

tip

Reaching for wait right after a comment is usually a sign that the comment needed more reading time. Remove both the wait and the comment's duration, and let the length decide.

Travel time

The same idea applied to space: a move with no duration derives one from the length of its trip, so two hops of different lengths are covered at the same apparent speed instead of in the same time. The eye reads a speed, not a duration — and a speed that changes for no reason reads as a mistake.

// No duration on either: the long hop simply takes longer.
{ type: 'move', object: 'rq', from: 'client', to: 'lb' },
{ type: 'move', object: 'rs', from: 'backend', to: 'client' },

A packet also waits at its origin long enough to be read. It carries text — a header, a query, a row count — and it meets the reader while still standing still, so that pause is at least as long as its content needs. It is counted once per packet: the next leg of the same route shows the same text.

Distances are measured in a fixed reference frame, never in the player's real pixels. That is what keeps one animation to one chronology: resize the player and the total duration, the step boundaries and the exported video all stay put.

Here too, an explicit duration wins. Reach for it when the pace of a movement carries meaning the distance cannot — a deliberately slow hand-off, a burst meant to feel abrupt.

Visual persistence

By default, an animated element remains visible for a short time after the end of its animation (ARRIVE_HOLD) and then disappears. Three props extend this presence:

keep_until

Remains visible until the start of an action targeted by id:

{ type: 'arrow', id: 'A', from: 'a', to: 'b', keep_until: 'C' },
{ type: 'move', object: 'p', from: 'a', to: 'b' },
{ type: 'comment', id: 'C', object: 'a', text: 'fin' },
// The arrow A remains displayed until the start of comment C.

keep_until_next

Remains visible until the start of the next root step (thus through the inter-step pause). Defaults by action type:

Action typekeep_until_next default
arrow, comment, set_content, highlighttrue
move, loading, set_visiblefalse

keep_until_end

Remains visible until the end of the chronology:

{ type: 'highlight', object: 'db', keep_until_end: true },

Offsets and fades

Three fields refine the fine timing of an action, beyond its logical sequencing.

delay_ms

Delays the start by delay_ms milliseconds, after the resolution of wait_for and snapping to the step. Its main use is to sequence actions within a parallel ("cascade" effect):

{
type: 'parallel',
actions: [
{ type: 'move', object: 'p1', from: 'a', to: 'b' },
{ type: 'move', object: 'p2', from: 'a', to: 'b', delay_ms: 150 },
{ type: 'move', object: 'p3', from: 'a', to: 'b', delay_ms: 300 },
],
}

The same fan-out as above, but with an increasing delay_ms: the packets stagger instead of departing as a block.

Loading…

Applied to an entire parallel, it delays the whole group.

fade_in_ms / fade_out_ms

Control the duration of the fade-in and fade-out (in ms). 0 gives an instantaneous transition. fade_out_ms has no effect if keep_until_end is true (the element never disappears).

{ type: 'comment', object: 'db', text: 'Lecture', fade_in_ms: 0, fade_out_ms: 600 }

Defaults: fade_out_ms → 250; fade_in_ms → 250 (300 for move).