// card-footage-cadence|字卡穿插节奏
// UI 镜头 ↔ 黑底字卡像对话接拍:0–14f UI A 缓推 → 硬切 8f 字卡 SHIP →
// 硬切 12f UI B 1.6x 裁切缓移 → 8f 字卡 FASTER → 10f UI A 2x 裁切 →
// 10f 字卡 TODAY(下划线) → 62f 硬切收尾全景定格。
// UI 段全部带微动(推/移),字卡段除落定微缩(1.05→1, out-cubic)外全静,
// 动↔静对比即手法本体。所有切换零过渡(条件挂载,无 crossfade)。
// 收尾 62–105f 轻推收完,105–150f 真静止 45f ≥ 40f。帧确定,无随机。
import React from 'react';
import { AbsoluteFill, useCurrentFrame, interpolate, Easing } from 'remotion';
import { FakeDashboard } from '../../_fixtures/Fixtures';
const CLAMP = { extrapolateLeft: 'clamp', extrapolateRight: 'clamp' } as const;
// 黑底白字字卡:落定微缩 1.05→1(out-cubic,段内前 5f 完成),其余全静
const TitleCard: React.FC<{ text: string; local: number; underline?: boolean }> = ({
text,
local,
underline = false,
}) => {
const scale = interpolate(local, [0, 5], [1.05, 1], {
...CLAMP,
easing: Easing.out(Easing.cubic),
});
return (
{text}
{underline && (
)}
);
};
// UI 镜头:包一层 transform 做缓推/缓移,硬切由外层条件挂载保证
const UiShot: React.FC<{
variant: 'A' | 'B';
transform: string;
origin?: string;
}> = ({ variant, transform, origin = '50% 50%' }) => (
);
export const CardFootageCadence: React.FC = () => {
const frame = useCurrentFrame();
// ---- 分段:切点 14 / 22 / 34 / 42 / 52 / 62,总长 150 ----
// 段1 0–14f:UI A 全景缓推 1.0→1.08
if (frame < 14) {
const s = interpolate(frame, [0, 14], [1, 1.08], CLAMP);
return ;
}
// 段2 14–22f:字卡 SHIP
if (frame < 22) {
return ;
}
// 段3 22–34f:UI B 1.6x 裁切 + 横向缓移(120px 内容位移,屏上 ~192px)。
// origin 偏左让列表行图标入画,读得出是 UI(QA 后调:50%→35%)
if (frame < 34) {
const tx = interpolate(frame - 22, [0, 12], [60, -60], CLAMP);
return (
);
}
// 段4 34–42f:字卡 FASTER
if (frame < 42) {
return ;
}
// 段5 42–52f:UI A 另一处 2x 裁切(左上卡片区)+ 继续缓推 2.0→2.14
if (frame < 52) {
const s = interpolate(frame - 42, [0, 10], [2, 2.14], CLAMP);
return ;
}
// 段6 52–62f:字卡 TODAY + 下划线
if (frame < 62) {
return ;
}
// 段7 62–150f:收尾全景 UI 定格。62–105f 极缓推收完(out-cubic),
// 105f 后 transform 值恒为 1.02,105–150f 真静止 45f。
const s = interpolate(frame, [62, 105], [1, 1.02], {
...CLAMP,
easing: Easing.out(Easing.cubic),
});
return ;
};