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.
| Computed | Value |
|---|---|
time | Composition seconds. |
local | Seconds 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). |
clipFrame | Math.round(local * fps), the frame number within the clip, and in a render with a shutter the frame its samples belong to. |
fps | The 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. |
progress | 0 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. |
remaining | Seconds until the clip ends, or Infinity with no end. |
enter | 0 to 1 over the first edges.enter seconds.
1 without edges. |
exit | 0 to 1 over the last edges.exit seconds, ending on the last captured frame.
0 without edges. |
presence | enter * (1 - exit), 0 at both ends of the clip and 1 between.
1 without edges. |
vars | The 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. |
inputs | The 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 thatidordata-composition-idends, which chains scenes one after another."intro + 0.5"starts half a second later, and"intro - 0.5"overlaps by half a second, whichdata-clip-transitioncan 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 clipvoicestarts (Starting with another clip).data-durationordata-endsets the clip's length. With neither,progress,remainingandexitrun to the composition's end, when it has a duration, andlocalhas no upper hold.- A composition root with no
data-startstarts 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 withdisplay: noneoutside its window, so it takes no layout space there; a clip without it is hidden withvisibilityand 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
localstops changing. - Its
seek(local)runs once at each edge and then not again until the clip comes round: a seek that leaves a scene'slocalas 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()runseek(local)whatever itslocal, socheck()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
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
Tracks are checked when the scene is defined.
- A bad keyframe throws (KN-120).
- Development builds warn about a length property such as
widthgiven numbers with nounit(KN-122), and a spring the next keyframe cuts off more than 2% short of rest (KN-123). - A
keyframesentry that shares a name with acomputedentry 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>
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,
nullor empty-string variable takes thedefault. Without one it is an error whenrequiredis set, andundefinedotherwise. Numberaccepts a number or a numeric string, and anything else is an error.Booleantakestrue,'true',1and'1'as true, andfalse,'false',0and'0'as false.Stringconverts any other value withString().validatorreturnsfalse, 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']); }
}
});
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).