// 组合:长卷急刹 × 准星咬合(brake-reticle-lock) // changelog 长列表从下往上高速掠过(加速冲刺→9f 猛减速带 30px 超调回弹), // 急刹帧(BRAKE=59)同帧四个 L 形角标从画外四个方向飞入,back-out 超调咬合 // 锁定停点条目四角;条目同步高亮 + 右侧弹出小标签。 // 组合命门:角标咬合帧与列表急刹帧必须同帧共振(都在 f=59 起跳),错开即退化。 // 关键帧:0–12 初始静置 → 12–50 加速冲刺(速度段 blur)→ 50–59 猛减速超调 → // 59 急刹+角标飞入 → 59–72 咬合回弹/高亮/标签 → 75–150 真静止 75f。 // 帧确定,无随机源。 import React from 'react'; import { useCurrentFrame, interpolate, Easing } from 'remotion'; import { G } from '../../_fixtures/Fixtures'; const SCROLL_START = 12; const BRAKE = 59; const DUR = 150; const PITCH = 156; // 行高 120 + 间距 36 const ROW_H = 120; const TARGET_ROW = 30; const FINAL_SCROLL = TARGET_ROW * PITCH - 480; // 停点条目 top 落在屏幕 y=480 const LIST_X = 360; const LIST_W = 1200; // 滚动位置:12–50 加速冲刺(sin-in,越滚越快)→ 50–59 猛减速冲过头 30px → // 59–63 回弹落定。59 帧为急刹帧(首次停住/反向)。 const scrollAt = (f: number): number => { if (f <= SCROLL_START) return 0; if (f <= 50) { return interpolate(f, [SCROLL_START, 50], [0, FINAL_SCROLL - 430], { easing: Easing.in(Easing.sin), }); } if (f <= BRAKE) { return interpolate(f, [50, BRAKE], [FINAL_SCROLL - 430, FINAL_SCROLL + 30], { easing: Easing.out(Easing.cubic), }); } return interpolate(f, [BRAKE, BRAKE + 4], [FINAL_SCROLL + 30, FINAL_SCROLL], { extrapolateRight: 'clamp', easing: Easing.out(Easing.quad), }); }; // 一行 changelog:图标块 + 标题条 + 尾部日期条,宽度由行号确定(帧确定) const Row: React.FC<{ i: number; highlight: number }> = ({ i, highlight }) => (
0 ? '#ffffff' : G.card, border: `${highlight > 0 ? 3 : 2}px solid ${highlight > 0 ? G.ink : G.border}`, borderRadius: 14, display: 'flex', alignItems: 'center', gap: 24, padding: '0 28px', boxSizing: 'border-box', boxShadow: highlight > 0 ? `0 10px 34px rgba(0,0,0,${0.2 * highlight})` : 'none', }} >
); // L 形角标:两条粗边组成的直角 const Corner: React.FC<{ flip: [number, number]; style: React.CSSProperties }> = ({ flip, style }) => (
); export const BrakeReticleLock: React.FC = () => { const f = useCurrentFrame(); const scroll = scrollAt(f); // 速度段 blur:由相邻帧位移决定,冲刺段全糊,刹停即清晰 const v = Math.abs(scroll - scrollAt(Math.max(0, f - 1))); const blur = Math.min(v * 0.12, 24); // 急刹帧起:高亮 0→1(6f),标签 63f 弹出 const highlight = interpolate(f, [BRAKE, BRAKE + 6], [0, 1], { extrapolateLeft: 'clamp', extrapolateRight: 'clamp', }); // 角标:急刹帧同帧从画外四方向飞入,back-out 超调咬合(59–68f) const lockT = interpolate(f, [BRAKE, BRAKE + 9], [0, 1], { extrapolateLeft: 'clamp', extrapolateRight: 'clamp', easing: Easing.out(Easing.back(2.4)), }); const fly = 1 - lockT; // 1=画外 0=咬合到位 // 停点条目屏幕坐标(落定后 top=480) const rowTop = TARGET_ROW * PITCH - scroll; const GAP = 10; // 咬合位:角外扩 10px const rect = { x: LIST_X - GAP, y: rowTop - GAP, w: LIST_W + GAP * 2, h: ROW_H + GAP * 2 }; const corners: Array<{ x: number; y: number; fromX: number; fromY: number; flip: [number, number] }> = [ { x: rect.x - 23, y: rect.y - 23, fromX: -620, fromY: -320, flip: [1, 1] }, { x: rect.x + rect.w - 23, y: rect.y - 23, fromX: 620, fromY: -320, flip: [-1, 1] }, { x: rect.x - 23, y: rect.y + rect.h - 23, fromX: -620, fromY: 320, flip: [1, -1] }, { x: rect.x + rect.w - 23, y: rect.y + rect.h - 23, fromX: 620, fromY: 320, flip: [-1, -1] }, ]; // 小标签:63f 从条目右侧弹出(超调) const tagT = interpolate(f, [BRAKE + 4, BRAKE + 12], [0, 1], { extrapolateLeft: 'clamp', extrapolateRight: 'clamp', easing: Easing.out(Easing.back(2.6)), }); return (
{/* 顶栏(不随滚动) */}
{/* changelog 长列表:整体上掠,速度段 blur */}
0.5 ? `blur(${blur}px)` : 'none' }}>
{Array.from({ length: 38 }).map((_, i) => ( ))}
{/* 准星角标:急刹帧同帧挂载(组合命门:与刹停同帧共振) */} {f >= BRAKE && corners.map((c, i) => ( ))} {/* 旁弹小标签 */} {f >= BRAKE + 4 && (
v2.41
)}
); };