// ===== demos/rhythm/beat-cut-moves/BeatCutAccelerando.tsx =====
import React from 'react';
import { AbsoluteFill, Easing, interpolate, useCurrentFrame } from 'remotion';
import { FakeDashboard, G } from '../../_fixtures/Fixtures';
// beat-cut-accelerando:六个不同构图按 16→12→8→6→4 帧递减间隔全屏硬切,
// 加速逼近,最后一切戛然定格回主画面并 1→1.06 慢推收住。
// 真硬切:frame 落在哪个区间就渲染哪个视图,无任何过渡。
// 一个视图 = FakeDashboard 变体 + 缩放 + 对焦点(画面上要被推到屏幕中心的点)
type View = { variant: 'A' | 'B'; scale: number; cx: number; cy: number };
const VIEWS: View[] = [
{ variant: 'A', scale: 1, cx: 960, cy: 540 }, // v0 全景(建立)
{ variant: 'A', scale: 1.8, cx: 1070, cy: 576 }, // v1 卡片区
{ variant: 'A', scale: 2.6, cx: 600, cy: 340 }, // v2 单卡特写
{ variant: 'B', scale: 1, cx: 960, cy: 540 }, // v3 列表页全景
{ variant: 'B', scale: 1.9, cx: 1070, cy: 500 }, // v4 列表行区
{ variant: 'B', scale: 2.8, cx: 1070, cy: 290 }, // v5 单行特写
];
// 每段起始帧:建立 49f,然后间隔 16→12→8→6→4,末段(回主画面)hold 35f
// 0–48 v0 | 49–64 v1 | 65–76 v2 | 77–84 v3 | 85–90 v4 | 91–94 v5 | 95–129 定格
const CUTS = [0, 49, 65, 77, 85, 91, 95];
const FINAL = 95; // 最后一切:戛然定格回主画面
const ViewShot: React.FC<{ view: View; extraScale?: number }> = ({ view, extraScale = 1 }) => {
const s = view.scale * extraScale;
return (
);
};
export const BeatCutAccelerando: React.FC = () => {
const frame = useCurrentFrame();
// 当前落在哪个区间(末段 = 主画面 v0)
let seg = 0;
for (let i = 0; i < CUTS.length; i++) {
if (frame >= CUTS[i]) seg = i;
}
const isFinal = seg === CUTS.length - 1;
const view = isFinal ? VIEWS[0] : VIEWS[seg];
// 末段慢推:scale 1 → 1.06,ease-out,推 22f 后静止(结尾静止 ≥15f)
const push = isFinal
? interpolate(frame, [FINAL, FINAL + 20], [1, 1.06], {
extrapolateLeft: 'clamp',
extrapolateRight: 'clamp',
easing: Easing.out(Easing.cubic),
})
: 1;
// 每次硬切的 1f 亮度跳变(约 +5%)模拟快门
const isCutFrame = CUTS.some((c, i) => i > 0 && frame === c);
const flash = isCutFrame ? 1.05 : 1;
return (
{/* 切帧再叠一层极薄白闪,保证肉眼可感 */}
{isCutFrame && (
)}
);
};
// ===== demos/rhythm/beat-cut-moves/PaparazziFlash.tsx =====
import React from 'react';
import { AbsoluteFill, Easing, interpolate, useCurrentFrame } from 'remotion';
import { FakeDashboard, TitleBlock, G } from '../../_fixtures/Fixtures';
// paparazzi flash 连闪定格:高光时刻三连白闪,每闪硬切同一素材的不同裁切
// (全景→卡片特写→数字特写),每闪切入画面带 1.03→1 回落 + 半格沉降像快门
// 余韵,白闪帧加微位移抖动模拟快门震,第三闪后停在数字特写收束。
const F1 = 30; // 第一闪(间隔 22f)
const F2 = 52; // 第二闪(间隔 18f)
const F3 = 70; // 第三闪,之后 hold 60f
const SETTLE = 6; // 切入回落时长
const DECAY = 4; // 白层衰减时长
const FLASHES = [F1, F2, F3];
// seed 正弦哈希,返回 -1..1(禁 Math.random)
const hash = (i: number) => {
const s = Math.sin(i * 127.3) * 43758.5453;
return (s - Math.floor(s)) * 2 - 1;
};
// 同一"素材":全景 dashboard,顶行中间卡片上叠一个大数字,供数字特写裁切
const Footage: React.FC = () => (
);
// 四个视图:闪前"活素材"慢推 + 三个定格裁切(全景 / 卡片 / 数字)
type View = { scale: number; origin: string };
const VIEW_WIDE: View = { scale: 1.0, origin: '960px 540px' };
const VIEW_CARD: View = { scale: 2.3, origin: '1070px 335px' };
const VIEW_DIGIT: View = { scale: 4.0, origin: '1070px 350px' };
export const PaparazziFlash: React.FC = () => {
const frame = useCurrentFrame();
// 当前处于哪一段:-1 = 闪前活素材段
let seg = -1;
for (let i = 0; i < FLASHES.length; i++) {
if (frame >= FLASHES[i]) seg = i;
}
let scale: number;
let origin: string;
let settleScale = 1;
let settleY = 0;
if (seg === -1) {
// 闪前:中景慢推(活的),衬托闪后定格的"死"
origin = '900px 480px';
scale = interpolate(frame, [0, F1], [1.16, 1.2], {
extrapolateRight: 'clamp',
easing: Easing.inOut(Easing.quad),
});
} else {
const view = [VIEW_WIDE, VIEW_CARD, VIEW_DIGIT][seg];
scale = view.scale;
origin = view.origin;
// 切入回落:1.03→1 的 6f 收敛 + 半格沉降(-16px 落回 0)像快门余韵
const t = interpolate(frame, [FLASHES[seg], FLASHES[seg] + SETTLE], [1, 0], {
extrapolateLeft: 'clamp',
extrapolateRight: 'clamp',
easing: Easing.out(Easing.cubic),
});
settleScale = 1 + 0.03 * t;
settleY = -16 * t;
}
// 白闪层:每个切点 0.95→0,4f 衰减,取各闪最大值
let flash = 0;
for (const f of FLASHES) {
const o = interpolate(frame, [f, f + DECAY], [0.95, 0], {
extrapolateLeft: 'clamp',
extrapolateRight: 'clamp',
easing: Easing.out(Easing.quad),
});
if (frame >= f && frame < f + DECAY + 1) flash = Math.max(flash, o);
}
// 白闪帧全屏微位移抖动模拟快门震(seed 哈希,非随机)
const inFlash = FLASHES.some((f) => frame >= f && frame < f + DECAY);
const jx = inFlash ? 2 * hash(frame * 7 + 1) : 0;
const jy = inFlash ? 2 * hash(frame * 13 + 5) : 0;
return (
{/* 白闪层 */}
{flash > 0 && (
)}
);
};