Scenes

scene(name, definition) registers a component that reads the clock, with computed properties for its clip and the edges, keyframes, pools, seek(local), waitFor, inputs and root: options.

A scene's timing comes from its clip attributes, data-start with data-duration or data-end. A scene's tick(dt) runs only in interactive mode, so the video never sees its changes. A pool in a scene's markup is static, so write its entities in seek(local) with update().

Scenes

scene(name, definition) registers a component with wildflower.component, so name is its data-component. The definition is a component definition plus edges, keyframes, follow, inputs, waitFor and seek(local). Kinetic subscribes the component to the kinetic store and adds these computed properties.

ComputedValue
timeComposition seconds.
localSeconds since the clip started, which is time minus data-start (with the host's offset), plus data-playback-start on a nested composition. Held at the clip's edges: 0 before the clip starts and the clip's duration after it ends (A scene outside its clip).
clipFrameMath.round(local * fps), the frame number within the clip, and in a render with a shutter the frame its samples belong to.
fpsThe clock's frames per second: a render's own, else the root's data-fps, else config({ fps }), else 30. A frame count in seconds is n / this.fps.
progress0 to 1 from the clip's start to its last captured frame, duration - 1/fps. 0 with no duration, and 1 throughout a clip one frame long or less.
remainingSeconds until the clip ends, or Infinity with no end.
enter0 to 1 over the first edges.enter seconds. 1 without edges.
exit0 to 1 over the last edges.exit seconds, ending on the last captured frame. 0 without edges.
presenceenter * (1 - exit), 0 at both ends of the clip and 1 between. 1 without edges.
varsThe composition variables: the data-composition-variables defaults with window.__hfVariables over them, which the batch script sets per row, or the values a renderer's runtime resolved.
inputsThe scene's inputs, read from vars, coerced and checked before init().

The scene reads these values over its own computed or state field of the same name, and development builds warn (KN-152).

A render captures frames 0 to duration * fps - 1, so the last captured frame is duration - 1/fps after the clip's start. progress and exit reach 1 there, so a fade out from presence completes in the file.

A scene's tick(dt), the frame-loop hook (dt in milliseconds), runs only in interactive mode, never in video mode (check(), render() or a renderer's runtime on the page). Use it for interactive work such as physics while a step holds. The video never sees its changes, so development builds name such a scene once when it is part of a render(), or the page is in video mode (KN-151); check() makes no video, so it says nothing. An interactive page that uses tick() gets no warning.

Timing

A scene's timing comes from its clip attributes.

  • data-start="2" starts the clip 2 seconds after the host composition starts.
  • data-start="intro" starts it after the clip with that id or data-composition-id ends, which chains scenes one after another. "intro + 0.5" starts half a second later, and "intro - 0.5" overlaps by half a second, which data-clip-transition can make a transition. "intro.end" is the same as "intro". A reference that matches nothing starts the clip at 0 and warns in development (KN-113).
  • data-start="voice.start" starts it when the clip voice starts (Starting with another clip).
  • data-duration or data-end sets the clip's length. With neither, progress, remaining and exit run to the composition's end, when it has a duration, and local has no upper hold.
  • A composition root with no data-start starts with its host.
  • data-playback-start="4" on a nested composition shows its own 4 seconds first.
  • data-hidden, a HyperFrames attribute, takes a clip out with display: none outside its window, so it takes no layout space there; a clip without it is hidden with visibility and keeps its space.

An element with no data-start is not a clip, and a scene on it takes the timing of the nearest ancestor with data-start or data-composition-id. With none, local equals time and development builds warn (KN-112).

Editing data-start, data-duration, data-end, data-media-start, data-playback-start or data-playback-rate under the root moves that clip, and the clips and scenes timed from it, at the next seek. A seek to the time already shown does not recompute local.

A clip edge between two frames takes effect in a render on the later frame. Development builds warn once per clip (KN-114) when its resolved data-start, data-end, or data-start plus data-duration, times the fps, is not a whole number. The message gives the attribute, its value and the two nearest frame times. Write an edge that is no round number as a frame time, n / fps to 8 decimals: frame 331 at 30 fps is 11.03333333. To put an edge on a beat, round the beat's time to its frame.

A scene outside its clip

A scene outside its clip is hidden: its clip, or the nearest clip around it, paints nothing there. So local holds at the clip's edges, 0 before the clip and its duration after it, and a hidden scene costs nothing on a seek.

  • Its computeds stop running, since local stops changing.
  • Its seek(local) runs once at each edge and then not again until the clip comes round: a seek that leaves a scene's local as it was at that scene's last seek skips it.
  • A Trillium view inside a hidden clip is not drawn, and is drawn on the first seek that shows the clip.
  • A change to the scene's state in interactive mode, a change of mode or of fps, and every seek of check() run seek(local) whatever its local, so check() still compares what it draws on every seek.

A page of many scenes on consecutive clips therefore pays on each seek for the scenes on screen only.

Starting with another clip

A clip name in data-start means after that clip ends. data-start="voice.start" starts the clip when the clip voice starts, and "voice.start + 0.5" and "voice.start - 0.5" start it half a second after or before. Subtitles over a voice-over start with the voice, so moving the voice moves them too.

<audio id="voice" src="voice.mp3" data-start="4" data-duration="12"></audio>

<div data-component="subtitles" data-start="voice.start" data-duration="12">
    <p data-bind="line.text"></p>
</div>

An element whose id or data-composition-id is the whole value, such as id="voice.start", is read first, as HyperFrames reads it. Only when there is none is .start or .end read as an edge of the clip before it.

The .start form is Kinetic's own. HyperFrames' renderer does not read it, so a page that uses it renders through Kinetic only.

The HyperFrames-compatible way to keep two clips together is a group, a nested composition with both clips at data-start="0" inside it. Moving the group moves both.

<div data-composition-id="spoken" data-start="4" data-duration="12">
    <audio id="voice" src="voice.mp3" data-start="0" data-duration="12"></audio>
    <div data-component="subtitles" data-start="0" data-duration="12">
        <p data-bind="line.text"></p>
    </div>
</div>

Edges

edges: { enter: 0.5, exit: 0.5 } sets the seconds that enter ramps over from the clip's start and exit ramps over up to its last captured frame.

wildflower.kinetic.scene('card', {
    edges: { enter: 0.5, exit: 0.5 },
    computed: {
        card() { return { opacity: String(this.presence) }; }
    }
});
// presence fades the card in over its first 0.5 s and out over its last 0.5 s
A 3 second clip from 0.5 s whose card has opacity presence, with meters for enter, exit and presence as the two edges change; click the stage to pause.

Each is seconds, 0 or more, and anything else throws (KN-100).

A clip that only fades in and out needs no edges: data-clip-enter and data-clip-exit do it in the markup, and presence is for a scene whose own code needs the value, such as to stagger its children by it (Which to use).

Keyframes

keyframes declares tracks as data, and each entry becomes a computed property of local, a style object for an object of tracks and one value for one track. The track format is under Motion.

wildflower.kinetic.scene('title', {
    keyframes: {
        heading: {
            opacity:    { at: [0, 0.5, 2.5, 3], to: [0, 1, 1, 0] },
            translateY: { at: [0, 0.6], to: [40, 0], ease: 'spring' }
        },
        sweep: { at: [0, 2], to: [0, 270], ease: 'power2.out' }
    }
});
// this.heading is a style object, and this.sweep is one number
The heading fades in, springs up and fades out by its two tracks, and the corner shows sweep as one number; click the stage to pause.

Tracks are checked when the scene is defined.

  • A bad keyframe throws (KN-120).
  • Development builds warn about a length property such as width given numbers with no unit (KN-122), and a spring the next keyframe cuts off more than 2% short of rest (KN-123).
  • A keyframes entry that shares a name with a computed entry is the one the scene reads, and development builds warn (KN-153).

An entry with several tracks folds its transform keys into transform, so this.heading.translateY is undefined, and development builds warn when code reads it (KN-159). For the number, give translateY its own entry, which is then one number, or call interpolate() with its track.

Pools

Write a pool's entities in seek(local) with update(key, props), where key is the entity's value of the field the container's data-key names. In a data-bind-attr object an attribute keeps its own name, so an SVG attribute with a hyphen is a quoted key, such as 'stroke-width'. Every seek flushes the page's pools with wildflower.flushPools() once its scenes have settled, so the DOM holds the frame before it is captured, with no animation frame needed.

wildflower.kinetic.scene('dots', {
    pools: { dots: {} },
    init() {
        for (let i = 0; i < 18; i++) this.pools.dots.add({ id: i, x: i * 30, style: {} });
    },
    seek(local) {
        this.pools.dots.forEach((d) => {
            this.pools.dots.update(d.id, { style: { transform: 'translate(' + d.x + 'px, ' + (local * 50).toFixed(1) + 'px)' } });
        });
    }
});
// markup: <div data-pool="dots" data-key="id"><template><i data-bind-style="style"></i></template></div>
Eighteen dots written with update() in seek(local) fall 50 px a second; click the stage to pause.

A pool that mounts after the scene initializes, or sits inside another component in the scene, is flushed by the seek the same way.

SVG shapes that come and go are a pool, because a pool's template works inside an <svg> and a data-list there renders nothing. The HTML parser drops a <template> inside <svg> before any script runs (WF-401 in development). Keep the rest of the frame in computeds, binding a fixed set of shapes directly and a changing outline as one <path> whose d is a computed.

seek(local)

An optional seek(local) runs with the clip-local time after every seek and once at init, except a seek that leaves local as it was (A scene outside its clip). It runs before the frame's bindings are written, so computeds see what it set, such as an aimed camera (Trillium).

wildflower.kinetic.scene('orbit', {
    seek(local) {
        // camera is the THREE camera the scene's view draws through
        camera.position.set(Math.cos(local) * 6, 2, Math.sin(local) * 6);
        camera.lookAt(0, 0, 0);
    }
});
  • A promise it returns holds the frame capture until it settles.
  • An exception it throws goes to the component's onError.

A render seeks to whole frames only, and this.clipFrame counts them from the clip's start. A landing on a whole frame has the pattern for an impact that falls between two frames.

waitFor

waitFor: ['sales'] lists the queries a scene reads, and frame 0 is not captured until each has its first rows.

Queries exist on the full tier only.

wildflower.query('sales', { from: 'data/sales.json', select: (data) => data.rows });

wildflower.kinetic.scene('sales-chart', {
    waitFor: ['sales'],
    computed: {
        rows() { return wildflower.getQuery('sales').rows; }
    }
});
  • On a tier without queries, development builds warn (KN-101).
  • A query that fails is a KN-131 error.
  • A name that matches no query is a KN-133 error.
  • A query that goes back to loading after frame 0 is warned about in development (KN-132).

The same codes are under Readiness.

inputs

inputs declares what a scene needs from its composition variables, typed like props. A bare constructor is shorthand for { type }.

wildflower.kinetic.scene('promo', {
    inputs: {
        name:   { type: String, required: true },
        price:  { type: Number, validator: (v) => v > 0 },
        accent: { type: String, default: '#c8452d' },
        sale:   Boolean
    }
});
// this.inputs.price is a number, checked before init() runs
  • A missing, null or empty-string variable takes the default. Without one it is an error when required is set, and undefined otherwise.
  • Number accepts a number or a numeric string, and anything else is an error.
  • Boolean takes true, 'true', 1 and '1' as true, and false, 'false', 0 and '0' as false.
  • String converts any other value with String().
  • validator returns false, or throws, to reject the value.

A failed input is a [KN-130] console error in every build, with the scene, the input and, in development, the problem with the value. Readiness checks every live scene before frame 0, so ready(), check() and render() reject while the scene is on the page.

A default in the root's data-composition-variables wins over the input's own default, which applies only to an undeclared variable, and development builds warn when the two differ (KN-134). Kinetic reads the declared defaults as the runtime does, so a plain tab shows what a render does.

Inputs are composition-wide, so every clip of one scene reads the same values; One scene, several clips has the per-clip forms.

The scene's own root

The root: block styles the scene's root element from the scene's own scope, wherever the scene sits. It also takes class, attr, show and directives (Bindings on a component's root).

wildflower.kinetic.scene('card', {
    root: { style: { background: 'bg' } },
    computed: {
        bg() { return wildflower.kinetic.interpolate(this.local, [0, 2], ['#102040', '#c8452d']); }
    }
});
The scene's root takes its background from bg, navy to red over 2 seconds; click the stage to pause.

A binding written on the root tag is read in the scope that contains the tag, as for every component.

  • Outside a component of your own, that is the scene's own scope. The wrapper Kinetic puts around a fitted stage is no component.
  • A scene is inside a component of your own only when you put the composition in one, or one scene inside another. The binding then reads that component, where the scene's values are not in scope. A development build of WildflowerJS warns when it uses a name only the scene has (WF-521), and Kinetic's development build warns when a scene inside another scene uses one of Kinetic's own values, such as progress, which the outer scene has too (KN-154).