// ===== demos/data/particle-celebrate-hits/ConfettiCrossfire.tsx ===== // confetti-crossfire —— 双侧礼炮交叉喷洒 // 中央大 KPI 卡 scale 落定(f0–14),揭晓帧 f16 左下+右下两门炮各射 50 颗矩形彩屑: // 闭式弹道(初速 ~18px/f + spread 55° + 重力 + decay 0.9 的位移闭式解),每帧翻转 8–15°, // 灰阶为主 + 1/3 琥珀。全部彩屑 ~f100 前落出画外(越界即条件卸载),结尾真静止 ≥55f。 // 帧确定性:sin 散列伪随机派生每颗初速/角度/翻转率,弹道 = 纯 age 的函数。 import React from 'react'; import { useCurrentFrame, interpolate, Easing } from 'remotion'; import { G, TitleBlock } from '../../_fixtures/Fixtures'; const AMBER = '#b45309'; const FIRE = 16; // 揭晓帧 = 发射帧 const DECAY = 0.9; const GRAV = 1.5; // px/f² (等效重力;decay 下终端落速 = GRAV/(1-d) = 15px/f) 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); // decay 弹道闭式解:v 每帧 ×0.9 → 位移 = v0·(1-d^age)/(1-d);重力项同样按衰减序列累积 const decaySum = (age: number) => (1 - Math.pow(DECAY, age)) / (1 - DECAY); type Confetto = { vx: number; vy: number; w: number; h: number; spin: number; phase: number; amber: boolean; shade: string; }; const makeGun = (originDeg: number, saltBase: number): Confetto[] => Array.from({ length: 50 }).map((_, i) => { const ang = ((originDeg + (rnd(i, saltBase) - 0.5) * 55) * Math.PI) / 180; // decay 0.9 下初速总位移只有 10×v0(velocity 6.6f 减半),18px/f 只够 180px 完全不可感 // → 加码到 70–95px/f:最陡颗峰值升到 y≈390(画面上 1/3),弹幕跨越中央卡,~f96 全部落出 const speed = 70 + rnd(i, saltBase + 1) * 25; const grays = ['#6d6d6b', '#8f8f8d', '#4a4a48', '#b0b0ae']; return { vx: Math.cos(ang) * speed, vy: -Math.sin(ang) * speed, // 屏幕坐标向下为正,射向斜上 w: 14 + rnd(i, saltBase + 2) * 12, h: 8 + rnd(i, saltBase + 3) * 8, spin: 8 + rnd(i, saltBase + 4) * 7, // 8–15°/f phase: rnd(i, saltBase + 5) * 360, amber: rnd(i, saltBase + 6) < 1 / 3, shade: grays[Math.floor(rnd(i, saltBase + 7) * 4)], }; }); const LEFT_GUN = makeGun(60, 3); // 左下炮朝 60°(偏右上) const RIGHT_GUN = makeGun(120, 9); // 右下炮朝 120°(偏左上) const LEFT_POS = { x: 140, y: 1040 }; const RIGHT_POS = { x: 1780, y: 1040 }; export const ConfettiCrossfire: React.FC = () => { const frame = useCurrentFrame(); const age = frame - FIRE; // KPI 卡 scale 入场落定 const cardScale = interpolate(frame, [0, 14], [0.6, 1], { easing: Easing.out(Easing.back(1.8)), extrapolateRight: 'clamp', }); const cardOp = interpolate(frame, [0, 8], [0, 1], { extrapolateRight: 'clamp' }); const renderGun = (gun: Confetto[], origin: { x: number; y: number }, keyBase: string) => gun.map((c, i) => { if (age <= 0) return null; const s = decaySum(age); const x = origin.x + c.vx * s; // 重力:每帧速度 +GRAV 再统一衰减 → 位移闭式 Σ 可整理为 GRAV·(age - s·d)/(1-d) 的近似; // 直接用精确序列和:g 项位移 = GRAV · Σ_{k=1..age} (1-d^k)/(1-d) = GRAV·(age-(d-d^{age+1})/(1-d))/(1-d) const gDisp = (GRAV * (age - (DECAY - Math.pow(DECAY, age + 1)) / (1 - DECAY))) / (1 - DECAY); const y = origin.y + c.vy * s + gDisp; // 落出画外即卸载(左右也裁) if (y > 1140 || x < -80 || x > 2000) return null; const rot = c.phase + c.spin * age; return (
); }); return (
{/* 中央 KPI 卡 */}
98.5%
{renderGun(LEFT_GUN, LEFT_POS, 'L')} {renderGun(RIGHT_GUN, RIGHT_POS, 'R')}
); }; // ===== demos/data/particle-celebrate-hits/CounterTickSparks.tsx ===== // counter-tick-sparks —— 数字跳动溅火 // 中央大计数器 0 → 12,847(easeOut 逐位跳动),每逢跳过整千的 tick 帧, // 数字顶部溅 6–10 颗大火星(初速向上 9–13px/f、重力下坠 14–18f 内坠灭); // 终值揭晓那跳翻倍 20 颗 + 数字弹 1.1x 回落。 // 结尾所有火星寿命耗尽条件卸载,真静止 ≥40f。 // 帧确定性:tick 帧从同一 easeOut 曲线预解析(模块级求出),火星 = 纯 age 闭式弹道。 import React from 'react'; import { useCurrentFrame, interpolate, Easing } from 'remotion'; import { G, TitleBlock } from '../../_fixtures/Fixtures'; const AMBER = '#b45309'; const TARGET = 12847; const COUNT_END = 78; // 计数结束帧 const easeOutCubic = (t: number) => 1 - Math.pow(1 - t, 3); const valueAt = (f: number) => Math.round(TARGET * easeOutCubic(Math.min(Math.max(f / COUNT_END, 0), 1))); 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); // 预解析 tick 帧:跳过整千的那一帧 + 终值揭晓帧 const TICKS: { f: number; big: boolean }[] = (() => { const out: { f: number; big: boolean }[] = []; let prev = 0; for (let f = 1; f <= COUNT_END; f++) { const v = valueAt(f); if (Math.floor(v / 1000) > Math.floor(prev / 1000)) out.push({ f, big: false }); prev = v; } out.push({ f: COUNT_END, big: true }); // 终值揭晓 return out; })(); const SPARK_LIFE = 18; const GRAV = 0.9; export const CounterTickSparks: React.FC = () => { const frame = useCurrentFrame(); const value = valueAt(frame); // 终值揭晓弹 1.1x 回落 const popScale = 1 + 0.1 * interpolate(frame, [COUNT_END, COUNT_END + 5, COUNT_END + 16], [0, 1, 0], { easing: Easing.out(Easing.quad), extrapolateLeft: 'clamp', extrapolateRight: 'clamp', }); const TOP_Y = 452; // 数字顶缘(火星发射线) const CX = 960; return (
{/* 计数卡 */}
{value.toLocaleString('en-US')}
{/* 火星:每个 tick 一簇,寿命耗尽条件卸载 */} {TICKS.map((tick, t) => { const age = frame - tick.f; if (age <= 0 || age >= SPARK_LIFE) return null; const n = tick.big ? 20 : 6 + Math.floor(rnd(t, 21) * 5); // 6–10,终跳 20 return Array.from({ length: n }).map((_, i) => { const salt = t * 31 + i; // 初速:向上 9–13px/f,水平 ±4.5px/f 扇形铺开(终跳更宽) const vy = -(9 + rnd(salt, 2) * 4); const vx = (rnd(salt, 3) - 0.5) * (tick.big ? 13 : 9); const x0 = CX + (rnd(salt, 4) - 0.5) * (tick.big ? 560 : 380); // 沿数字顶缘散布 const x = x0 + vx * age; const y = TOP_Y + vy * age + 0.5 * GRAV * age * age; const life = 1 - age / SPARK_LIFE; const size = (tick.big ? 7 : 5.5) * (0.4 + 0.6 * life); return (
); }); })}
); };