Skip to main content
The page contract is the whole vocabulary a deck page may write. An element on a slide has four moments, and each of them is a cue name. An element that travels for longer than a moment declares a motion instead, which names two cues. Everything else is either how a moment or a motion looks, which is a closed word the contract owns, or what it means, which is a sentence for the transcript. The second a thing happens is never written on the page, because cues.json owns it.
  1. decktalk-runtime.js implements the contract. DeckTalk serves it at /__decktalk/decktalk-runtime.js on the origin it opens the page at, and a page that would rather follow it by hand can.
  2. A scene is markup: a [data-scene] wrapper holding one <template data-slide> per slide. Attributes do the work, so a page needs no JavaScript of its own.
  3. The runtime adds only the stage element, the slide transitions and the reveal effects. Every rule of its own sits inside :where(), so one rule of yours wins.
  4. To write your first page, follow Your first deck.

The four moments

An element declares up to four moments, and each attribute’s value is a cue name rather than a number. They play in that order: in, back, front, out. An exit declared at or before its own entrance never plays, which is PAGE_MOMENT_ORDER, and the order is read off the cues the slide declares rather than guessed. data-back and data-front are attention rather than reveal. Use them when one thing on the slide has to recede while another is read, so the page never has to remove something and bring it back.

The cue id

Each moment names a cue, and the runtime qualifies that name with its slide into the cue id cues.json carries. Here it is 4.1:expand.
deck/index.html
That split is the contract’s whole shape. The page owns what a thing looks like and what it means, and cues.json owns the second it happens on. An author writing a slide never types a number of seconds, and an author writing cues.json never types a class or an effect.

The subjects

Every attribute is written on one of four subjects, and the table below names the subjects each row admits.

Every attribute

This is the whole contract. Value gives the closed word set where a row has one, and otherwise says what kind of value the row takes. A row that publishes a default may be written bare to take it, so data-words alone is data-words="highlight". Range is the range the row admits, in seconds, and a value outside it or off its step earns PAGE_BAD_VALUE. Span is the seconds of motion the row puts between its own cue and the next one, where 0 is a row that never moves a pixel and from the page is a span read off the page or off a closed word set. A data- attribute that is not in this table is PAGE_ATTR_UNKNOWN, reported before a single pixel is drawn, so a misspelling is a finding rather than an attribute nothing reads. decktalk schema page prints this same table as JSON, with each row’s on, kind, values, default, range, code, span, affects and summary, and beside it the entrance, exit, word, count, attention and slide word sets with their lengths, the query keys, the t0_signal value, measurable_span_seconds and slowest_fps. The table above and the schema are written from one registry, so the page cannot say something the registry does not.

The five rows that earn no code

An attribute lives in the contract only when its value can change a judgement or be named by a finding. Five rows can do neither, because each of them carries text a person wrote or a preview length nothing records, and each publishes the sentence that says why. The list is closed and a test counts it. An open field saying what a row affects would let every label back into a contract that exists to hold the things a measurement depends on.

How long a moment may move

MEASURABLE_SPAN_SECONDS is 0.5 seconds, and it is the one ceiling above which a cue cannot be measured. An effect still playing half a second after its cue covers the frames the next cue is read from, so it marks its own cue unmeasurable. No span a page can declare may reach it. data-in-seconds runs from 0.12 to 0.44 in steps of 0.04, which is three to eleven frames at 25 frames a second, the slowest rate a film is recorded at. data-stagger runs from 0.04 to 0.2 in the same steps, which is one to five frames. A staggered container’s total is exact arithmetic, and that is why its overrun is an error. The last child starts one step per earlier child after the cue, and then plays its own entrance.
A container with six children at data-stagger="0.08" and the default rise entrance totals 0.08 * 5 + 0.32, which is 0.72 seconds. That is over the ceiling, so it earns PAGE_STAGGER_OVERRUN. Four children at the same step total 0.56 seconds and still overrun. Four children at 0.04 with a pop entrance total 0.32 seconds and are fine. A container that staggers no children at all earns PAGE_STAGGER_EMPTY, and a data-spotlight container with no child that declares data-in earns PAGE_SPOTLIGHT_EMPTY. motion.scale multiplies every declared span and every duration in the deck. A scale above 1 slows motion down, and each scaled span is clamped one frame under the ceiling rather than obeyed, so a project cannot change a setting until its own cues stop being measurable. A motion still playing at the frame the next cue is read from is PAGE_MOTION_OVERRUN. A data-class change is the one span the contract cannot bound in advance, because its length belongs to your own stylesheet, so the runtime measures it off a laid-out element instead. Reduced motion has two switches, and neither is a flag. motion.reduce asks Chromium to report prefers-reduced-motion to the page, and motion.scale is published to the page as --dt-motion-scale on :root. A class change that still animates under a reduced render is PAGE_CLASS_NOT_REDUCED, against the stylesheet that owes the reduction. The frame reaches the page the same way. [video] size names the frame, wide, square, vertical or a width and a height in pixels, and the page is laid out on a canvas of that shape whose short side is 1080 CSS pixels. The runtime puts the canvas’s size on :root as --dt-width and --dt-height and its shape as data-dt-shape, so a stylesheet that reads them lays one page out for any frame. One project makes one video: a square cut is the project copied with its size changed.

Motion

A moment is a reveal of at most 0.46 seconds, measured as one step in the picture at its cue. A motion is the other noun. It may run for any length, it is declared on the element it moves, and its length is the seconds between the cue it departs on and the cue it arrives on. No length is ever written in the markup, so a line voiced again times the motion again, as it times every reveal.
deck/index.html
data-motion names the @keyframes the element moves through, and data-motion-from and data-motion-to are cue names local to the slide, which join the slide’s cues as a moment’s do. data-motion-ease is one of firm, glide, settle and spring, and firm is the default because it keeps a little speed into its last frame, so its arrival stops on a frame edge that verify can measure. settle and spring ease to rest, so an arrival on either is allowed only on a cue cues.json marks "verify": false, and anywhere else is MOTION_EASE_UNMEASURABLE. Three more things on a page may move for longer than a moment, and each is declared too. Things that move shows each of them in a working deck.

Undeclared motion is refused

Verify reads a reveal as a rise in how much of the frame changed, and anything else moving at the same time rises with it, so an undeclared animation makes a cue read early or makes a control span dishonest. So anything that still moves 0.46 seconds after it starts, and is not inside a declared motion, loop or region, is PAGE_MOTION_UNDECLARED. The rule is held in two places. The index page reads the computed style of every element of every slide, so check names a long or endless CSS animation the markup forgot before a frame is recorded. The recorder meets every animation as the stepped clock first sees it, so record names one a script starts. A frame hook with no region is undeclared from the call that registers it. A script can also change the picture in one step, with no animation at all, such as a setTimeout that shows an element. Verify reads that as a reveal nobody cued, or a mask hides it. So the recorder watches what the page changes between two frames, outside every cue, frame hook and animation the runtime ran, and a change that sits inside no declared motion, loop or region and shows in the frame is PAGE_CHANGE_UNDECLARED. Make the change in a cue’s handler, draw it from a frame hook with a region, or declare it as a motion. A <video> or an <audio> element is refused outright as PAGE_REALTIME_MEDIA, because it plays at its own rate and not on the stepped clock. Footage plays in a clip section, and a picture another program draws plays in a render section. On a still, which has no clock, a loop rests in its own style and a motion whose departure has fired is drawn where it ends.

The lane report

While the recorder steps a section, the runtime samples every declared motion, loop and region on each frame and keeps one lane for each. The recording log carries the lanes as recording.report.motions, and verify masks and measures from them rather than from any file kept by hand. A render section’s command writes its lanes in this same shape, so verify reads one shape whoever drew the picture. Verify says how a cue is measured with the lanes masked and how each landing is measured on its own lane.

Cue ownership

Ownership is declared and never inferred. A slide owns exactly the cues it names, which is the whole rule:
  1. Every moment attribute written on an element inside its template, qualified with the slide’s id.
  2. Every moment named on the left of a data-class pair, qualified the same way.
  3. Every cue name in the slide’s data-owns, which is for a cue that only a handler serves.
  4. Every key of a slide’s own on object, for a slide a script declares.
  5. Every cue a motion written in its markup departs or arrives on.
There is no prefix rule and no guessing, so a cue id may carry any characters an author likes. Two templates claiming one slide id would give every moment inside either of them two owners, which is PAGE_SLIDE_DOUBLED. In record mode, ownership decides when each slide mounts.
  • A slide mounts at the earliest cue time it owns.
  • The first slide to mount is on the stage before the recorder’s first step, so no frame of a recording is ever drawn on an empty stage.
  • An element that arrives on a cue due at the section’s start arrives as a cut, as the first slide does, so the section’s first frame shows it. An entrance starts on its cue’s own frame and draws nothing there, and the section’s start has no frame before it to begin on.
  • A slide that owns no cue in ?cues= never mounts, which is PAGE_SLIDE_UNUSED.
  • A cue that no slide of the playing scene owns, and that no handler serves, is PAGE_NO_OWNER.
  • The last slide to mount stays until the recording ends.
Cue mismatches lists what happens when cues.json and the page disagree.

When a cue fires

When a cue fires, the runtime adds it to window.__decktalk.fired and then does three things, in this order.
  1. Every moment on the mounted slide that names the cue plays.
  2. The mounted slide’s own on handler for that cue name runs, if it has one.
  3. Every handler that DeckTalk.on(id, fn) registered for the cue id runs, in the order they were registered.
Each handler is given the slide element and a context object of id, at, frozen and slideId, so a handler finds its own elements with slide.querySelector and needs no global of its own. A handler that throws is PAGE_HANDLER_THREW, or PAGE_SLIDE_HANDLER_THREW for a slide’s own, and the rest of the cue still runs. A cue that draws nothing and runs nothing is PAGE_CUE_UNKNOWN, because a name that drew nothing is almost always a name that drifted between the two files.

Browser preview and recording

A page holds scenes, a page section plays one scene, and the URL decides what the page is doing. It lists its scenes and slides with no query, previews a scene at ?scene=N at the speed a person reads it, plays a scene against its cue times at ?cues= in the mode every recording is made in, and freezes one slide at ?freeze=ID for storyboard and check. window.__decktalk.mode holds the word. Modes and the query keys give each mode and each key, and a section’s params may name no key outside that list. A preview and a recording differ in three places. record, storyboard and check open every page at http://project.localhost/, and answer each request under that origin from your project directory. A page may therefore fetch() a file beside it and load an ES module, neither of which works from a file:// URL, and a relative URL resolves exactly as the author wrote it. A request is answered only from the directory that holds each page and from the files decktalk.toml names, such as a clip, a music file or an effect, so the script, the cues, the build directory and the .env holding the speech key are beside the deck and are not part of it. A declared file is reached only by the spelling decktalk.toml gives it, so a request that spells it with other capitals is refused on every platform alike. A declared directory is a place, so a file inside it is served however the page spells its name. Nothing is downloaded and no port is opened. A page that loads an asset from another origin is PAGE_CDN_ASSET, because the film would depend on a host the project does not own. To open a page in your own browser the same way, run decktalk serve and follow the URL it prints.

The handshake

deadline is record.page_deadline_seconds, 15 by default, the seconds the page has to answer each call before the recorder stops it. With t0=signal in the URL the page’s section clock moves only when the recorder steps it. A browser preview has no t0=signal, so its clock starts when the page loads and plays in real time. A page that starts a clock of its own at load fires every cue early, by the whole time between load and the recorder’s first step, and every reveal in the film then comes before its word. A page that animates something itself draws it from DeckTalk.frame, which hands it the second each frame shows, and names the region it draws in, as Motion says. The recorder waits on readiness and two painted frames and on nothing else, so no fixed settle is added on top. Frame 0 is the page at second 0, and How it works explains why every frame after it is the page at its own second. The probe reads the whole page back in one call, and the recording log keeps what it answered. One report() call lists its fields. The catalog is what every static judgement is read from, rather than a scan of the markup, because data-spotlight, data-class and data-owns all declare moments no text scan can see. A page that publishes no catalog is a page whose runtime never registered a scene, and record counts that as a page error of its own.

A seamless cut

seamless = true on a [[section]] says the section opens on the picture the section before it ended on, so the cut between them should not show. It is a claim the page keeps rather than an effect: the runtime carries nothing across a cut, and a scene always starts on its own first slide. decktalk.toml refuses the key on the first section, which has no picture before it. verify compares the last frame of the outgoing section with the first frame of the incoming one, both taken outside any dip, and a changed share over verify.cut_change_max_percent is CUT_POP. check makes the same comparison from two frozen states before anything is recorded. To make a scene open on the previous scene’s last picture, rebuild that picture as the new scene’s first state.
  1. Copy the markup of the previous scene’s last slide into the new scene’s first slide.
  2. Take the moment attributes off the copies, so they are there from the first frame instead of arriving on a cue. The first slide of a played scene enters with no animation.
  3. Give the copies the same classes and the same styles, so they draw in the same place at the same size.
  4. Add the new elements with their own data-in, so they arrive on their own words.
Leave the pair out of [transition] dips, because a dip fades through black and a seam that fades cannot be seamless.

Next