Speed Ramp
The code of the Speed Ramp demo, 2 files.
index.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Speed Ramp | WildflowerJS Demo</title>
<script defer src="/js/analytics.js"></script>
<!-- The framework, then Kinetic with its preview bar, then the scene. -->
<script defer src="/js/dist/wildflower.lite.min.js"></script>
<script defer src="/js/extensions/kinetic.wf.min.js" data-preview></script>
<script defer src="app.js"></script>
<style>
* { box-sizing: border-box; }
html, body { margin: 0; background: #0a0b10; color: #e6e6f0; font-family: system-ui, sans-serif; }
.demo-toolbar { display: flex; justify-content: space-between; align-items: center; padding: 8px 20px; background: #12141c; border-bottom: 1px solid rgba(255,255,255,0.08); }
.demo-toolbar a { display: flex; align-items: center; gap: 10px; text-decoration: none; color: #5fa6a6; font-size: 1rem; }
.demo-toolbar .logo { height: 32px; }
.demo-toolbar .brand-wf { font-weight: 520; }
.demo-toolbar .brand-js { font-weight: 700; }
.demo-toolbar .demo-info { font-size: 11px; color: rgba(255,255,255,0.6); text-align: right; }
.demo-toolbar .demo-info strong { color: rgba(255,255,255,0.9); }
#stage:not([data-kinetic-ready]) { visibility: hidden; }
.frame { padding: 24px; }
#stage { position: relative; width: 1280px; height: 720px; overflow: hidden; background: #0b0a10; }
.label { position: absolute; top: 22px; margin: 0; font-size: 22px; letter-spacing: 0.06em; color: #c9c3b8; }
.label b { color: #f2efe6; }
.clip { position: absolute; top: 64px; width: 600px; height: 338px; object-fit: cover; border-radius: 6px; background: #000; }
.readout { position: absolute; top: 412px; margin: 0; font: 20px ui-monospace, Menlo, monospace; color: #c9c3b8; }
.readout b { color: #f5c542; font-weight: 600; }
.layer { position: absolute; inset: 0; }
.graph { position: absolute; left: 160px; top: 456px; }
.graph text { font: 15px ui-monospace, Menlo, monospace; fill: #8a8496; }
</style>
</head>
<body>
<div class="demo-toolbar">
<a href="/demos/">
<img class="logo" src="/assets/wildflower_96.png" alt="WildflowerJS">
<span><span class="brand-wf">Wildflower</span><span class="brand-js">JS</span> Demos</span>
</a>
<div class="demo-info"><strong>Speed Ramp</strong>: a clip time-remapped</div>
</div>
<div class="frame">
<div id="stage" data-composition-id="ramp" data-start="0" data-duration="9" data-width="1280" data-height="720" data-fps="30">
<p class="label" style="left:24px"><b>1×</b> the file as it is</p>
<p class="label" style="left:656px"><b>data-remap</b> fast, hold, slow, normal</p>
<!-- The same file twice: on the left at its own speed, muted; on the
right remapped, with its sound, a pluck half a second into every second of the file. -->
<video class="clip" style="left:24px" src="/media/timecode.mp4" data-start="0" data-duration="8.5" muted playsinline></video>
<video class="clip" style="left:656px" src="/media/timecode.mp4" data-start="0" data-has-audio="true" playsinline
data-remap="{ at: [0, 1.5, 3, 4, 6, 8.5], to: [0, 1.5, 5, 5, 5.5, 8], ease: 'smooth' }"></video>
<div class="layer" data-component="ramp-readout" data-start="0" data-duration="9">
<p class="readout" style="left:656px">clip <b data-bind="clip"></b> s · file <b data-bind="file"></b> s · <b data-bind="speed"></b>×</p>
<svg class="graph" width="960" height="240" viewBox="0 0 960 240">
<line x1="40" y1="200" x2="940" y2="200" stroke="#3a3448" stroke-width="2"/>
<line x1="40" y1="200" x2="40" y2="10" stroke="#3a3448" stroke-width="2"/>
<text x="44" y="226">clip time →</text>
<text x="50" y="22">↑ file time</text>
<path data-bind-attr="{ d: straight }" fill="none" stroke="#3a3448" stroke-width="2" stroke-dasharray="6 8"></path>
<path data-bind-attr="{ d: curve }" fill="none" stroke="#f5c542" stroke-width="4"></path>
<circle r="9" fill="#e0702f" data-bind-attr="{ cx: dotX, cy: dotY }"></circle>
</svg>
</div>
</div>
</div>
<div class="about">
<p>Press play with the sound on. The right clip is the left one remapped with <code>data-remap</code>, and the yellow curve is the remap.</p>
</div>
<style>
.about { max-width: 1280px; margin: 0 auto; padding: 0 24px 96px; color: #a9adc2; font-size: 14px; line-height: 1.55; }
.about p { margin: 0 0 10px; }
.about code { color: #e6e6f0; font-size: 13px; }
</style>
</body>
</html>
app.js
// Speed Ramp: the readout and the graph beside a remapped clip. The clip's
// own markup carries the remap; this scene reads the same keys through
// interpolate(), which is how Kinetic maps the clip's time to the file's.
const { scene, interpolate } = wildflower.kinetic;
const AT = [0, 1.5, 3, 4, 6, 8.5];
const TO = [0, 1.5, 5, 5, 5.5, 8];
const OPTS = { ease: 'smooth' };
const fileAt = (t) => interpolate(t, AT, TO, OPTS);
// The graph: clip time 0 to 9 s across 900 px, file time 0 to 9 s up 190 px.
const X = (t) => 40 + t * 100;
const Y = (f) => 200 - f * (190 / 9);
const pathOf = (fn) => {
let d = '';
for (let i = 0; i <= 170; i++) {
const t = i * 0.05;
d += (i ? ' L ' : 'M ') + X(t).toFixed(1) + ' ' + Y(fn(t)).toFixed(1);
}
return d;
};
const CURVE = pathOf(fileAt);
const STRAIGHT = pathOf((t) => t);
scene('ramp-readout', {
computed: {
curve() { return CURVE; },
straight() { return STRAIGHT; },
now() { return Math.min(Math.max(this.local, 0), 8.5); },
clip() { return this.now.toFixed(2); },
file() { return fileAt(this.now).toFixed(2); },
speed() {
const t = this.now, a = Math.max(0, t - 0.02), b = Math.min(8.5, t + 0.02);
return b > a ? ((fileAt(b) - fileAt(a)) / (b - a)).toFixed(2) : '0.00';
},
dotX() { return X(this.now).toFixed(1); },
dotY() { return Y(fileAt(this.now)).toFixed(1); },
},
});