// ===== demos/data/scroll-brake-moves/BrakeReticleLock.tsx =====
// 组合:长卷急刹 × 准星咬合(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
)}
);
};
// ===== demos/data/scroll-brake-moves/ChangelogScrollBrake.tsx =====
// changelog-scroll-brake —— Changelog 长卷急刹
// ~34 行灰阶条目(行高错落)从下往上高速掠过(easeOutExpo 指数减速),
// 高速段叠 blur(速度差分驱动,糊成色带),急刹精准停位后目标行抬升
// (scale 1.03 + 阴影加深)+ 高亮描边,其余行退暗。f=84 后全静止(56f)。
import React from 'react';
import { useCurrentFrame, interpolate, Easing } from 'remotion';
import { G } from '../../_fixtures/Fixtures';
const CL = { extrapolateLeft: 'clamp', extrapolateRight: 'clamp' } as const;
// 时间轴
const SCROLL0 = 14; // 前 14f 初始静置(停在长卷顶部)
const SCROLL1 = 64; // 急刹停位帧
const LIFT0 = 68; // 目标行抬升开始
const LIFT1 = 82;
const COL_W = 1000;
const COL_X = (1920 - COL_W) / 2;
const GAP = 20;
const N = 34;
const TARGET = 28;
// 行高错落(帧确定性:全由 i 决定)
const rowH = (i: number) => 72 + ((i * 29) % 3) * 22; // 72 / 94 / 116
// 预计算每行 y
const rowY: number[] = [];
{
let y = 0;
for (let i = 0; i < N; i++) {
rowY.push(y);
y += rowH(i) + GAP;
}
}
const TARGET_CY = rowY[TARGET] + rowH(TARGET) / 2;
const END_T = 540 - TARGET_CY; // 目标行停在画面正中
const START_T = 80; // 起始:长卷顶部略下
const scrollAt = (f: number): number =>
interpolate(f, [SCROLL0, SCROLL1], [START_T, END_T], {
easing: Easing.out(Easing.exp),
...CL,
});
const Row: React.FC<{ i: number; frame: number }> = ({ i, frame }) => {
const isTarget = i === TARGET;
const h = rowH(i);
const titleW = 30 + ((i * 37) % 45);
// 目标行抬升 + 高亮;其余行退暗
const t = interpolate(frame, [LIFT0, LIFT1], [0, 1], {
easing: Easing.out(Easing.cubic),
...CL,
});
const lift = isTarget ? t : 0;
const dim = isTarget ? 0 : t;
return (
0 ? `3px solid rgba(47,47,47,${lift})` : `2px solid ${G.border}`,
borderRadius: 14,
display: 'flex',
alignItems: 'center',
gap: 24,
padding: '0 30px',
boxSizing: 'border-box',
transform: `scale(${1 + 0.03 * lift})`,
boxShadow:
lift > 0
? `0 ${6 + 22 * lift}px ${16 + 44 * lift}px rgba(0,0,0,${0.08 + 0.28 * lift})`
: '0 2px 8px rgba(0,0,0,0.06)',
opacity: 1 - 0.62 * dim,
zIndex: isTarget ? 2 : 1,
}}
>
);
};
export const ChangelogScrollBrake: React.FC = () => {
const frame = useCurrentFrame();
const T = scrollAt(frame);
// 速度差分驱动模糊:v 达 60px/f 即满 6px blur
const v = Math.abs(scrollAt(frame) - scrollAt(frame - 1));
const blur = Math.min(v / 60, 1) * 6;
return (
0.15 ? `blur(${blur}px)` : undefined,
}}
>
{Array.from({ length: N }).map((_, i) => (
))}
);
};