// particle-sand-fill — particles rain into bars // Four bars inside a chart card; above each bar it "rains": 14px square grains fall with // stagger (gravity accelerates), stop the moment they hit the pile, and bounce ~15% once, stacking // layer by layer — the pile height is pre-resolved in closed form (layer k's top = baseline - // (k+1)×grain size, no real collision). Bars start 6f apart; once full, the particle surface fades // out to a solid bar while a value label pops in above. // At the end all particles conditionally unmount, leaving only the solid bars + labels, with true // stillness ≥35f. // Frame determinism: a sin hash derives each grain's departure jitter/start offset; landing frames // are inverted in closed form from the height difference. import React from 'react'; import { useCurrentFrame, interpolate, Easing } from 'remotion'; import { G, TitleBlock } from '../../_fixtures/Fixtures'; const AMBER = '#b45309'; const frac = (x: number) => x - Math.floor(x); const rnd = (i: number, salt: number) => frac(Math.sin(i * 12.9898 + salt * 78.233) * 43758.5453); const CARD = { x: 460, y: 300, w: 1000, h: 560 }; const PLOT_BOTTOM = CARD.y + CARD.h - 70; // pile floor (baseline inside the card) const GRAIN = 14; // grain size (err big: 4px is imperceptible inside a 1080p card, so cranked to 14) const PER_LAYER = 9; // 9 grains per layer → bar width 126px const BAR_W = GRAIN * PER_LAYER; const DROP_FROM = 230; // grains start ~230px above their landing spot const GRAV = 1.6; // px/f² const STAGGER = 6; // bars stagger their start const RATE = 0.28; // departure interval between grains (frames) — the tallest bar needs ~60f to launch 216 grains, all finished within the global f120 const BARS = [ { cx: CARD.x + 175, h: 238, label: '238' }, { cx: CARD.x + 395, h: 336, label: '336' }, { cx: CARD.x + 615, h: 182, label: '182' }, { cx: CARD.x + 835, h: 294, label: '294' }, ].map((b) => ({ ...b, layers: Math.round(b.h / GRAIN), n: Math.round(b.h / GRAIN) * PER_LAYER })); const fallTime = (dist: number) => Math.sqrt((2 * dist) / GRAV); const departOf = (bar: number, i: number) => 8 + bar * STAGGER + i * RATE + rnd(i, bar * 7 + 1) * 1.5; export const ParticleSandFill: React.FC = () => { const frame = useCurrentFrame(); return (