// tape-scroll-fixed-pointer —— 滚带定针 // 取景窗+琥珀三角指针钉死在画面正中,竖向长刻度带(0–500,每 50 一大格) // 从窗后滚过:"世界动、针不动"。先慢速爬(f12–55 读数 60→140 缓涨), // 然后一次大跳变:刻度带冲刺 ~23f(峰值 ~50px/f)冲过 420 到 442, // spring 刹车回摆一次(442→415→420)落定。窗内读数同步。f96 后真静止 44f。 // 帧确定性:value(frame) 纯分段 interpolate 全 clamp,带偏移 = value 线性映射。 import React from 'react'; import { useCurrentFrame, interpolate, Easing } from 'remotion'; import { G, TitleBlock } from '../../_fixtures/Fixtures'; const AMBER = '#b45309'; const PXU = 3; // px per unit:500 量程 → 1500px 长带 const CENTER_Y = 590; // 取景窗中线(屏幕坐标) const TAPE_X = 830; // 刻度带左缘 const TAPE_W = 260; // 读数时间线:静置→慢爬→冲刺(过冲到442)→回摆(415)→落定420→真静止 const valueAt = (frame: number): number => { if (frame <= 12) return 60; if (frame <= 55) { return interpolate(frame, [12, 55], [60, 140]); // 慢爬 ~5.6px/f } if (frame <= 78) { return interpolate(frame, [55, 78], [140, 442], { easing: Easing.inOut(Easing.cubic), // 冲刺段,峰值 ~50px/f }); } if (frame <= 88) { return interpolate(frame, [78, 88], [442, 415], { easing: Easing.out(Easing.cubic), // 刹车回摆:甩过头再拉回 }); } return interpolate(frame, [88, 96], [415, 420], { easing: Easing.out(Easing.cubic), extrapolateRight: 'clamp', }); }; export const TapeScrollFixedPointer: React.FC = () => { const frame = useCurrentFrame(); const v = valueAt(frame); // 刻度带:数值大在上。单位 u 的屏幕 y = CENTER_Y + (v - u) * PXU const yOf = (u: number): number => CENTER_Y + (v - u) * PXU; const ticks: React.ReactNode[] = []; for (let u = 0; u <= 500; u += 10) { const y = yOf(u); if (y < 180 || y > 1010) continue; // 视野外裁剪 const major = u % 50 === 0; ticks.push(
); if (major) { ticks.push(
{u}
); } } return (
{/* 刻度带容器(世界层:整体在动) */}
{ // 内层坐标 = 屏幕 y - 180
{ticks}
}
{/* 固定层:取景窗 + 琥珀三角指针 + 读数(针不动) */}
{/* 同步读数窗 */}
{Math.round(v)}
); };