deck/index.html
decktalk-runtime.js is that script. It reads the markup, mounts the slides of a scene, and makes
each element arrive on the second its cue was given.
Where a page gets it
DeckTalk opens every page at a local origin, and the engine answers every path under/__decktalk/
on that origin itself, from the installed package. Four things live there:
record, check, storyboard, cue when a section’s recording no longer stands, the poster and
the captioned copy’s captions assemble draws, and decktalk serve all open the page
at that origin, so every one of them plays the runtime of the engine that is running, and a project
holds no copy that could fall behind it. A build needs no network to draw a page.
The path is a closed list. A name under /__decktalk/ that is not one of those files is refused, the
probe a command injects is never on it, and no file of the project is ever served under it, so
nothing an author writes can shadow the engine’s files.
A page opened from a file:// URL resolves /__decktalk/ against the root of the disk and finds
nothing there, so it draws no slide. Open it with decktalk serve
instead.
The runtime is written in TypeScript under src/decktalk/runtime/src/ and compiled by esbuild into
one bundle. The page contract it is written against lives in exactly one file, and Python reads that
same contract, so what an author may write is one list rather than two that can disagree.
The modules
Thirteen modules make the runtime. Each owns one thing, and nothing imports the facade.
A fourteenth file,
src/decktalk/runtime/src/probe/probe.ts, is a bundle of its own. It imports the
contract and the telemetry seam and nothing else, which is what keeps the runtime free of everything
only a command needs.
contract.ts is the one home
Every attribute an author may write lives in contract.ts with its values, its safe range, its
default, the code that is raised when it is wrong, and the seconds of motion it puts between its cue
and the next one. Every other runtime module reads its attribute names from there rather than
spelling a data- literal of its own.
Two rules decide what may live there. An attribute survives only when its value can change a
judgement or be named by a finding, and the five rows that do neither are listed in EXEMPT with
the sentence that says why. No span a page can declare may reach MEASURABLE_SPAN_SECONDS, because
an effect still moving half a second after its cue makes its own cue unmeasurable.
src/decktalk/page.py is generated from contract.ts, so Python and the page cannot disagree about
what an attribute means. It is vocabulary and arithmetic and nothing else: it opens no file, reads
no settings and imports only the finding codes from the rest of DeckTalk, because each attribute
names the code that judges it. So every layer above it can read the contract without a browser and
without a project.
Building the bundle
contract.ts as a CommonJS
module, node prints CONTRACT as JSON, and every artifact is written from that JSON, so no regular
expression ever reads TypeScript. Each artifact is written through the formatter that owns its
language, the pinned Biome for JavaScript and the project’s ruff for Python, so --check compares
two files that were made the same way.
Four artifacts are committed and held to their source by --check.
A module reaches a bundle only by being imported from one of the two entry points, which is what
keeps the probe out of the runtime and the runtime out of the probe.
What a page declares
A[data-scene] element declares a scene, and every <template data-slide> inside it declares one
slide, in document order. The wrapper never draws, and the runtime clones a template’s content each
time its slide mounts.
An element has four moments, each naming a cue local to its slide. It arrives, it steps back, it
comes to the front, and it leaves. The runtime qualifies a cue name with its slide into the
cue id cues.json carries.
deck/index.html
data-in="formula" inside <template data-slide="2.1"> is the cue id 2.1:formula, and that is
what cues.json gives a second to. No attribute says when a cue fires, because cues.json owns
that and the page owns what a thing looks like and what it means.
decktalk schema page publishes every attribute with its values, its default, its range, its code
and its motion span. Read that and write from it.
Three numbers bound every span a page may declare.
How long a moment may move gives the range of
each attribute in those frames, and the stagger’s arithmetic.
A motion is not bounded by them, because its length is the seconds between its two cues. Two
constants say how it moves.
A motion in markup plays its
@keyframes with the CSS timing function of its ease, and a script
motion is handed the same curve’s progress, so the two arrive alike. The spring reaches CSS as a
linear() curve through 33 points.
The query keys
Every key a DeckTalk page reads is published by the contract, and each is read once, inmodes.ts,
so no other module ever touches the URL.
after and before are read by the probe alone, so a page opened by hand at ?freeze=2.1 shows
every reveal and ignores both.
Modes
The URL picks one, andwindow.__decktalk.mode reports it.
caption wins over every other key, and freeze wins over scene and cues. With cues and no scene, the scene that owns the earliest
cue plays. speed divides preview time and its floor is 0.05. With t0=signal the clock waits for the
recorder, which moves it one frame at a time through DeckTalk.stepTo(t).
The DeckTalk object
A scene is markup, and a page that needs no behaviour needs no JavaScript at all. A page that wants
behaviour of its own drives this object.
deck/index.html
waitFor never fails the page. One that rejects raises PAGE_WAIT_REJECTED and
one that has not settled after five seconds raises PAGE_WAIT_UNSETTLED, and the page is drawn
either way, because a deck that stopped at its first broken promise would cost a recording rather
than save one.
window.__decktalk is the live view whoever is reading the page back uses. Every field is a getter,
so a reader sees the page as it stands: version, ready, mode, scene, slide, cues,
fired, warnings, catalog, motions, frozen and signalled. now() and started() are the clock’s own
two functions, which say the second on the section clock and whether the clock has begun.
undeclared(nodes) answers { what, box } for those of some nodes the page changed that sit inside
nothing declared as moving: what names the first, and box is where all of them are laid out, in
viewport pixels, or null when one is laid out nowhere. It answers null when every node is declared,
which is how the probe asks about a change it saw between two steps.
caption({ lines, voice, turn, spoken }) draws one caption in the band of a page opened with
?caption=1, or none when it is handed null, and answers once the caption’s fonts have loaded and
two frames have painted. spoken is the first line the voice speaks and the line after its last,
and every other line is a sound, which takes data-sound.
The probe
decktalk-probe.js is the instrumentation every command that opens a page needs and no deck
carries: a step that moves the section clock to one recorded frame, a helper that says when the
page has settled, a measured catalog that says where every reveal sits, a freeze that stops
at one cue, and a record of how the page behaved while it was captured.
Playwright adds it with an init script, so it runs before the page’s own scripts and no page ever
references it. It is never a <script> tag, and decktalk init never writes it. A page opened
without it plays, previews, freezes and lists its scenes exactly the same, and keeps nothing. Once
it is on the window it is frozen and made read-only, so a deck cannot take its name.
The recorder holds it as window.__decktalkProbe.
Three more are read by the runtime, and only when the probe is there:
recorder is the telemetry
sink, read once at startup, freezeCues(order, slide, warn) narrows a frozen slide’s cues to the
stop the URL names, and measure(catalog, canvas) adds one measured box per element onto the
catalog.
One report() call
record and check read what a page says about itself through this one call rather than through a
handful of evaluations, so what a command knows about a page is one snapshot taken at one moment.
Its fields are exactly page.REPORT.
The telemetry the probe keeps is what only the runtime can describe. A cue row carries the second it
was due, the second it ran, the second of the frame that showed it and the sentence its reveals
describe themselves as, leaving out any sentence an earlier cue of the same slide already wrote. A word row carries the line, the second its cue ran, the second
the voice reaches it and the second its first word was drawn, which the debug log prints. Each list is capped at five thousand rows, so a long
recording cannot grow without a bound.
A deck in a browser tab pays nothing for any of this. The runtime reports through the seam in
telemetry.ts, and a page opened without the probe keeps the no-op default and allocates nothing.
Python reads the answer once, at the boundary where the page stops being trusted. Every row is
validated into a model before anything above uses it, and a row that cannot be read is dropped with
the reason rather than carried into an artifact.
The codes the page raises
The page never throws at its author. It records what it could not honour, echoes the sentence to the console with adecktalk: prefix, and carries on drawing. A row is recorded once per code, slide,
cue and attribute, so a repeat adds nothing.
code, message, slide, cue and attr. The sentence belongs
to the contract and never to the module that raises it, so a code’s wording changes in one file and
reaches the console, the finding and this documentation at once.
record and check read those rows and turn each into a finding under the same code, with the
slide or the cue as its location and the page as its file. Python refuses a code the page has no
business raising, because the page codes measured from the frames or from the catalog after the run
belong to Python, and a page that reported one of those would be a deck judging itself.
Each has its own page under
/reference/findings/, and code.url on the Python member points at it.
Reduced motion
Reduced motion is a render rather than an attribute, so the same page produces both films and an author writes neither twice.motion.reduce asks Chromium for a page that prefers reduced motion, and the runtime reads
prefers-reduced-motion: reduce through matchMedia. A person who has asked their own machine for
it gets the same render in a preview. When it is on, the runtime adds the class dt-reduced to the
root element, which the page’s own stylesheet must honour. A class change that declares no reduced
form raises PAGE_CLASS_NOT_REDUCED, because a reduced render would otherwise animate anyway.
motion.scale reaches the page as one declaration on :root, which the recorder adds and an author
may write for themselves.
deck/style.css
motion.scale runs
from 0.25 to 4.0, and a span it pushes past the ceiling raises PAGE_MOTION_OVERRUN.
Neither is a flag. Both are settings keys, so --set motion.reduce=true and
--set motion.scale=1.5 are how a run asks for them.
CSS hooks
The runtime prepends one generated stylesheet as#dt-style, before anything the page declares.
Every selector in it sits inside :where(), which gives it no specificity at all, so a page rule of
equal weight wins on document order and an author never reaches for !important.
A deck is drawn into a canvas of the project’s frame shape with a short side of 1080 CSS pixels, 1920
by 1080 for [video] size = "wide", scaled and centred in whatever window the page is opened in, so
an element measured on a laptop is at the pixel a recording will put it at. The recorder tells a page
its canvas before any script of it runs, and a page previewed in a browser asks its origin for
/__decktalk/canvas.json and lays itself out again on the answer.
The classes the runtime puts on what it draws are
dt-slide, dt-leaving, dt-arriving,
dt-hidden, dt-shown, dt-back, dt-front, dt-word, dt-frozen, dt-reduced, dt-captioning,
dt-caption and dt-caption-line.
Related
- Learn how a page, a cue file and a script fit together: Page contract
- Look up a settings key, its range and its hazard: Configuration
- Look up the commands that open a page: CLI
- Look up the files a run writes: Build artifacts