// 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 && ( )} )} ); };