Skip to main content
A moment is a reveal. It is over in 0.46 seconds at most, and verify measures it as one step in the picture at its cue. Anything longer is a motion, the other noun the page contract has. A motion may run for any length, it is declared on the element it moves, and its length is the seconds between two cues rather than a number in the markup, so a line voiced again times it again. DeckTalk asks for every thing that moves to be declared, and refuses motion it was not told about. Verify reads a reveal as a rise in how much of the frame changed, so anything moving beside the reveal rises too and reads as the reveal starting early. A declared thing is masked out of every cue it overlaps, and a motion that starts or stops on a cue is measured on its own instead. decktalk init --example motion my-motion writes a project that does every kind of motion on this page. Its first section moves a packet across the frame, turns a ring and springs a card in. Its second fills a bar from script. Its third is drawn by a Python script rather than a page. That script is a program the project names, and a machine refuses a project’s programs until you allow them, because a program runs with your user’s rights and can read every file you can, your .env included. Read render/draw_bars, then build the example on placeholders with it allowed for that one run:
Programs a project names says how to allow them on a machine instead.

Choose the noun

A <video> or an <audio> element on a page is PAGE_REALTIME_MEDIA. It plays at its own rate rather than on the stepped clock, so the recording could not say which frame of it shows at which second. Footage belongs in a clip section, and a moving picture a program makes belongs in a render section.

The four eases

data-motion-ease, and ease in DeckTalk.motion, names how a motion moves between its two cues. firm is the default because an arrival has to stop on a frame edge to be measured. Verify finds an arrival as the last frame on which the motion still moved, and a motion that keeps some speed to the end has a last moving frame that sits exactly on its cue. A motion that eases to rest moves less and less, so its last measurable frame comes before its cue by however long the slow tail is, and a viewer sees it settle before the word. So a settle or spring arrival is allowed only on a cue that cues.json marks "verify": false. Anywhere else, decktalk check raises MOTION_EASE_UNMEASURABLE before anything is recorded. A departure is measured whatever the ease, because every ease leaves its first frame moving.

A motion in markup

A motion names the @keyframes it moves through, the cue it departs on and the cue it arrives on. Both are cue names local to the slide, like a moment’s, and both join the slide’s cues, so each needs its row in cues.json. This is the packet in the example’s first section.
deck/index.html
cues.json
The packet leaves on “go” and stops on “now”, and the runtime plays travel for exactly the seconds between the two. Nothing in the markup is a length, so rewriting the sentence between the two words makes the packet travel faster or slower and still land on “now”. Until its departure the element rests as its own style draws it, so the first step of its @keyframes should be that same style. A first step that differs makes the element jump on the departure frame. A motion whose arrival is not after its departure, or whose cue has no second, never plays, and the page reports it as PAGE_MOTION_CUE_UNKNOWN. The label beside the packet is an ordinary data-in on the same cue the packet arrives on. Verify measures it as a reveal with the packet masked out, which is what keeps the cue measured as a step.

A spring that comes to rest

The card in the same slide springs in from “spring” and comes to rest on “rest”. A spring has no edge to measure, so the arrival cue opts out of verify.
deck/index.html
cues.json
Its departure on “spring” is still measured, and only its arrival is left out.

A loop

A loop names the @keyframes it turns through and how long one turn takes. data-loop-seconds runs from 0.2 to 60 and is 1 when it is left out.
deck/index.html
A loop starts turning when its slide mounts and turns until the slide leaves. It names no cue, so it is never a landing, and verify masks the whole box it sweeps out of every cue on its slide.

A motion drawn from script

DeckTalk.motion(el, { from, to, ease, draw }) is the same noun for a page that draws the motion itself. from and to are cue ids, ease is one of the four eases and is firm when it is left out, and el is the element, or a selector for it, that the motion draws inside. draw(progress, seconds) is called on every frame between the two cues, with the eased progress from 0 to 1 and the section second. Nothing is drawn before the departure, and the last call is made exactly at the arrival with progress 1. This is the bar in the example’s second section. Nothing is drawn after the arrival, so draw finishes by then. Just before its last call, draw is called once more with progress 1 at half a second past the arrival, and when the element shows something different there, such as a fade timed from the second once the progress reached 1, the page reports PAGE_MOTION_UNFINISHED at the arrival’s cue: the picture stops where the arrival left it. Move the arrival to a later cue, or end what draw changes by the arrival.
deck/index.html
No attribute in the markup names “fill” or “finish”, so the slide lists them in data-owns. That is how a slide owns a cue only a script serves. A cue the page does not know, or an arrival that is not after its departure, is PAGE_MOTION_CUE_UNKNOWN, and the motion draws nothing.

A frame hook and its region

DeckTalk.frame(fn, { region }) calls fn(t) on every frame with the section second, for something that is drawn rather than animated: a canvas, a typed line, a scrolling voice line. region is the element, or a selector for it, that the hook draws inside, and verify masks that box out of every cue while it is on the page. A hook with no region is PAGE_MOTION_UNDECLARED, because nothing tells verify where it draws. A hook is also how the recorder knows a frame changed: it writes the frame before again for a frame where nothing it knows of moved, so anything a page draws outside the DOM, a cue, an animation or a declared motion is drawn from a hook. A page that holds a canvas is captured on every frame regardless. Still frames lists what counts. A hook that throws is reported once as PAGE_FRAME_THREW and is not called again. This voice line scrolls under a fixed playhead at the speed of speech. It is drawn from the second alone, so a recorded frame and a preview frame of the same second are the same picture.
deck/index.html
The sample is drawn for the wide canvas. On a square or vertical frame, size the canvas from --dt-width, which the runtime puts on :root. The pictures above the voice line still arrive with data-in on their words, and verify measures each of them with the voice line masked out.

A composition page

A composition page is a page section whose page draws the whole picture itself, in a canvas, in WebGL or with three.js. It is an ordinary page section, and it reads its timing from the runtime rather than keeping a clock. Key every change on a cue’s second from DeckTalk.cues rather than on a number of your own, so a line voiced again moves the composition with it. A cue that only the composition uses still needs a slide that owns it, through data-owns or a script slide’s owns. A WebGL canvas drawn from a frame hook records frame for frame, and two recordings of it are the same frames. That is tested with raw WebGL and a context created with preserveDrawingBuffer: true. three.js draws through the same WebGL and is not tested itself.

A render section

A render section is a section whose picture a program draws rather than a page. It names a render_command and no page and no scene.
decktalk.toml
It is narrated exactly like a page section. Its take decides its length, cue resolves its cues from its words, and the voice runs straight through it. That is the difference from a clip section, which carries its own sound and pauses the narration while it plays. record writes the section’s motion document and runs the command once, in an empty scratch folder, with each {name} word in it replaced. The motion document holds the frame, the length, every spoken word and every cue, and every second in it is on the section clock.
build/recordings/render/03/motion.json
The numbers above show the shape and are not from a real run. A renderer keys a change on a cue at frame round(cue * fps). The command is a program the project names, so it runs only on a machine that allows it through record.programs, as programs a project names says. It runs with your user’s rights and can read every file you can, .env included. It runs in an empty scratch folder rather than the project, a word that names a project file by a relative path such as render/draw_bars is handed to it as that file’s absolute path, and it is given the scrubbed environment with no proxy, so a credential in your shell does not reach it. It may run for tools.timeout_seconds. It fails the section when it exits with an error or writes no video. The section is recorded again when the command, the motion document or a project file the command names changes, so editing render/draw_bars draws the section again. A machine whose record.page_policy is untrusted runs no program a project names, so a render section is refused there with INPUT.

Landings

A command may write {landings} as a list of lanes in the shape a page reports its own, and verify then masks and measures them as it does a page’s.
build/recordings/render/03/landings.json
A file in any other shape fails the section. A section whose command writes no landings has every cue skipped as rendered, and verify says on its stream that it measured none of them, because a frame difference over a picture nobody described could read the picture’s own motion as a reveal. render/draw_bars in the example is a Python script with no dependency beyond the standard library. It reads the motion document, composes one ffmpeg filter graph in which a bar slides in on each of the three cues, runs the ffmpeg it was handed, and writes one lane per bar so verify measures each departure against its word. Remotion, a three.js scene under a frame stepper of its own, or any program that turns a JSON file into a video fits the same contract. Wiring Remotion as a render_command is untested, and it is never a dependency of DeckTalk. Remotion is free for individuals and small companies and paid above that, so read its licence before a project depends on it.

What verify does with each

The recorder samples every declared motion, loop and region on every frame it steps, and the recording log keeps one lane for each: the box it swept, the size of the element, the first and last second it moved, and the cues it lands on. The lane report gives its fields. Masks. Every cue is measured with every lane that is alive anywhere from its control spans to its last probe masked out of the changed share, the control share and the onset. The Masked column of the verify table says how much of the frame that took. Over verify.motion_mask_max_percent of the frame, 40 percent by default, raises MOTION_MASK_LARGE, a warning that the cue was judged on what was left. Landings. A cue that is only a motion’s departure or arrival, where the page reveals nothing else, is skipped in the cue table as motion_landing and measured on the motion’s own lane instead. Inside the lane, frame by frame, the motion counts as moving while more than verify.motion_floor_percent of its own area changes, 0.5 percent by default. Its departure is the first moving frame from one reference lead before its cue, and its arrival is the last moving frame. Each landing is a row in a second table, under the cue table. A landing on a cue that cues.json marks "verify": false has no row. Verify gives the whole measurement.

On a still

storyboard and check draw stills, and a still has no clock. A loop rests in its own style, and a motion whose departure cue has fired is drawn where it ends.

The findings