A simulation baked at load
Three balls drop and bounce, each losing a different share of its speed on every landing. A simulation steps forward in time while a render asks for frames in any order, so it never runs in a seek: beside the scene, each ball is stepped to the end of the clip at 1/240 s, finer than any frame, and its height at every step is kept in a typed array.
<div id="stage" data-composition-id="sim" data-start="0" data-duration="4" data-width="1280" data-height="720" data-fps="30">
<div class="layer" data-component="balls" data-start="0" data-duration="4">
<div class="floor"></div>
<div data-list="balls" data-key="key">
<template><div class="ball" data-bind-style="style"></div></template>
</div>
</div>
</div>
const { scene, mix, styleAt } = wildflower.kinetic;
const DURATION = 4, DT = 1 / 240, STEPS = Math.ceil(DURATION / DT) + 1;
const FLOOR = 600; // px, where a ball's bottom edge lands
const G = 2400; // px/s²
const BALLS = [
{ x: 440, bounce: 0.8, color: '#5fd3c6' },
{ x: 640, bounce: 0.6, color: '#f2c14e' },
{ x: 840, bounce: 0.4, color: '#aa3de6' },
];
// Baked once: each ball's height at every step, from a drop at y 120.
for (const b of BALLS) {
b.y = new Float32Array(STEPS);
let y = 120, v = 0;
for (let k = 0; k < STEPS; k++) {
b.y[k] = y;
v += G * DT;
y += v * DT;
if (y > FLOOR) { y = FLOOR - (y - FLOOR) * b.bounce; v = -v * b.bounce; }
}
}
// The height at time t, between the two steps either side of it; held at the ends.
function heightAt(b, t) {
const u = Math.min(STEPS - 1, Math.max(0, t / DT)), k = Math.min(STEPS - 2, Math.floor(u));
return mix(b.y[k], b.y[k + 1], u - k);
}
scene('balls', {
computed: {
balls() {
return BALLS.map((b, key) => ({ key, style: { background: b.color, ...styleAt(this.local, { translateX: b.x, translateY: heightAt(b, this.local) }) } }));
},
},
});
html, body { margin: 0; background: #0a0b10; }
#stage { position: relative; overflow: hidden; background: #0c0c10; }
.layer { position: absolute; inset: 0; }
.floor { position: absolute; left: 0; top: 600px; width: 1280px; height: 2px; background: #888; }
.ball { position: absolute; left: -22px; top: -44px; width: 44px; height: 44px; border-radius: 50%; }
[data-composition-id]:not([data-kinetic-ready]) { visibility: hidden; }
A simulation baked at load
- The computed reads the two steps either side of the time and mixes them, so a seek to any frame, in any order, draws the same thing.
- Any physics engine works this way when it runs at load with a fixed step and takes its randomness from
random(seed). - A landing between two frames is never drawn. For a contact that has to be seen, choose the drop and the speeds so it lands on a whole frame, as A landing on a whole frame does.
- When a learner's setting changes the result, bake again from a watcher on that store field.