The Browser Runtime¶
The HTML presentation is a single self-contained file, and its script is the browser runtime of Animo. This page describes how that runtime is built: the elements and attributes it finds on the page, the files it is written in, and the controller that turns input into steps and announces them as events. The Architecture section of the design document specifies what the runtime does in a step, and why. See Where the Specification Lives for where to find the design document.
The Page¶
The page nests three kinds of element, as .animo-deck > .animo-stage > .animo-slide.
The deck element has one child, the stage element,
and every slide container is a child of the stage element.
A slide container holds up to three elements in this order:
.animo-background, .animo-canvas and .animo-overlay.
The canvas is always present, and holds one html.frame that contains the whole body of the slide.
A background or an overlay made of content is a frame inside
.animo-background or .animo-overlay.
A color background is a declaration on the slide container,
and a color overlay is a declaration on the .animo-overlay element.
The runtime reads the attributes below. When a step does not do what the timeline says, inspect these attributes in the developer tools of the browser.
- Every slide container carries
data-animo-slide, which holds the position of the slide in the deck, anddata-animo-states, which holds the number of states of the slide. - Every slide container also carries
data-animo-plan, which holds the resolved display state of every state of that slide as JSON. The browser receives no other information about the timeline. When a step goes wrong, this attribute therefore shows whether typst resolved a wrong plan or the runtime applied a correct plan wrongly. - Every slide container carries
data-animo-transition, which names the transition at the slide boundary that leads into this slide. When that transition has parameters,data-animo-transition-argsholds them. - The deck element carries
data-animo-config, which holds the settings of the deck as JSON. The runtime reads this attribute once, at load. The settingtransitionis the transition of every slide whoseinitnames no transition. - The root element carries
lang, and the head carries a<title>. They come fromset text(lang: ..)andset document(title: ..), in the same way as in a page whose head typst builds.
The runtime writes the attributes below while it shows the deck.
data-animoon the root element holds the position that the runtime has reached, in the same form as the fragment of the URL.data-animo-modeon the root element holds the name of the active mode.data-animo-pausedis present on the root element while the clock of the deck is stopped.data-animo-currentmarks the slide that is shown.data-animo-leavingandinertmark the slide that a slide transition is leaving.
The Files of the Runtime¶
Because the HTML output is a single self-contained file,
its script cannot import modules from other files.
The runtime is written as the files under src/js.
src/deck.typ reads these files in a fixed order
and joins them into the single <script type="module"> of the page.
The files therefore share one module scope.
Function declarations are hoisted,
so a function in one file can call a function declared in any other file.
A top-level const is initialised only when the script reaches the file that declares it,
so code that runs at load time can use a const only from its own file or from an earlier file.
boot.js is the last file, and the only file that calls into the other files at load time.
| File | Holds |
|---|---|
slides.js |
config, readSlide, the registry of slides, count, clamp, parseHash |
stacks.js |
the kinds of stack, readStacks, planStacks and the subslide stack |
effects.js |
timing, scheduled, span, showing, plan and apply |
display.js |
positions and anchors, the CSS of a display state and a pan, planState |
boundaries.js |
the transitions of both boundaries, planBoundary and planEpoch |
paths.js |
alignPaths, which rewrites two paths into one structure |
morph.js |
the matches of a morph, their routes, and settleMorphs |
controller.js |
the position, show, step, jump, and the clock with its pause |
input.js |
key, pointer and hash events, turned into intents by the active mode |
boot.js |
preparing the page, reading the slides and the first jump |
Each part of the design is implemented in one place, as listed below.
- Stacks.
src/stack.typbuilds the epoch stacks and the subslide stacks, andsrc/region.typandsrc/tag.typplace an epoch stack when their view asks for one. In the browser,readStacksfinds every stack of a slide by the labels of its renderings.stackKindsdefines, for each kind of stack, how the runtime plans that stack for a state. - The two phases of a step.
planineffects.jsrecords one effect per element and property, and writes nothing to the page.applythen writes the styles of the step and creates its animations.PROPERTIESlists the properties whose unset value the browser reports as a keyword, such asnonefortranslate, together with the value that this keyword stands for. - Epoch boundaries.
planBoundaryfinds the stacks that a step carries. It gives each of these stacks the record of the first tag that the boundary changes in it, together withnames, the list of every tag that the step changes in the stack.differscompares the renderings of a stack on the two sides of a boundary.planEpochthen plans every epoch stack. It hands each carried stack to the entry oftransitionsthat the record of the stack names, or tocrossfadewhen the record names no transition. - The hand-over.
handOverplans how every transition hands a region from one rendering to the other. The rendering that comes first in the document is drawn below the other and stays opaque, and the rendering above fades in or out. A transition giveshandOverthe pairs of elements that draw the same ink at one place:inPlacefinds them for a crossfade, by comparing attributes, and the morph has its matches.handOverhides the copy above of every identical pair, so the shared ink is drawn once, and fades the ink of the rendering below that has no partner.gatheredhides or fades a group as one when all its children are hidden or faded.settleHandOverstakes what an earlier hand-over hid or faded back to rest at every step. No rendering blends and no stack isolates, because firefox draws every frame of a transition several times more slowly then (see Performance). - Slide boundaries.
slideTransitionsholds one entry per slide transition. Each entry gives the display state of the slide that owns the transition and of the other slide, at the start and at the end of the transition.slideTransitionsis a separate table fromtransitions, because a slide transition receives two slide containers rather than the renderings of a region. - The morph.
morphStackinmorph.jsplans the routes of the matches of a morph, and hands them tohandOver.commonSubsequencepairs the drawn elements (the ink) of the two renderings, using the keys thatinkKeyandclipsAbovecompute.commonSubsequencegives up when the renderings have more thanMORPH_DIFFERENCESdifferences.resizesfinds the paths that a morph resizes, with the help ofpathStructure,labelsAboveandsameFrame, andshapeMorphsfinds the shapes that a morph turns into another shape. Both kinds of morph run only in an engine that animatesd, whichRESIZESrecords.alignPathsinpaths.jsreads nothing from the DOM, so its tests work on numbers only.slide.morphedholds every element that a morph is still moving. At every step,settleMorphsreadsslide.morphedbefore any transition is planned. A copy thathandOverhides takes no route, andslide.standInsrecords the copy below that shows it, whichdisplayedreads when a later step starts a route from it. - Shared definitions.
hoistPaintServersinboot.jscopies the SVG definitions that several slides share, such as gradients and patterns, into one<svg>before the first slide is read.
The Controller and Its Events¶
controller.js holds every piece of state that the runtime keeps between two inputs:
the position, the direction of travel, the pending step, the reasons the clock is stopped,
and the active mode.
show is the only function that writes the position.
The controller announces each change as an event on the root element.
It dispatches the event only after it has updated the DOM, the fragment,
the data-animo attribute and the clock,
so a listener finds the page in the state that the event describes.
The events bubble, so a listener on the root element, on the document or on the window
receives them.
| Event | Detail | Dispatched |
|---|---|---|
animo:leave |
{slide} |
when the new position is on another slide; slide is the previous slide |
animo:enter |
{slide} |
in the same case, and for the first position; slide is the slide entered |
animo:position |
{from, to, animated} |
after every position that is shown |
animo:mode |
{from, to} |
when the active mode changes; from and to are the names of the modes |
A position is {slide, state}.
For a change of slide, the events come in the order leave, enter, position.
For the first position of the page, the controller dispatches animo:enter and
animo:position without an animo:leave, and from is null.
The controller also announces the mode that the page starts in, with a from of null.
animated is true when a step reached the position and the deck lets its motion run.
animated is false for a deep link, for Home and End, for a cut,
and for a viewer who asked for reduced motion.
A join (a gap of zero) consists of two steps that the clock makes,
and the controller announces each of them.
The runtime reads input according to the active mode, an object {name, keymap, pointer}.
keymap maps event.key to an intent,
and pointer maps the type of a pointer event to an intent.
The intents are next, previous, first, last and toggle-pause,
and the controller implements each intent as one entry of its intents table.
The only mode is present.
In present, Space means toggle-pause while the deck has a clock to pause,
and next otherwise.
The clock of the deck is stopped while at least one hold reason is held.
The pause key holds the reason user.
Backward travel that comes to rest at the first state of the deck holds the reason travel,
and the next forward step releases it.
When either reason is held, the pause key releases both.
When neither reason is held, the pause key holds user.
The runtime confines its effects to the deck in the following ways.
- Only the current slide receives pointer events.
The slide that a slide transition is leaving stays laid out,
so that a step back finds it laid out,
and that slide is
inertfor as long as the transition leaves it. - A key press or a click whose target is inside an element with the attribute
data-animo-controldoes not step the deck. - The pause key pauses and resumes only the animations that the runtime created,
which carry the id
animo. Every other animation on the page keeps running.