// ===== demos/rhythm/panel-grid-moves/ComicPanelSplit.tsx ===== // comic-panel-split|漫画分格并列 // FakeDashboard A(加 KPI 数字块)全屏 20f → 咔咔咔切成 3 个斜线分格 // (12° 斜边,10px 白缝 + 墨线描边),逐格 2f 间隔弹入(3f scale 1.06→1 // + 加深脉冲)。三格 = 同页面三机位:全景 1x / 卡片特写 1.9x / 数字区 // 特写 2.6x。定格 18f 各格缓慢微推近保持活 → 第三格斜边框 12f out-cubic // 扩张吃掉全屏成为下一镜特写。57f 起摘罩(特写直出、分格结构与缝线 // 全部卸载),57–150f 真静止 93f ≥ 40f。帧确定,无随机源。 import React from 'react'; import { AbsoluteFill, useCurrentFrame, interpolate, Easing } from 'remotion'; import { FakeDashboard, G } from '../../_fixtures/Fixtures'; const SPLIT = 20; // 全屏 A 结束、开始分格 const POP = 3; // 每格弹入 3f const STAGGER = 2; // 逐格 2f 间隔 const HOLD_END = 45; // 27f 弹完 + 定格 18f const EXPAND_END = 57; // 第三格 12f 扩张结束 // 57–150 真静止 93f // 斜边 12°:tan(12°)×1080 ≈ 230,顶边比底边右移 230 // 缝 10px(水平半宽 5px) // 边界1:顶 750 / 底 520;边界2:顶 1405 / 底 1175 const outCubic = Easing.out(Easing.cubic); // 页面 = FakeDashboard A + 左上卡片内 KPI 数字块(给"数字区特写"一个锚点) const PageA: React.FC = () => (
1,284
); type PanelSpec = { clip: (f: number) => string; // clip-path polygon centroidX: number; // 弹入缩放原点 originX: number; originY: number; // 内容变换原点(焦点) baseScale: number; // 机位倍率 tx: number; ty: number; // 焦点搬到格中心的位移 z: number; }; export const ComicPanelSplit: React.FC = () => { const frame = useCurrentFrame(); // ===== 摘罩:扩张完成后特写直出,分格结构 / 缝线全部卸载 ===== if (frame >= EXPAND_END) { return (
); } // ===== 阶段 1:全屏 A ===== if (frame < SPLIT) { return ( ); } // ===== 阶段 2/3:分格 + 第三格扩张 ===== // 定格期微推近(27–45f 线性,很缓) const push = interpolate(frame, [SPLIT + 2 * STAGGER + POP, HOLD_END], [0, 1], { extrapolateLeft: 'clamp', extrapolateRight: 'clamp', }); // 第三格扩张进度(扩散用 out-cubic) const ex = interpolate(frame, [HOLD_END, EXPAND_END], [0, 1], { extrapolateLeft: 'clamp', extrapolateRight: 'clamp', easing: outCubic, }); // 第三格左斜边(= 边界2 吃屏轨迹) const e3Top = 1410 + ex * (-60 - 1410); // 1410 → -60 const e3Bot = 1180 + ex * (-290 - 1180); // 1180 → -290 const panels: PanelSpec[] = [ { // 全景 1x clip: () => 'polygon(0px 0px, 745px 0px, 515px 1080px, 0px 1080px)', centroidX: 315, originX: 960, originY: 540, baseScale: 1 + push * 0.03, tx: 0, ty: 0, z: 1, }, { // 卡片特写 1.9x(中上卡片) clip: () => 'polygon(755px 0px, 1400px 0px, 1170px 1080px, 525px 1080px)', centroidX: 962, originX: 1070, originY: 371, baseScale: 1.9 + push * 0.055, tx: -108, ty: 169, z: 1, }, { // 数字区特写 2.6x(KPI 块),扩张时焦点从格中心搬到屏中心 clip: () => `polygon(${e3Top}px 0px, 1920px 0px, 1920px 1080px, ${e3Bot}px 1080px)`, centroidX: 1607, originX: 518, originY: 400, // 扩张时 push 增量退掉,scale 收敛回 2.6(与摘罩帧完全一致) baseScale: 2.6 + push * 0.08 * (1 - ex), tx: 1089 + ex * (442 - 1089), ty: 140, z: 3, }, ]; // 缝线透明度:边界1 随第 2 格弹入出现、扩张期被吃前线性淡出; // 边界2 随第 3 格弹入出现、扩张末 4f 线性淡出(消散用线性) const seam1O = Math.min( interpolate(frame, [SPLIT + STAGGER, SPLIT + STAGGER + 2], [0, 1], { extrapolateLeft: 'clamp', extrapolateRight: 'clamp' }), interpolate(frame, [HOLD_END, HOLD_END + 3], [1, 0], { extrapolateLeft: 'clamp', extrapolateRight: 'clamp' }), ); const seam2O = Math.min( interpolate(frame, [SPLIT + 2 * STAGGER, SPLIT + 2 * STAGGER + 2], [0, 1], { extrapolateLeft: 'clamp', extrapolateRight: 'clamp' }), interpolate(frame, [EXPAND_END - 4, EXPAND_END], [1, 0], { extrapolateLeft: 'clamp', extrapolateRight: 'clamp' }), ); return ( {panels.map((p, i) => { const start = SPLIT + i * STAGGER; if (frame < start) return null; // 未弹入不渲染 // 弹入:3f scale 1.06→1 + 加深脉冲 const pop = interpolate(frame, [start, start + POP], [0, 1], { extrapolateLeft: 'clamp', extrapolateRight: 'clamp', easing: outCubic, }); const popScale = 1.06 - 0.06 * pop; const pulse = 0.3 * (1 - pop); return (
{pulse > 0.005 && (
)}
); })} {/* 斜缝:白缝命门 + 墨线描边(暗 16 下、白 10 上 → 两侧各 3px 墨边) */} {(seam1O > 0.005 || seam2O > 0.005) && ( {seam1O > 0.005 && ( )} {seam2O > 0.005 && ( )} )} ); }; // ===== demos/rhythm/panel-grid-moves/FlipGridReflow.tsx ===== // flip-grid-reflow —— 网格 FLIP 重排 // 6 张 Card 初始横排一行(marquee 式,屏心偏上)静止 30f,节拍点集体换位: // 每卡沿直线飞向 3×2 网格目标位(预写两套坐标表),scale 1→1.28, // delay = 卡索引×1.5f 微错峰,16f inOut(cubic) + 落定 3f 过冲 1.02。 // 全部落定后整体加深脉冲收束。收尾真静止 ≥40f。 import React from 'react'; import { useCurrentFrame, interpolate, Easing } from 'remotion'; import { G, Card, TitleBlock } from '../../_fixtures/Fixtures'; const CARD_W = 280; const CARD_H = 170; const N = 6; // ---- 坐标表 A:横排一行(卡间 24px,屏心偏上)---- // 总宽 6*280 + 5*24 = 1800,x0 = 60;中心 y = 330 const ROW_Y = 330; const ROW_CENTERS: Array<[number, number]> = Array.from({ length: N }, (_, i) => [ 60 + CARD_W / 2 + i * (CARD_W + 24), ROW_Y, ]); // x: 200, 504, 808, 1112, 1416, 1720 // ---- 坐标表 B:3×2 网格(卡放大 1.28 → 358.4×217.6,网格间距 30px)---- // 网格总宽 3*358.4 + 2*30 = 1135.2 → x0 = 392.4;总高 2*217.6 + 30 = 465.2 → y0 = 307.4 const GC = [571.6, 960, 1348.4]; // 列中心 const GR = [416.2, 663.8]; // 行中心 // 交错映射:偶数索引去上排、奇数去下排(每卡直线飞行、互不对穿) const GRID_CENTERS: Array<[number, number]> = [ [GC[0], GR[0]], // card0 → 上左 [GC[0], GR[1]], // card1 → 下左 [GC[1], GR[0]], // card2 → 上中 [GC[1], GR[1]], // card3 → 下中 [GC[2], GR[0]], // card4 → 上右 [GC[2], GR[1]], // card5 → 下右 ]; const BEAT = 30; // 节拍点 const STAGGER = 1.5; // 每卡延迟 const MOVE = 16; // 飞行帧数 const SETTLE = 3; // 落定过冲帧数 const SCALE_END = 1.28; const OVERSHOOT = 1.02; // 末卡完全落定:30 + 5*1.5 + 16 + 3 = 56.5 const ALL_SETTLED = BEAT + (N - 1) * STAGGER + MOVE + SETTLE; // 56.5 // 加深脉冲:f58 → f61 谷值 → f64 回正(谷值 brightness 0.78,spec 4f 放宽到 6f 保可感) const PULSE_IN = 58; const PULSE_MID = 61; const PULSE_OUT = 64; const moveEase = Easing.inOut(Easing.cubic); const FlipCard: React.FC<{ i: number; frame: number }> = ({ i, frame }) => { const t0 = BEAT + i * STAGGER; const [x0, y0] = ROW_CENTERS[i]; const [x1, y1] = GRID_CENTERS[i]; const x = interpolate(frame, [t0, t0 + MOVE], [x0, x1], { easing: moveEase, extrapolateLeft: 'clamp', extrapolateRight: 'clamp', }); const y = interpolate(frame, [t0, t0 + MOVE], [y0, y1], { easing: moveEase, extrapolateLeft: 'clamp', extrapolateRight: 'clamp', }); // scale:16f 冲到 1.28*1.02,随后 3f 回落到 1.28 const sUp = interpolate(frame, [t0, t0 + MOVE], [1, SCALE_END * OVERSHOOT], { easing: moveEase, extrapolateLeft: 'clamp', extrapolateRight: 'clamp', }); const sBack = interpolate(frame, [t0 + MOVE, t0 + MOVE + SETTLE], [SCALE_END * OVERSHOOT, SCALE_END], { easing: Easing.out(Easing.cubic), extrapolateLeft: 'clamp', extrapolateRight: 'clamp', }); const s = frame < t0 + MOVE ? sUp : sBack; return (
); }; export const FlipGridReflow: React.FC = () => { const frame = useCurrentFrame(); // 加深脉冲:仅脉冲窗口内挂载 filter,窗口外完全不挂(摘罩) const pulsing = frame >= PULSE_IN && frame <= PULSE_OUT; const bright = pulsing ? interpolate(frame, [PULSE_IN, PULSE_MID, PULSE_OUT], [1, 0.78, 1], { extrapolateLeft: 'clamp', extrapolateRight: 'clamp', }) : 1; return (
{Array.from({ length: N }).map((_, i) => ( ))}
); }; // ===== demos/rhythm/panel-grid-moves/GridFlashMosaic.tsx ===== // grid-flash-mosaic —— 九宫格闪切 // 浅底 → f25 起 3×3 网格按十六分音符(每 2f)逐格啪啪硬入(顺序 h(i) 打乱), // 每格 = FakeDashboard 不同区域裁切片或灰卡;入格 3f scale 1.18→1 + 2f 加深脉冲。 // 填满后停 14f(整墙微呼吸 1.008) → 中心格 14f Easing.in(cubic) 放大吞掉全屏 // 成为满屏页面。收尾真静止 ≥40f。 import React from 'react'; import { useCurrentFrame, interpolate, Easing } from 'remotion'; import { G, Card, FakeDashboard } from '../../_fixtures/Fixtures'; const h = (n: number) => { const s = Math.sin(n * 127.3) * 43758.5453; return s - Math.floor(s); }; const CELL_W = 600; const CELL_H = 340; const GAP = 12; const GRID_X = (1920 - (CELL_W * 3 + GAP * 2)) / 2; // 48 const GRID_Y = (1080 - (CELL_H * 3 + GAP * 2)) / 2; // 18 const FILL_START = 25; // 第一格落下 const STEP = 2; // 十六分音符:每 2f 一格 // 打乱顺序:索引按 h(i+1) 排序 const ORDER = Array.from({ length: 9 }, (_, i) => i).sort( (a, b) => h(a + 1) - h(b + 1) ); const RANK: number[] = []; ORDER.forEach((cell, k) => (RANK[cell] = k)); const LAST_IN = FILL_START + 8 * STEP + 3; // 最后一格入格动画结束 f44 const HOLD_END = LAST_IN + 14; // 整墙呼吸段结束 f58 const ZOOM_DUR = 14; const ZOOM_END = HOLD_END + ZOOM_DUR; // f72,之后真静止 // 中心格内是整页 FakeDashboard 缩小版(0.3125),放大后正好成为满屏页面 const MINI_SCALE = CELL_W / 1920; // 0.3125 // 3.2 恰好铺满;加到 3.28 让格子的深色描边完全滑出画外,收尾满屏页面干净 const ZOOM_SCALE = 3.28; // 非中心格:FakeDashboard 裁切偏移 (x, y, variant) 或灰卡(null) const CROPS: Array<{ x: number; y: number; v: 'A' | 'B' } | null> = [ { x: -120, y: -60, v: 'A' }, null, // 灰卡 { x: -1260, y: -120, v: 'B' }, { x: -60, y: -520, v: 'B' }, null, // 中心格(单独处理,占位) { x: -1300, y: -640, v: 'A' }, { x: -420, y: -300, v: 'B' }, null, // 灰卡 { x: -900, y: -680, v: 'A' }, ]; const CellContent: React.FC<{ i: number }> = ({ i }) => { if (i === 4) { // 中心格:整页 dashboard 缩到格内 return (
); } const crop = CROPS[i]; if (crop === null) { // 灰卡格 return (
); } // 裁切片:整页 dashboard 以不同偏移塞进格子(相当于 backgroundPosition 各异) return (
); }; export const GridFlashMosaic: React.FC = () => { const f = useCurrentFrame(); // 整墙微呼吸:仅在填满后的 14f 停顿段,一个正弦来回 1→1.008→1 const breath = f >= LAST_IN && f < HOLD_END ? 1 + 0.008 * Math.sin((Math.PI * (f - LAST_IN)) / 14) : 1; // 中心格放大:14f Easing.in(cubic),吞掉全屏 const zoom = interpolate(f, [HOLD_END, ZOOM_END], [1, ZOOM_SCALE], { easing: Easing.in(Easing.cubic), extrapolateLeft: 'clamp', extrapolateRight: 'clamp', }); return (
{Array.from({ length: 9 }).map((_, i) => { const start = FILL_START + RANK[i] * STEP; if (f < start) return null; // 硬入:未到拍点不渲染,无淡化 const row = Math.floor(i / 3); const col = i % 3; // 入格 3f scale 1.18→1 const popScale = interpolate(f, [start, start + 3], [1.18, 1], { extrapolateLeft: 'clamp', extrapolateRight: 'clamp', }); // 2f 加深脉冲 const darken = interpolate(f, [start, start + 2], [0.45, 0], { extrapolateLeft: 'clamp', extrapolateRight: 'clamp', }); const isCenter = i === 4; const cellScale = isCenter ? popScale * zoom : popScale; return (
{darken > 0.001 && (
)}
); })}
); };