Which to use
Blur, grain, music, and a clip's fade each have more than one route in Kinetic. This page starts from what you want, gives the route for it, and says which route is the default where there is one.
Each table links to the route's own section, which has its options and errors. The examples put the routes side by side, so the difference between them is on the screen.
Blur
Kinetic blurs six different things, so start from what should be soft: one element, a part of the frame, a cut between two clips, whatever is away from the focal plane, single letters, or the whole frame.
| What you want | The route |
|---|---|
| An element that blurs in or out | A blur track in keyframes, in px: blur: { at: [0, 0.6], to: [12, 0] } (A layer with keyframes). |
| A part of the frame blurred, such as tilt-shift focus or a soft round patch | blur(element, { scene: this, radius, marks }) on an empty element over it (Blur where the marks paint). |
| A cut from one clip to the next through a blur | data-clip-transition="blur" on the incoming clip, or blur with a length in data-clip-enter and data-clip-exit (The kinds). |
| Depth of field: the farther from the focal plane, the softer | focus(z, plane, { range, max }), which returns the px for a filter: blur() (focus()). |
| Single letters of a title, each blurred by its own amount | text()'s per-letter blur, in px, from letter(i, ch) (Lines and letters). |
| The whole frame a little soft, more toward its edges | The finish's soften: config({ finish: { soften: 4 } }) (The finish). |
Blur from movement is motion blur, which a render's shutter gives, with smear() for the preview (Motion blur).
The per-letter blur draws from a font's baked outlines, so its example is on Type with a weight per letter.
<div id="stage" data-composition-id="blurs" data-start="0" data-duration="6" data-width="1280" data-height="720" data-fps="30">
<p class="label a">An element<code>a blur track</code></p>
<p class="label b">A region<code>blur(el, { marks })</code></p>
<p class="label c">A cut<code>clip transition: blur</code></p>
<div class="panel a" data-component="word" data-start="0" data-duration="6">
<h1 data-bind-style="word">Hello</h1>
</div>
<div class="panel b" data-component="region" data-start="0" data-duration="6">
<img src="media/daylily.jpg" alt="">
<div class="over"></div>
</div>
<div class="panel c">
<div id="one" class="card" data-start="0" data-duration="3.5"><img src="media/daylily.jpg" alt=""></div>
<div class="card two" data-start="one - 1" data-duration="3.5" data-clip-transition="blur">Two</div>
</div>
</div>
const { scene, blur } = wildflower.kinetic;
// An element: a blur track, sharp between 1 and 4.5 s.
scene('word', {
keyframes: {
word: { blur: { at: [0, 1, 4.5, 5.5], to: [28, 0, 0, 28], ease: 'power2.inOut' } }
}
});
// A region: two bands blur the top and the foot of the picture, and leave a strip across the middle sharp.
scene('region', {
init() {
blur(this.element.querySelector('.over'), { scene: this, radius: 20, marks: (m) => m
.band({ from: [0, 0], to: [380, 0], width: 220, side: 'right', falloff: 'smooth' })
.band({ from: [380, 540], to: [0, 540], width: 220, side: 'right', falloff: 'smooth' }) });
}
});
// A cut: the markup's data-clip-transition does it, with no scene code.
html, body { margin: 0; background: #0a0b10; }
#stage { position: relative; overflow: hidden; background: #0b0f1a; }
.label { position: absolute; top: 28px; width: 380px; margin: 0; font: 700 32px system-ui, sans-serif; color: #fff; }
.label code { display: block; margin-top: 8px; font: 500 24px ui-monospace, monospace; color: #b8a6ff; }
.panel { position: absolute; top: 140px; width: 380px; height: 540px; overflow: hidden; border-radius: 16px; background: #161b2c; }
.a { left: 30px; }
.b { left: 450px; }
.c { left: 870px; }
.panel h1 { position: absolute; inset: 0; display: flex; align-items: center; justify-content: center; margin: 0; font: 800 110px system-ui, sans-serif; color: #fff; }
img { position: absolute; inset: 0; width: 100%; height: 100%; object-fit: cover; }
.over { position: absolute; inset: 0; }
.card { position: absolute; inset: 0; }
.two { display: flex; align-items: center; justify-content: center; background: #2c5a46; font: 800 110px system-ui, sans-serif; color: #fff; }
[data-composition-id]:not([data-kinetic-ready]) { visibility: hidden; }
The word blurs as a whole, by its own track. The picture stays sharp across the middle and soft at the top and the foot, and the strip holds still. The cut is markup only: the second clip starts a second before the first ends, and the overlap is the blur.
<div id="stage" data-composition-id="depth" data-start="0" data-duration="6" data-width="1280" data-height="720" data-fps="30">
<p class="label a">Depth of field<code>focus(z, plane)</code></p>
<p class="label b">The whole frame<code>finish: { soften: 8 }</code></p>
<div class="panel a" data-component="depth" data-start="0" data-duration="6">
<p class="plane" data-bind="planeText"></p>
<div class="row" data-list="discs" data-key="key">
<template><div class="disc" data-bind-style="style"><span data-bind="z"></span></div></template>
</div>
</div>
<div class="panel b" data-finish>
<img src="media/daylily.jpg" alt="">
</div>
</div>
const { scene, focus, styleAt } = wildflower.kinetic;
// The whole frame: the finish's soften, on the element marked data-finish.
wildflower.kinetic.config({ finish: { soften: 8 } });
const DEPTHS = [0, -200, -400, -600, -800];
// Depth of field: the focal plane moves back to -800 and returns, and each disc is blurred by its distance from it.
scene('depth', {
keyframes: {
plane: { at: [0.5, 2.5, 3.5, 5.5], to: [0, -800, -800, 0], ease: 'power2.inOut' }
},
computed: {
planeText() { return 'plane ' + Math.round(this.plane); },
discs() {
return DEPTHS.map((z, i) => ({
key: i,
z: String(z),
style: styleAt(this.local, { scale: 1000 / (1000 - z), blur: focus(z, this.plane, { range: 200, max: 12 }) })
}));
}
}
});
html, body { margin: 0; background: #0a0b10; }
#stage { position: relative; overflow: hidden; background: #0b0f1a; }
.label { position: absolute; top: 28px; width: 580px; margin: 0; font: 700 32px system-ui, sans-serif; color: #fff; }
.label code { display: block; margin-top: 8px; font: 500 24px ui-monospace, monospace; color: #b8a6ff; }
.panel { position: absolute; top: 140px; width: 580px; height: 540px; overflow: hidden; border-radius: 16px; background: #161b2c; }
.a { left: 40px; }
.b { left: 660px; }
.plane { position: absolute; left: 24px; top: 16px; margin: 0; font: 500 26px ui-monospace, monospace; color: #c9c9dc; }
.row { position: absolute; inset: 0; display: flex; align-items: center; justify-content: space-evenly; }
.disc { display: flex; align-items: center; justify-content: center; width: 100px; height: 100px; border-radius: 50%; background: #d99a2b; font: 700 24px system-ui, sans-serif; color: #1b1406; }
img { position: absolute; inset: 0; width: 100%; height: 100%; object-fit: cover; }
[data-composition-id]:not([data-kinetic-ready]) { visibility: hidden; }
Each disc is sharp while the focal plane is at its depth and softens as the plane moves away from it. The picture is soft all over, by the finish, which a render applies to every frame of the file.
Grain
Grain over the whole frame is the finish's grain, and it is the default.
It stays still unless its fps is set, and at the frame rate it is film grain.
shade() with kind: 'grain' is for grain that varies across the frame, and the finish's dust is specks rather than grain.
| What you want | The route |
|---|---|
| Grain over the whole frame (the default) | The finish's grain: config({ finish: { grain: 0.3 } }), still, a paper's tooth (The finish). |
| Film grain that changes every frame | The finish's grain with fps at the frame rate: config({ finish: { grain: { amount: 0.3, fps: 30 } } }) (The finish). |
| Grain heavier in some places, such as toward the edges | shade(canvas, { scene: this, kind: 'grain', fps: 30, marks }), as strong as the marks' field (Light, grain and blending). |
| Specks of dust that come and go | The finish's dust: config({ finish: { dust: 0.4 } }) (The finish). |
The finish is one setting for the composition, so the page shows it on one element: the root, or the element in it marked data-finish.
That is why the grain and the dust below are two examples.
<div id="stage" data-composition-id="grain" data-start="0" data-duration="6" data-width="1280" data-height="720" data-fps="30">
<p class="label a">Over the frame<code>finish: { grain }</code></p>
<p class="label b">Heavier at the edges<code>shade(canvas, { kind: 'grain' })</code></p>
<div class="panel a" data-finish>
<img src="media/daylily.jpg" alt="">
</div>
<div class="panel b" data-component="edges-grain" data-start="0" data-duration="6">
<img src="media/daylily.jpg" alt="">
<canvas></canvas>
</div>
</div>
const { scene, shade } = wildflower.kinetic;
// Over the frame: the finish's grain, on the element marked data-finish.
wildflower.kinetic.config({ finish: { grain: { amount: 0.6, size: 2 } } });
// Heavier at the edges: a glow with invert: true is empty at its center and full past its radius.
scene('edges-grain', {
init() {
shade(this.element.querySelector('canvas'), { scene: this, kind: 'grain', fps: 30, scale: 2, marks: (m) => m
.glow({ at: [290, 270], radius: 400, invert: true, falloff: 'power2.in' }) });
}
});
html, body { margin: 0; background: #0a0b10; }
#stage { position: relative; overflow: hidden; background: #0b0f1a; }
.label { position: absolute; top: 28px; width: 580px; margin: 0; font: 700 32px system-ui, sans-serif; color: #fff; }
.label code { display: block; margin-top: 8px; font: 500 24px ui-monospace, monospace; color: #b8a6ff; }
.panel { position: absolute; top: 140px; width: 580px; height: 540px; overflow: hidden; border-radius: 16px; background: #161b2c; }
.a { left: 40px; }
.b { left: 660px; }
img, canvas { position: absolute; inset: 0; width: 100%; height: 100%; }
img { object-fit: cover; }
[data-composition-id]:not([data-kinetic-ready]) { visibility: hidden; }
The finish's grain is even over the whole picture, and still. The shaded grain changes 30 times a second, and it is clean at the flower's center and heaviest in the corners, where the glow's field is full.
<div id="stage" data-composition-id="dust" data-start="0" data-duration="6" data-width="1280" data-height="720" data-fps="30">
<p class="label">Dust<code>finish: { dust }</code></p>
<div class="panel" data-finish>
<img src="media/daylily.jpg" alt="">
</div>
</div>
// Dust: sparse light specks, new on every frame. The picture between them stays clean.
wildflower.kinetic.config({ finish: { dust: { amount: 1, size: 2 } } });
html, body { margin: 0; background: #0a0b10; }
#stage { position: relative; overflow: hidden; background: #0b0f1a; }
.label { position: absolute; left: 350px; top: 28px; width: 580px; margin: 0; font: 700 32px system-ui, sans-serif; color: #fff; }
.label code { display: block; margin-top: 8px; font: 500 24px ui-monospace, monospace; color: #b8a6ff; }
.panel { position: absolute; left: 350px; top: 140px; width: 580px; height: 540px; overflow: hidden; border-radius: 16px; background: #161b2c; }
img { position: absolute; inset: 0; width: 100%; height: 100%; object-fit: cover; }
[data-composition-id]:not([data-kinetic-ready]) { visibility: hidden; }
Dust is a few light specks in new places on every frame, with the picture clean between them, where grain covers every pixel.
Music
The beat of steady music, which is most music made in a DAW, is a tempo on the audio clip, read with tempo(), and that is the default.
A beat file read by beat() is for music whose tempo drifts.
Loudness and named moments each have a route of their own.
| What you want | The route |
|---|---|
| Cuts and moves on the beat of steady music (the default) | data-tempo, data-downbeat, and data-meter on the audio clip, read with tempo(): tempo().at(n) is beat n's time, and tempo().of(t) the beat at t. scripts/beatgrid.cjs finds the three values (Markers and tempo). |
| The beats of music whose tempo drifts, such as live playing or an old recording | A beat file, which lists each beat's time, read by beat(t, beats) as a pulse from 0 to 1; HyperFrames' beat detection writes one (beat()). |
| Something that follows how loud the music is | level(t, band) for a band's level, or spectrum(t, bins) for levels from low to high (level() and spectrum()). |
| A named moment, such as the drop | data-markers="{ drop: 14.28 }" on the audio clip, in the file's seconds, and timeOf('drop') for its composition time (Markers and tempo). |
A tempo is one number for the whole file, so it stays on the beat only while the music does. A beat file gives every beat its own time, which follows a tempo that drifts.
data-tempo, data-downbeat, and data-meter describe, a tick per beat with the beat now lit, and the disc swelling on each beat. The speaker turns the sound on, and a click pauses.beat() reads the beats' times and strengths and scales the disc. A click on the stage pauses.Fading a clip
The default is the clip attributes, which are markup only, with no scene code: data-clip-enter and data-clip-exit fade one clip in and out, and data-clip-transition fades from one clip into the next.
edges and presence are for a scene whose own code needs to know how far in or out it is.
| What you want | The route |
|---|---|
| A clip that fades in, out, or both (the default) | data-clip-enter="fade 0.5" and data-clip-exit="fade 0.5" on the clip, with a length in seconds or frames (The attributes). |
| A fade from one clip into the next | The clips overlap, and the incoming one carries data-clip-transition="fade"; the overlap is the fade's length (Transitions). |
| A scene whose code needs to know how far in or out it is, such as to stagger its children by it | edges: { enter, exit }, then this.enter, this.exit, and this.presence, each 0 to 1 (Edges). |
| A fade that comes with a move or a scale on one element | An opacity track in keyframes beside the move (A layer with keyframes). |
| A fade on an element inside a scene that is no clip | ramp(t, from, to, ease) as its opacity, or transition('fade', p) bound with data-bind-style (ramp(), transition()). |
<div id="stage" data-composition-id="fades" data-start="0" data-duration="6" data-width="1280" data-height="720" data-fps="30">
<p class="label a">By attribute<code>data-clip-enter, data-clip-exit</code></p>
<p class="label b">By presence<code>edges, this.presence</code></p>
<div class="panel a" data-start="0.5" data-duration="5" data-clip-enter="fade 0.6" data-clip-exit="fade 0.6">
<h1>Fade</h1>
</div>
<div class="panel b" data-component="rows" data-start="0.5" data-duration="5">
<div class="list" data-list="rows" data-key="key">
<template><div class="bar" data-bind-style="style"></div></template>
</div>
</div>
</div>
const { scene, ramp, styleAt } = wildflower.kinetic;
// By attribute: the markup fades the clip, with no scene code.
// By presence: each bar takes its own part of the scene's presence, so the bars come in one after another and leave in the reverse order.
scene('rows', {
edges: { enter: 1, exit: 1 },
computed: {
rows() {
const out = [];
for (let i = 0; i < 6; i++) {
const p = ramp(this.presence, i * 0.1, i * 0.1 + 0.5, 'power2.out');
out.push({ key: i, style: styleAt(this.local, { opacity: p, translateX: (1 - p) * -60 }) });
}
return out;
}
}
});
html, body { margin: 0; background: #0a0b10; }
#stage { position: relative; overflow: hidden; background: #0b0f1a; }
.label { position: absolute; top: 28px; width: 580px; margin: 0; font: 700 32px system-ui, sans-serif; color: #fff; }
.label code { display: block; margin-top: 8px; font: 500 24px ui-monospace, monospace; color: #b8a6ff; }
.panel { position: absolute; top: 140px; width: 580px; height: 540px; overflow: hidden; border-radius: 16px; background: #161b2c; }
.a { left: 40px; }
.b { left: 660px; }
.panel h1 { position: absolute; inset: 0; display: flex; align-items: center; justify-content: center; margin: 0; font: 800 120px system-ui, sans-serif; color: #fff; }
.list { position: absolute; inset: 60px; display: flex; flex-direction: column; justify-content: space-between; }
.bar { height: 60px; border-radius: 12px; background: #9d7bff; }
[data-composition-id]:not([data-kinetic-ready]) { visibility: hidden; }
The left panel fades as one clip, by its two attributes.
The right panel needs to know how far in the scene is to stagger its bars, so it reads presence; a fade that needs no such value stays with the attributes.