Particles from an emitter
A fountain throws sparks up from one point, forty a second, each falling back and fading over a second and a half.
Every particle the clip will ever have is made beside the scene, with its birth time and its speeds from random(seed), and each one is a function of its age, so a seek to any time draws the right sparks.
<div id="stage" data-composition-id="sparks" data-start="0" data-duration="5" data-width="1280" data-height="720" data-fps="30">
<div class="layer" data-component="fountain" data-start="0" data-duration="5">
<div data-pool="sparks" data-key="id">
<template><i class="spark" data-bind-style="style"></i></template>
</div>
</div>
</div>
const { scene, random, styleAt } = wildflower.kinetic;
const DURATION = 5;
const RATE = 40; // particles a second
const LIFE = 1.5; // seconds each one lives
const G = 900; // px/s², pulling them back down
const SLOTS = Math.ceil(RATE * LIFE) + 1;
const rnd = random(9);
const SPARKS = Array.from({ length: Math.ceil(RATE * DURATION) }, (_, i) => ({
born: i / RATE, vx: -120 + 240 * rnd(), vy: -620 - 160 * rnd(), size: 4 + 6 * rnd(),
}));
// A hidden slot writes the same properties a shown one does, so nothing a
// spark set at another time is left on it.
const HIDDEN = { width: '0px', height: '0px', opacity: '0', transform: 'none' };
scene('fountain', {
pools: { sparks: {} },
init() { for (let s = 0; s < SLOTS; s++) this.pools.sparks.add({ id: s, style: HIDDEN }); },
seek(local) {
const n = Math.floor(local * RATE + 1e-9); // the newest particle born by now
for (let s = 0; s < SLOTS; s++) {
const p = SPARKS[n - ((((n - s) % SLOTS) + SLOTS) % SLOTS)]; // the newest one in slot s
const age = p ? local - p.born : -1;
if (!(age >= 0 && age < LIFE)) { this.pools.sparks.update(s, { style: HIDDEN }); continue; }
const x = 640 + p.vx * age, y = 620 + p.vy * age + 0.5 * G * age * age;
this.pools.sparks.update(s, {
style: { width: p.size + 'px', height: p.size + 'px', opacity: (1 - age / LIFE).toFixed(3), ...styleAt(local, { translateX: x, translateY: y }) },
});
}
},
});
html, body { margin: 0; background: #0a0b10; }
#stage { position: relative; overflow: hidden; background: #0c0c10; }
.layer { position: absolute; inset: 0; }
.spark { position: absolute; left: -4px; top: -4px; border-radius: 50%; background: #f2c14e; }
[data-composition-id]:not([data-kinetic-ready]) { visibility: hidden; }
Particles from an emitter
- A pool holds a fixed number of slots, and particle
ilives in sloti % SLOTS, so no two live particles share one. SLOTSis the rate times the life, rounded up, plus one. A particle born just as the last one in its slot reaches the end of its life would otherwise find that one still alive by a rounding error, and one of the two would not be drawn.- Before the first particle, and in a slot whose index is above the newest particle's, the lookup finds no particle and the slot is hidden.
update()writes the properties it is given and leaves the rest, so a hidden slot writes every property a shown one does. Withopacityalone, a slot keeps the size and place of whatever spark it last showed, and a frame depends on the frames seeked before it, whichcheck()reports.- A burst is the same with every birth time equal, and a drift reads
noise()at the particle's age.