Action types
The following action types are available. All share a set of common fields
(timeline lifecycle: id, duration,
wait_for, keep_until, keep_until_next, keep_until_end).
move
Moves a dynamic object from from to to.
{
type: 'move',
object: 'req', // id of a packet
from: 'browser', // id of a node
to: 'api', // id of a node
duration: 600,
}
Produces two breakpoints: appearance and arrival. By default, the object disappears at the end of its animation.
arrow
Draws an animated arrow between two nodes (progressive drawing).
{
type: 'arrow',
from: 'a',
to: 'b',
style: 'dashed', // 'solid' | 'dotted' | 'dashed' | 'animated'
path: 'bezier', // 'bezier' | 'simplebezier' | 'straight' | 'step' | 'smoothstep'
arrow_head: 'both', // 'forward' | 'backward' | 'both' | 'none'
text: 'request',
}
For permanent arrows (decor displayed right from the initialization),
use the connections root array instead.
parallel
Wraps child actions executed at the same time.
{
type: 'parallel',
actions: [
{ type: 'move', object: 'p1', from: 'a', to: 'b' },
{ type: 'arrow', from: 'c', to: 'd' },
],
}
The wait_for and keep_until inside a parallel continue to
work normally. Here a move and an arrow of different types start at the
same instant.
loading
Displays a loading spinner on a target node (simulates processing).
{ type: 'loading', object: 'db', duration: 900 }
set_content
Mutates the content of a node. The content follows the ObjectContent shape, shared
with node.content (see content):
code: dark terminal with Prism highlighting, no URL bar; the font adjusts to fit the longest line without wrapping.text: browser window with a configurable URL bar (url).image: image (value= path/URL) displayed in a window, or an animated sequence (frames).table: data table (columns+rows_data).html: rich markup, sanitized (value).
valueThe content-bearing field is named value (not content). The content is
the wrapping object.
code mode:
{
type: 'set_content',
object: 'editor',
content: {
type: 'code',
language: 'javascript',
value: 'const add = (a, b) => a + b;',
},
}
text mode (browser window with address bar):
{
type: 'set_content',
object: 'browser',
content: {
type: 'text',
url: 'exemple.com/users',
value: 'Alice\nBob',
},
}
table mode:
{
type: 'set_content',
object: 'admin',
content: {
type: 'table',
columns: ['id', 'nom'],
rows_data: [
[1, 'Alice'],
[2, 'Bob'],
],
},
}
| Field | Affected modes | Role |
|---|---|---|
type | all | code | text | image | table | html. |
value | code/text/image/html | Source text, image path, or markup. |
language | code | Highlighting language (cf. HighlightLanguage). |
url | text/html | Address bar of the window. |
columns | table | Column headers. |
rows_data | table | Data rows. |
screen_width | html | Design width in px — turns the panel into a screen. |
screen_height | html | Design height in px (default screen_width × 0.625). |
frames | image | Frame sequence, played from the animation's clock. |
fps | image | Rate of frames (default 12, clamped to 0.1–60). |
loop | image | false holds the last frame instead of repeating. |
The three text modes, side by side (code, text, then table):
html — rich markup
The markup in value is parsed inert and then rebuilt from an allow-list:
formatting, lists, tables, img, and a narrow inline-SVG subset. A spec is data
— it may come from a CMS or a form — so anything not on the list is dropped
silently: <script>, <style>, <iframe>, every on* handler, and any URL
whose scheme is not http, https or data:image/….
classclass and id are dropped, and this is not a security decision. A rasterized
video frame carries only the CSS rules whose selector contains rdfa, so a class
the host page styles would render on screen and render unstyled in the exported
video — the same spec, two different pictures. Inline style travels with the
element, so use it. The style attribute is filtered too: position and custom
properties are refused (they would let content escape its node or repaint the
player), and url(…) is refused in every value.
{
type: 'set_content',
object: 'card',
content: {
type: 'html',
value:
'<h3>Cache warmed</h3>' +
'<ul><li><b>Hit rate</b> 94 %</li></ul>' +
'<p style="color:#16a34a">All shards green</p>',
},
}
Unlike text, an html panel shows the browser chrome only if you give it a
url — rich markup is as often a legend or a card as it is a web page.
screen_width — a page that scales instead of re-flowing
By default a rich panel behaves like a paragraph: its box is capped by the node's allowance and the type keeps the size the stylesheet gives it, so the same markup is a two-column layout on a wide player and a stack of wrapped lines on a narrow one. For a simulated interface that is the wrong behaviour — the arrangement IS the message, and a student has to recognise the same screen at every size.
Set screen_width and the panel becomes a screen: the markup is laid out
once, in that pixel space, and the renderer only ever changes the SCALE.
content: {
type: 'html',
url: 'videos.example/watch?v=42',
screen_width: 480, // the design space the markup is written against
screen_height: 300, // optional — defaults to 480 × 0.625, a 16:10 lid
value: '<div style="display:flex">…</div>',
}
Three consequences worth knowing:
- The address bar moves inside the screen, so it scales with the page rather than sitting beside it at the player's own size — a simulated browser is one picture.
- The player's own scaling stops at the screen's edge. Inside,
12pxmeans 12 design pixels, at every window size. - A screen is a fixed box: what does not fit is cropped, exactly as a real one crops what is below the fold.
Below: a page whose shell never moves while its data arrives. Both set_content
carry fade_in_ms: 0, so the swap is instant — and because the shell markup is
identical either side of it, only the region that changed appears to change.
That is what makes it read as a single-page app rather than a reload.
image — a still, or an animated sequence
value alone is a still image. frames is an ordered list of image sources
played from the animation's own clock: the frame on screen is a pure function
of t, exactly like every other rendered value.
{
type: 'set_content',
object: 'worker',
content: {
type: 'image',
frames: ['data:image/svg+xml,…', 'data:image/svg+xml,…'],
fps: 8,
},
}
That is what an animated GIF cannot do. A GIF's playhead is not reachable from
JavaScript, so it keeps running while the player is paused, does not rewind when
the scrub bar goes backwards, and an exported frame catches whichever moment the
decoder happened to be on. A frames sequence pauses, rewinds and exports
frame-accurately.
A set_content with no explicit duration is given the time its sequence needs
to play through once, instead of the flat beat a still image gets.
data: URIsA rasterized video frame cannot load anything from another origin. A frames
entry (or an <img src> inside an html panel) pointing at a remote URL renders
on screen and makes the video export fail.
comment
Displays a text bubble fading in near a node, identified by object.
{
type: 'comment',
object: 'browser',
text: 'The user clicks',
duration: 400,
}
Omniscient comment: omit object to display the bubble at the top
of the scene rather than next to a node — ideal for a general narration that
is not attached to any specific component.
{ type: 'comment', text: 'Step 2 — Authentication' }
Below, an omniscient comment (top of the scene) followed by two bubbles attached to nodes:
set_visible
Shows or hides a static node with a fade. If this parameter is false (or
if the node has no initial visible in its configuration see
Nodes), it appears hidden.
The visibility state persists until the end of the timeline (or until
a contrary set_visible).
{ type: 'set_visible', object: 'cache', visible: true }
set_color
Recolors a static node at runtime. Mutates any of the three color channels —
background_color, border_color, text_color — over duration ms, with a
deterministic eased cross-fade between the previous color and the new one (via
CSS color-mix, so it stays scrubbable in both directions). Only the channels
you provide change; the others keep their current value.
{ type: 'set_color', object: 'node', background_color: '#1f2937', duration: 600 }
Same value space as the node colors: a predefined
CSS name (tomato, steelblue…) or an exact hex (#1a1a1a). The same
auto-derivations apply — a background_color without border_color derives a
coordinated border, and without text_color a high-contrast ink (outside
syntax-highlighted areas). The reached color persists until the end of the
timeline (like set_visible), and successive set_color on the same node
chain: each one cross-fades from the previous color.
This is the core operation for algorithm visualizations that recolor nodes — for instance the red/black recoloring of a red-black tree. Below, a node inserted "red" is recolored to "black":
set_color also recolors a permanent connection:
pass the connection's id as object and a single color (the three node
channels don't apply to a line). It cross-fades the line and its arrow head from
the current color — ideal for lighting up the edge a graph traversal just
followed:
{ type: 'set_color', object: 'edge', color: 'seagreen', duration: 500 }
set_icon
Updates a node's corner icon badge at runtime — the small overlaid badge
(icon: a known technology, a registered icon, or short free text). The badge
swaps to the new value when the action starts; the reached value persists
until the end of the timeline (like set_color), and successive set_icon on
the same node chain. An empty string clears the badge.
{ type: 'set_icon', object: 'v', icon: '7' }
This is how an algorithm visualization keeps a per-node scalar that evolves
legible on the node itself — a Dijkstra node's tentative distance (∞ → 7 → 5),
an A* node's f = g + h — rather than only in a comment. Below, a node's badge
walks from ∞ down to its final value:
rotate
Animates the visual rotation of a node. Two mutually exclusive modes; only the visual rotates (the label below stays upright), and the angle reached when the rotation ends persists until the end of the timeline.
Target angle to (degrees, clockwise): a single eased rotation. The
starting angle is the node's current rotation — its static
rotation field, or the
target of a previous rotate — so successive rotations chain.
{ type: 'rotate', object: 'gear', to: 90, duration: 600 }
A full turn is just to: 360 (from 0); rotate back with to: 0.
Continuous spin spin (degrees per second, signed — positive turns
clockwise, negative counter-clockwise): the node turns at a constant speed
(linear, no easing). How long it spins reuses the usual
timing fields — duration, keep_until, or
keep_until_end — instead of a target angle.
{ type: 'rotate', object: 'gear', spin: 360, keep_until_end: true } // until the end
{ type: 'rotate', object: 'gear', spin: -180, duration: 3000 } // 3 s, counter-clockwise
{ type: 'rotate', object: 'gear', spin: 90, keep_until: 'response' } // until action "response"
If both to and spin are given, spin wins.
rotate_subtree
Restructures a binary tree with a tree rotation around a pivot, then animates
the nodes gliding to their new places while the parent/child edges re-route.
Only valid in direction: 'tree', which
supplies the tree block this action mutates.
{ type: 'rotate_subtree', object: '10', rotation: 'left' } // or 'right'
A left rotation lifts the pivot's right child; a right rotation lifts its left child (the required child must exist, else a warning is emitted and nothing moves). A rotation preserves the tree's in-order traversal, so horizontal positions stay put — the motion is the pivot and the moved subtree changing depth.
Successive rotate_subtree actions chain (each starts from the topology the
previous one left), which is how AVL double rotations are expressed — an
LR case is a left rotation on the child followed by a right rotation on the
grandparent. Below, a single left rotation rebalances a right-leaning chain:
highlight
Highlights (pulsing halo) a static node or a permanent connection,
identified by their id.
{ type: 'highlight', object: 'db' }
Here the halo pulses first on the db node, then on the permanent
connection link:
To emphasize a connection from the start (no timeline action), use its static
highlighted field
instead.
flow
Animates an electric current circulating along a chain of wires — the
signature animation of an electrical schematic.
A train of evenly-spaced charge dots rides the route (an ordered list of
node / node:pin references forming a path or a closed loop), advancing one
full lap per duration ms and, by default, looping continuously. Each
consecutive pair of the route must be joined by an actual wire (connection), so
the charges follow the drawn path exactly.
{
type: 'flow',
route: ['battery:+', 'R1:a', 'R1:b', 'led:a', 'led:b', 'battery:-'],
color: '#f59e0b', // default: the theme accent
keep_until_end: true,
}
reverse: true sends the charges the other way (electron flow − → + instead of
conventional current + → −); loop: false plays a single pass; count sets how
many dots ride the route (default: about one per segment). Being a pure function
of t, the current scrubs both ways like everything else. See the Electrical
circuit demo in the gallery.
toggle
Flips an electrical switch / push_button between open and closed, swinging
the lever over duration ms. Like set_visible, the reached
state persists until the end of the timeline (or the next contrary toggle)
and scrubs both ways. Pair it with a flow that starts once the contact is
closed to show the circuit energizing.
{ type: 'toggle', object: 'sw', closed: true }
wait
Inserts a dead time: nothing happens for duration ms (default:
1000). The action produces no element on screen; it simply
delays the next step. Content already placed (via keep_until_next)
remains displayed during the wait, which freezes the frame for the desired time.
{ type: 'wait', duration: 1500 }
Useful to let the viewer read a step before continuing, or
to pace a playback in autoPlay.
Complete reference
For the exhaustive list of fields (types, default values, constraints), see the API reference generated from the JSON Schema.