// ===== demos/data/particle-sand-fill/ParticleSandFill.tsx ===== // particle-sand-fill —— 粒子落斗成柱 // 图表卡内 4 根柱,每根柱上方"下雨":14px 方点错峰坠落(重力加速),触堆积面即停 // + 15% 回弹一下,逐层堆高——堆积高度闭式预解析(第 k 层顶面 = 基线 - (k+1)×粒径, // 无真碰撞)。各柱错峰 6f 启动;堆满后粒子面淡出换实体柱 + 顶部数值标签弹出。 // 结尾全部粒子条件卸载、只剩实体柱 + 标签,真静止 ≥35f。 // 帧确定性:sin 散列派生每颗出发帧抖动/起点错高,落地帧由高度差闭式反解。 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; // 堆积地面(卡内基线) const GRAIN = 14; // 方点边长(宁大勿小:4px 在 1080p 卡内不可感,加码到 14) const PER_LAYER = 9; // 每层 9 颗 → 柱宽 126px const BAR_W = GRAIN * PER_LAYER; const DROP_FROM = 230; // 距各自落点上方 ~230px 起落 const GRAV = 1.6; // px/f² const STAGGER = 6; // 各柱错峰启动 const RATE = 0.28; // 颗间出发间隔(帧)——最高柱 216 颗需 ~60f 发完,全局 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 (