Skip to main content

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.

Loading…

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.

Loading…

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…

loading​

Displays a loading spinner on a target node (simulates processing).

{ type: 'loading', object: 'db', duration: 900 }
Loading…

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).
The text is in value

The 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'],
],
},
}
FieldAffected modesRole
typeallcode | text | image | table | html.
valuecode/text/image/htmlSource text, image path, or markup.
languagecodeHighlighting language (cf. HighlightLanguage).
urltext/htmlAddress bar of the window.
columnstableColumn headers.
rows_datatableData rows.
screen_widthhtmlDesign width in px — turns the panel into a screen.
screen_heighthtmlDesign height in px (default screen_width × 0.625).
framesimageFrame sequence, played from the animation's clock.
fpsimageRate of frames (default 12, clamped to 0.1–60).
loopimagefalse holds the last frame instead of repeating.

The three text modes, side by side (code, text, then table):

Loading…

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/….

Style inline, never class

class 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&nbsp;%</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, 12px means 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.

Loading…

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.

Prefer data: URIs

A 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.

Loading…

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:

Loading…

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 }
Loading…

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":

Loading…

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 }
Loading…

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:

Loading…

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.

Loading…

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:

Loading…

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:

Loading…

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.