The composition and the clock

This page covers the composition root and its attributes, the kinetic store that holds the clock, the channels a seek arrives by, and the controls and options that drive playback.

The clock is a store named kinetic, which markup binds as $kinetic.time. With no runtime on the page, a requestAnimationFrame loop plays the composition and loops at its duration, and Kinetic hides each clip outside its window. seek(), play(), pause() and config() drive it, and a bad value or an unknown option throws (KN-100).

The composition root

The composition is an element with data-composition-id. Each scene is a component inside it, timed by its clip attributes. A timeline here is a GSAP timeline registered on window.__timelines under the composition's id, which a renderer's runtime seeks.

<div data-composition-id="main" data-start="0" data-duration="6"
     data-width="1920" data-height="1080" data-fps="30">
    <div data-component="title" data-start="1" data-duration="3">
        <h1 data-bind-style="style">Launch</h1>
    </div>
</div>
wildflower.kinetic.scene('title', {
    keyframes: {
        style: {
            opacity:    { at: [0, 0.5, 2.5, 3], to: [0, 1, 1, 0] },
            translateY: { at: [0, 0.6], to: [40, 0], ease: 'spring' }
        }
    }
});
AttributeMeaning
data-composition-idThe composition's id. It is also the default file name of a render, and the key config({ channel: 'timeline' }) registers under.
data-start="0"The composition's start, in seconds.
data-durationThe composition's length, in seconds. A time-driven composition has no timeline to take it from, and development builds warn once the page has loaded without it, or without a composition root at all (KN-110).
data-width, data-heightThe pixel size, which render() uses by default. Kinetic gives the root this width and height before the page shows, wherever the page's CSS leaves them at auto, so the CSS need not repeat them; a size the CSS gives the root stays.
data-fpsOptional. Frames per second, 30 by default.

Until readiness, the root holds unbound markup and every clip at once. Kinetic sets data-kinetic-ready on the root once the page may be shown, to true, or to failed when readiness failed, and one CSS rule, Kinetic's cloak, holds the stage until then.

[data-composition-id]:not([data-kinetic-ready]) { visibility: hidden; }

The rule stays in the page, since Kinetic's script is deferred and the stage would flash before it runs. It is named after WildflowerJS's [data-cloak], and differs from it in two ways.

  • It lifts when Kinetic is ready: the fonts loaded, the media decoded, the effects drawn and the first seek landed. That is later than WildflowerJS's cloak, which lifts when the components initialize.
  • It hides with visibility, which keeps the layout Kinetic measures, where WildflowerJS's cloak uses display: none.

With the runtime on the page, Kinetic sets it once the runtime has published __renderReady, the flag that marks the page as ready to capture.

The clock

The clock is a store named kinetic. Markup can bind it as $kinetic.time.

FieldValue
timeComposition seconds.
frameMath.round(time * fps), and in a render with a shutter the number of the frame its samples belong to.
fpsA render's own fps, else the root's data-fps, else config({ fps }), else 30.
durationThe root's data-duration in seconds, or the runtime player's duration. 0 when unknown.
playingTrue while the clock is playing. With no runtime on the page that is the standalone clock, and with one it follows the player, checked every 200 ms.
levelsHow many times the level tables have changed, 0 on a page with no audio clip. level() and spectrum() read it, so a computed that calls them runs again when the tables change.
shutterThe shutter angle, in degrees, of the render with a shutter that is running, and 0 at any other time. smear() reads it, so a computed that calls it runs again when such a render starts and ends.
mode'interactive' or 'video', the mode in force.
step, stepIndex, stepCount, stepStart, stepLength, waiting, canAdvanceThe step fields of interactive mode.

Channels

A seek sets the clock to one time. A time reaches the store by one of three channels.

  • hf-seek, the default. The HyperFrames runtime dispatches hf-seek on every seek, in preview and in a render, with { time, waitUntil } (Rendering through HyperFrames). Kinetic applies the time and calls waitUntil with a promise that settles after the framework's flush, every promise a scene's seek() returned, the CSS-animation catch-up and any Trillium draws, so the frame is captured only once the DOM holds it.
  • Timeline, the fallback, chosen with config({ channel: 'timeline' }). Kinetic registers an object at window.__timelines[rootId] that the runtime drives through its timeline registry. The root needs a data-composition-id and has to be in the document, or the call throws (KN-100).
  • Standalone, when no runtime is on the page. A requestAnimationFrame loop starts on load, so a scene file previews in a plain tab. At the composition's duration it holds the last frame for 3 seconds, then starts again from 0. The loop stops when a seek arrives from the runtime.
    • config({ loop: { hold: seconds } }) sets how long the last frame holds, and with the preview bar up there is no hold.
    • With config({ loop: false }) it stops on the composition's last frame, duration - 1/fps, and sets playing to false, and play() on that frame starts again from 0.
    • play({ from, to }) holds the last frame before to the same way, also when a slow frame passes to.
    • With the preview bar down, a click on the composition root pauses the clock, and another click plays it again, keeping the hold of play({ from, to }). Kinetic makes the root focusable (tabindex="0") unless it has a tabindex of its own, and Space or Enter on the focused root does what a click does. A click that ends a drag, or that leaves text selected, does nothing. In an interactive lesson a click pauses a step only while it plays, and the second click plays it on to its hold; at a hold a click does nothing, since the learner's controls take it on from there. While a click has it paused, a large pause mark sits over the center of the stage, beside the composition root and outside it, so no check or render shows it; it appears and goes with no fade, and a click on it plays. A click on a control inside the root does neither: a, button, input, select, textarea, label, summary, an element with contenteditable, tabindex or data-action, or one whose role is a widget such as button or slider.

Clips outside their windows

Kinetic treats the page as having the runtime when it finds window.__player, window.__renderReady or window.__hyperframes. Without it, on every seek each clip (every element with data-start) is hidden outside its window, by the rule the runtime applies when it is present.

  • The window is half-open, a clip that runs to the composition's end keeps the final instant, and a descendant of a hidden clip is hidden with it.
  • An SVG filter that makes its own pixels (feFlood, feImage, feTurbulence) on a clip or its descendant paints through visibility: hidden, and so does a descendant with its own visibility: visible.
  • So a hidden clip also carries data-kinetic-hidden, which a style sheet Kinetic adds clips to nothing with clip-path, and nothing inside it paints. Its layout and its own inline style stay as they are.
  • Inside its window a clip gets an inline visibility: visible, which shows it even inside an element the page hides with visibility: hidden, so a footage clip shows through a parent hidden that way. Hide an element that holds clips with opacity: 0 or display: none, or give it a data-start of its own, whose window then hides what is inside it.
  • With the runtime on the page, the runtime's visibility is the only hiding, so such a filter goes on only inside its clip's window (Still effects with an SVG filter).

Controls

  • seek(seconds) moves the clock. With the runtime on the page it calls the player's renderSeek, and otherwise it sets the store and stops the standalone loop. A value that is not a finite number throws (KN-100).
  • play() and pause() drive the runtime's player, or the standalone loop. In interactive mode on a page with steps, play() plays on to the current step's hold, and at or past the hold plays the step again from its start (Interactive mode).
  • play({ from, to }) seeks to from, plays, and holds on the last frame before to, which is to - 1/fps when to falls on a frame, with playing false. from defaults to the current time and to to the composition's duration, and a to past the duration holds on the composition's last frame.
    • A to that is not after from, a value that is not a finite number, or no to on a composition with no duration throws (KN-100).
    • With the runtime on the page, Kinetic calls the player's renderSeek(from) and play(), applies the first hf-seek at or past the held frame as that frame, then pauses the player and seeks it there. The runtime's own clip visibility follows the player's time for that one frame.
  • config(options) sets options and returns the settings in force. With no argument it only returns them. An option not in the table below throws (KN-100), and nothing is applied.

The config options

OptionDefaultMeaning
fps30Frames per second when neither a render nor the root's data-fps sets one. A number above 0.
channel'event''event' listens for hf-seek. 'timeline' registers the timeline fallback.
autoplaytrueWhether the standalone clock starts on load when no runtime is on the page.
previewfalseThe preview bar. true when ?preview or data-preview is present.
looptrueWhether the standalone clock starts again from 0 at the end. true holds the last frame 3 seconds first, { hold: seconds } sets those seconds, and false holds the last frame and stops. There is no hold while the preview bar is up, and config() reports the option as { hold }, or false. With the runtime on the page, its player runs playback and this option has no effect.
fitas previewWhether the stage is scaled down to fit the window, following preview until it is set on its own. The root goes into a plain <div class="kinetic-fit"> (no component) as wide as data-width and no wider than its container, scaled from its top left corner to the wrapper's width, never above 1, so its layout stays data-width by data-height. It is never applied during a render, where render() draws at the composition's own size.
soundtrueWhether Kinetic plays the audio clips in the preview, and false plays nothing and takes the bar's speaker button away. The render and the levels are unaffected.
shutterfalseThe shutter every render() uses when the call gives none, the preview bar's Render button included, as an angle in degrees or { angle, samples }, and 0, false or null turns it off. config() reports it as { angle, samples }, or false.
finishfalseThe finish every render() uses when the call gives none, the preview bar's Render button included, and the page shows where the browser can carry it (The finish in the page). 0, false or null turns it off, and config() reports it as its fields, each an object of all its named parts such as { grain: { amount, size, fps } }, which config({ finish }) takes back, or false.
mode'auto''auto', 'interactive' or 'video', the mode setting. Under 'auto' the page is in video mode with the preview bar up and in interactive mode otherwise.
// Play one step of a lesson and hold its last frame, 8.9667 s at 30 fps.
wildflower.kinetic.play({ from: 0, to: 9 });
// Hold the last frame 1 s before starting again.
wildflower.kinetic.config({ loop: { hold: 1 } });
// Hold the composition's last frame and stop.
wildflower.kinetic.config({ loop: false });