// 里程表数字滚动大字报(odometer-digit-roll)——声画钩子 / Vercel Ship 指标段。 // 全屏巨号 "99.98%"(190px fw800 tabular-nums),四个数位各是一条 0–9 纵向 // strip(overflow:hidden 数位盒),高速滚动后逐位停稳:位 i 在 20+i*7f 开始 // 减速(Easing.out(cubic)),过冲半格再 6f 弹回锁定。滚动期每位叠 2 个错帧 // 残影副本(translateY ±行高*0.5,opacity 0.25/0.12),按帧速度门控,停稳即摘。 // 小数点与 % 不滚动,一直驻场。 // 关键帧:0–20 四位全速滚(0.85 行/帧)→ 20/27/34/41 逐位开始减速(各 16f // 减速 + 6f 回弹,锁定于 42/49/56/63)→ 63–71 整体加深脉冲(ink→#000→ink, // 附 1.035 微缩放加码)→ 66–84 下方标签条淡入 → 84–150 全静止(66f ≥45f)。 import React from 'react'; import { useCurrentFrame, interpolate, interpolateColors, Easing } from 'remotion'; import { G, TitleBlock } from '../../_fixtures/Fixtures'; const ROW = 210; // 数位行高(overflow 盒高) const DW = 126; // 数位盒宽 const FS = 190; // 字号 const SPIN = 0.85; // 高速滚动速度:行/帧 const DIGITS = [9, 9, 9, 8]; // 目标数位("99.98" 中可滚的四位) // 位 i 的 strip 位置(单位:行,连续值)。纯帧函数,天然确定性。 const posAt = (f: number, i: number): number => { const d = DIGITS[i]; const s = 20 + i * 7; // 开始减速帧 const p0 = SPIN * s; // 最小再走 6 行后,落在个位 = d 的最近整数位置 const T = Math.ceil((p0 + 6 - d) / 10) * 10 + d; if (f < s) return SPIN * Math.max(f, 0); if (f < s + 16) return interpolate(f, [s, s + 16], [p0, T + 0.5], { extrapolateLeft: 'clamp', extrapolateRight: 'clamp', easing: Easing.out(Easing.cubic), }); if (f < s + 22) return interpolate(f, [s + 16, s + 22], [T + 0.5, T], { extrapolateLeft: 'clamp', extrapolateRight: 'clamp', easing: Easing.out(Easing.cubic), }); return T; }; // 一条 0–9 纵列 strip(两轮 20 格,容过冲跨界) const Strip: React.FC<{ pos: number; color: string; opacity?: number; dy?: number }> = ({ pos, color, opacity = 1, dy = 0, }) => (