// ===== demos/data/timeline-travel/TimelineTravel.tsx ===== // timeline-travel —— 时间轴横移(《反恐王国》式) // 镜头沿水平刻度轴加速横移,v1.0/v2.0/v3.0/Today 四个刻度依次掠过, // 每过刻度对应 Card 从刻度线 spring 过冲弹立 + 短停,镜头不停; // 末刻度 4f 急停 + 推近 1.28×。世界层只动 translateX/scale。 // f0–12 初始静置;f118 起真静止 ≥42f(160f 总长)。 import React from 'react'; import { useCurrentFrame, interpolate, Easing, spring } from 'remotion'; import { G, Card, TitleBlock } from '../../_fixtures/Fixtures'; const W = 1920; const AXIS_Y = 700; const TICK_GAP = 1400; // 刻度间距(世界坐标) const TICKS = [ { label: 'v1.0', x: 960 }, { label: 'v2.0', x: 960 + TICK_GAP }, { label: 'v3.0', x: 960 + TICK_GAP * 2 }, { label: 'Today', x: 960 + TICK_GAP * 3 }, ]; const WORLD_W = 960 + TICK_GAP * 3 + 960; const TRAVEL_START = 12; const TRAVEL_END = 104; // 急停帧 const ZOOM_END = 114; // 相机 X:in-out 但前段慢后段快(poly(3) in 为主,末端 out 急收) // 用两段拼:0–0.82 加速段(Easing.in(poly(2.2))),0.82–1 急刹段 const camXAt = (f: number): number => { const total = TICKS[3].x - 960; // 需要位移的世界距离 const t = interpolate(f, [TRAVEL_START, TRAVEL_END], [0, 1], { extrapolateLeft: 'clamp', extrapolateRight: 'clamp', }); // 加速→巡航→急刹:分段缓动,前 15% 缓起,中段近匀加速冲刺,末 12% 急收 const eased = interpolate(t, [0, 0.15, 0.88, 1], [0, 0.055, 0.9, 1], { easing: Easing.inOut(Easing.quad), }); return eased * total; }; // 每张卡的弹立帧:相机中心扫过该刻度的时刻(数值上预先求好,避免逐帧求逆) // 通过 camXAt 反查:找到 camX == tick.x - 960 的帧 const popFrameOf = (tickX: number): number => { for (let f = TRAVEL_START; f <= TRAVEL_END; f++) { if (camXAt(f) >= tickX - 960) return f; } return TRAVEL_END; }; const CARD_W = 360; const CARD_H = 240; const TickStop: React.FC<{ i: number; frame: number }> = ({ i, frame }) => { const tick = TICKS[i]; const pop = popFrameOf(tick.x) - 6; // 提前 6f 起弹,掠过时正好立起 const s = spring({ frame: frame - pop, fps: 30, config: { damping: 11, stiffness: 160, mass: 0.9 }, // 明显过冲 durationInFrames: 26, }); const appeared = frame >= pop; return (
{/* 刻度竖线 */}
{/* 刻度标签 */}
{tick.label}
{/* 卡片从刻度线弹立:以底边为轴 scaleY 0→1(带过冲),伴随上移 */} {appeared && (
)}
); }; export const TimelineTravel: React.FC = () => { const frame = useCurrentFrame(); const camX = camXAt(frame); // 急停后推近末刻度:scale 1 → 1.28,中心对准 Today 刻度 const zoom = interpolate(frame, [TRAVEL_END, ZOOM_END], [1, 1.28], { easing: Easing.out(Easing.cubic), extrapolateLeft: 'clamp', extrapolateRight: 'clamp', }); return (
{/* 推近层:以画面中央偏下(末刻度落点)为原点放大 */}
{/* 世界层:唯一横移的容器 */}
{/* 主轴线 */}
{/* 次刻度(小点,增强速度感) */} {Array.from({ length: 22 }).map((_, i) => (
))} {TICKS.map((_, i) => ( ))}
); };