// ===== demos/transition/shot-transitions/BlackCardTransition.tsx ===== // D 式 黑场字卡(black-card)——前镜收尾淡入黑场,字卡逐词压印出现(paper-title-card // 的暗场变体),再交棒后镜。章节级分段 + 呼吸位二合一;一支 30s 片 D 式 ≤2 次。 // 参考实现(真实纹理):A 景 = projects-full 项目板(前镜收尾 **8f** 淡入黑场, // 符合定义 6–10 帧);暗底字卡 = "weekly brief, every project linked" 逐词 letterpress // (暗底 + 页面底色浅色字,不用强调色当正文字——浅底字卡才不像报错弹窗),等宽小字 // 副行;完整标题落定后 **hold ~15 帧** 再退场,避免"最后一个字刚出现就淡出"; // 后镜 = wbr-full 周报页从黑场淡入 8f 交棒。 // 节拍:0–8 A 淡出 → 8–20 黑场静 → 20–47 字卡逐词压印 → 47–62 完整标题 hold → // 62–70 字卡退场 → 70–78 淡入 B 景 → 78–120 B hold。 import { AbsoluteFill, Img, interpolate, staticFile, useCurrentFrame, Easing } from 'remotion'; export const BLACKCARD_DUR = 120; const A_VIEW_Y = -180; const SERIF = 'ui-serif, Georgia, "Times New Roman", serif'; const MONO = 'ui-monospace, SFMono-Regular, Menlo, monospace'; // 暗底字卡的正文用页面底色(浅色),强调色只给重点词 const PAPER_LIGHT = 'oklch(92% 0.01 82)'; const AMBER = 'oklch(68% 0.12 65)'; const DIM = 'oklch(72% 0.01 82)'; const WORDS: { text: string; accent?: boolean }[] = [ { text: 'Every' }, { text: 'project,' }, { text: 'linked' }, { text: 'to' }, { text: 'your', accent: true }, { text: 'weekly' }, { text: 'report.' }, ]; const Scene: React.FC = () => { const frame = useCurrentFrame(); // A 景收尾:0→8 淡出到黑场(定义要求 6–10 帧,此处 8) const aOut = interpolate(frame, [0, 8], [1, 0], { extrapolateLeft: 'clamp', extrapolateRight: 'clamp', easing: Easing.bezier(0.5, 0, 0.4, 1), }); // 字卡:20 起逐词压印(delay=20+i·3,最后词 i=6 delay=38 完成 47) const cardOpacity = interpolate(frame, [18, 26], [0, 1], { extrapolateLeft: 'clamp', extrapolateRight: 'clamp', easing: Easing.bezier(0.2, 0.7, 0.3, 1), }); // 完整标题 47f 落定,hold 到 62 再退场(保留 ~15 帧完整展示) const cardOut = interpolate(frame, [62, 70], [0, 1], { extrapolateLeft: 'clamp', extrapolateRight: 'clamp', easing: Easing.bezier(0.4, 0, 0.5, 1), }); // 后镜 B 淡入:70→78(字卡 62→70 完全淡出后再交棒) const bIn = interpolate(frame, [70, 78], [0, 1], { extrapolateLeft: 'clamp', extrapolateRight: 'clamp', easing: Easing.bezier(0.3, 0, 0.2, 1), }); return ( {/* A 景 */} {frame < 14 ? ( ) : null} {/* 黑场字卡(18–74) */} {frame >= 18 && frame < 74 ? ( {WORDS.map((w, i) => { const delay = 20 + i * 3; const t = interpolate(frame, [delay, delay + 9], [0, 1], { extrapolateLeft: 'clamp', extrapolateRight: 'clamp', easing: Easing.bezier(0.2, 0.75, 0.3, 1), }); return ( {w.text} ); })} Weekly Brief · 2026-W28 ) : null} {/* B 景(70 起淡入) */} {frame >= 70 ? ( ) : null} ); }; export const BlackCardTransition: React.FC = () => ( ); // ===== demos/transition/shot-transitions/DarkTunnelTransition.tsx ===== // B 式 穿暗场直航(dark-tunnel)——相机顺运动方向推出前景 → 纯暗场滑行几帧 → // 后景从景深迎面放大而来,一条 take 不切。高能量→高能量的场景跳转主力转场。 // 参考实现(真实纹理):A 景 = projects-full 全满项目板(前镜收尾段), // 推出 10f 相机向右(前景在屏幕空间向左滑出画);暗场段保留微渐变尘点让 // "still moving" 可感,不纯黑死场;B 景 = wbr-full 周报页从 scale ~0.6 + blur // 起步、10f 放大收焦入场。出与入同向(相机向右推出 → B 景从右缘迎来、 // 屏幕空间同样向左),速度曲线连续。 // 节拍:0–26 A hold → 26–36 推出(前景左移出画,暗场 32f 起淡入压住尾巴, // 不留纯黑死帧)→ 40–42 暗场满 → 42–52 B 放大入场 → 52–120 B hold(真静止)。 import { AbsoluteFill, Img, interpolate, staticFile, useCurrentFrame, Easing } from 'remotion'; export const DARKTUNNEL_DUR = 120; // 两景都以 cards 网格区为观察窗;A 景顶对 y=180,B 景(wbr)顶对 y=0 const A_VIEW_Y = -180; const A_W = 1920; const Scene: React.FC = () => { const frame = useCurrentFrame(); // A 景推出:26→36,相机向右推 = 前景在屏幕空间向左平移出画。 // 屏幕空间方向必须与 B 景一致(都向左):之前写成 0→+1920(内容向右) // 与 B 景 +960→0(向左)对撞,读作两条片子硬拼。 const pushX = interpolate(frame, [26, 36], [0, -A_W], { extrapolateLeft: 'clamp', extrapolateRight: 'clamp', easing: Easing.bezier(0.45, 0, 0.2, 1), }); // 暗场:32→40 从 0→1(进入暗场),42→50 1→0(让 B 景迎入可见)。 // 淡入必须压着 A 段推出的尾巴(f34–35 A 景已基本出画)——从 f36 才起 // 会先漏 2f 纯黑死帧;不纯黑——保留微渐变 + 尘点,让"still moving"可感 const tunnelVis = interpolate(frame, [32, 40, 42, 50], [0, 1, 1, 0], { extrapolateLeft: 'clamp', extrapolateRight: 'clamp', easing: Easing.bezier(0.4, 0, 0.6, 1), }); // B 景迎面放大入场:42→52,从 scale 0.6 + blur 起步收焦,同时沿 // 与 A 相同的运动轴(X 向右)继续移动——出与入同向、速度连续, // 读作"穿过暗场直航"而不是两景各自动自己的。 const bIn = interpolate(frame, [42, 52], [0, 1], { extrapolateLeft: 'clamp', extrapolateRight: 'clamp', easing: Easing.bezier(0.3, 0, 0.2, 1), }); const bScale = 0.6 + 0.4 * bIn; const bBlur = (1 - bIn) * 12; // 从右缘滑入(+960→0),屏幕空间向左——与 A 段前景向左出画同向, // 相机就是一路向右的一条 take。 const bShiftX = (1 - bIn) * 960; // 尘点:确定性 index 派生(渲染必须可复现)。相机一路向右, // 世界固定的尘点在屏幕空间向左掠过——漂移方向必须与两景同向 const DUST = Array.from({ length: 4 }, (_, i) => ({ x: 780 + i * 320, y: 300 + ((i * 137) % 480), drift: 6 + (i % 3) * 4, // px/frame,缓慢漂移表示"still moving" })); return ( {/* A 景(推出段) */} {frame < 42 ? ( ) : null} {/* 暗场:微渐变 + 尘点,非纯黑死场 */} {frame >= 32 && frame < 50 ? ( {DUST.map((d, i) => ( ))} ) : null} {/* B 景:沿 A 的运动轴继续向右滑入(自左缘)+ 迎面放大 + 收焦 */} {frame >= 42 ? ( ) : null} ); }; export const DarkTunnelTransition: React.FC = () => ( ); // ===== demos/transition/shot-transitions/FocusHandoffTransition.tsx ===== // C 式 虚焦接力(focus-handoff)——前景滑出焦平面(blur 渐深)同时后景反向 // 收焦入场,焦点当剪辑点。同页面内区块→区块 / 文档长页游览的分段转场。 // 参考实现(真实纹理):A/B 都取自**同一张** projects-full 长页——A 景对准 // 上部网格区(cards,y≈180 起),B 景对准**同一页的下半部**(Perception & // Sensing / Research Infra 区块,y≈900 起),模拟"游览同一长页、区块接力"。 // 节拍:0–24 A hold → 24–40 A 淡出 + blur 0→8px 推出,同时 26–42 B 从 8px→0 // 收焦 + 淡入 + 反向滑入(错开 2f 起跑——同帧起跑读作整屏糊掉);两景在交叉 // 窗口内互为焦点交换 → 42–120 B hold(真静止)。浅景深语言先立后用。 import { AbsoluteFill, Img, interpolate, staticFile, useCurrentFrame, Easing } from 'remotion'; export const FOCUSHANDOFF_DUR = 120; const PAGE_H = 1746; // projects-full 的 CSS 高(layout.projects.pageH) // A 景观察窗:对准上部网格区(viewport 顶对页面 y=180) const A_VIEW_Y = -180; // B 景观察窗:同一页面的下半部(viewport 顶对页面 y=900,显示 // Perception & Sensing / Research Infra 区块),viewport 底不超页底 const B_VIEW_Y = -900; const Scene: React.FC = () => { const frame = useCurrentFrame(); // A 景:24→40 淡出 + blur 加深 + 略推出(前景滑出焦平面) const aOut = interpolate(frame, [24, 40], [0, 1], { extrapolateLeft: 'clamp', extrapolateRight: 'clamp', easing: Easing.bezier(0.45, 0, 0.3, 1), }); const aBlur = aOut * 8; const aDrift = aOut * 60; // 轻微左移,读作"滑出"而非原地淡 // B 景:26→42 收焦入场(错开 2f 起跑),8px→0 + 淡入 + 反向滑入 const bIn = interpolate(frame, [26, 42], [0, 1], { extrapolateLeft: 'clamp', extrapolateRight: 'clamp', easing: Easing.bezier(0.3, 0, 0.2, 1), }); const bBlur = (1 - bIn) * 8; const bDrift = (1 - bIn) * -50; // 反向(从右滑入) return ( {/* A 景:同一页的上半部(网格区) */} {/* B 景(上层):同一页的下半部(sections 区),交叉窗口内收焦 */} ); }; export const FocusHandoffTransition: React.FC = () => ( ); // ===== demos/transition/shot-transitions/MaskWipeReal.tsx ===== // mask-wipe 元素遮罩擦除(轮 D)——真实项目卡(card4-hires)放大成 // 全屏窗口,projects 全景从窗内长出接管:"点开一张卡进入它的世界"。 // 节拍:0–40 全景 hold → 40–85 卡放大成窗(窗内新景反向补偿)→ 85–120 hold。 import { AbsoluteFill, Img, interpolate, staticFile, useCurrentFrame, Easing } from 'remotion'; import layout from '../../_textures/live-layout.json'; export const MASKWIPE_DUR = 120; const C4 = layout.projects.cards[3]; // 页面空间 x=781,y=616,w=357,h=312 const VIEW_Y = 180; // 全景观察窗:页面 y=180 起 export const MaskWipeReal: React.FC = () => { const frame = useCurrentFrame(); const t = interpolate(frame, [40, 85], [0, 1], { extrapolateLeft: 'clamp', extrapolateRight: 'clamp', easing: Easing.bezier(0.5, 0, 0.2, 1), }); // 卡在屏幕空间的初始 geometry(全景视口下) const g0 = { x: C4.x, y: C4.y - VIEW_Y, w: C4.w, h: C4.h, r: 12 }; const x = interpolate(t, [0, 1], [g0.x, 0]); const y = interpolate(t, [0, 1], [g0.y, 0]); const w = interpolate(t, [0, 1], [g0.w, 1920]); const h = interpolate(t, [0, 1], [g0.h, 1080]); const r = interpolate(t, [0, 1], [g0.r, 0]); // 窗内新景(detail 视角用 papers 页顶部模拟):从 0.42 缩放反向长到 1 const innerScale = interpolate(t, [0, 1], [0.42, 1]); return ( {/* 背景:projects 全景 */} {/* 卡片即窗口 */} 0 && t < 1 ? '0 16px 56px rgba(31,28,23,0.25)' : 'none', }} > {/* 窗内新景:projects 页深部(detail 语义),反向补偿铺满 */} {/* 卡片脸:随放大渐隐露出窗内景 */} ); }; // ===== demos/transition/shot-transitions/PortalWipeV2.tsx ===== import React from 'react'; import { AbsoluteFill, Easing, interpolate, useCurrentFrame } from 'remotion'; import { FakeDashboard, Card, TitleBlock, G } from '../../_fixtures/Fixtures'; // portal-wipe 穿窗入景·改〔批次 1 重做〕 // 批次 1 弱点:3 层 7 卡散开幅度大(0.85)+blur,穿窗后画面碎读不清。 // 本版:窗放大 bezier(0.7,0,0.3,1) 40f 先慢后快;窗内只 2 层 // (远景整页 dashboard 缩略 + 近景 2 张卡),散开系数近 0.3 / 远 0.08, // 不加 blur;穿窗完成(f65)后所有层 8f 内缓停(f73),之后静止 hold 77f 读清新场景。 // // 节拍(150f @30fps): // 0–25 hold:旧 dashboard 建立,目标卡原位可见 // 25–65 窗放大 40f:卡放大成全屏窗,窗内新场景随之显形 // 65–73 缓停 8f:近/远两层视差余势收干(Easing.out 自然归零) // 73–150 静止 hold:新场景完整可读 export const PortalWipeV2: React.FC = () => { const frame = useCurrentFrame(); // ── 窗放大:40f,先慢后快再缓收 ── const t = interpolate(frame, [25, 65], [0, 1], { extrapolateLeft: 'clamp', extrapolateRight: 'clamp', easing: Easing.bezier(0.7, 0, 0.3, 1), }); const c0 = { x: 1180, y: 620, w: 480, h: 320, r: 14 }; const x = interpolate(t, [0, 1], [c0.x, 0]); const y = interpolate(t, [0, 1], [c0.y, 0]); const w = interpolate(t, [0, 1], [c0.w, 1920]); const h = interpolate(t, [0, 1], [c0.h, 1080]); const r = interpolate(t, [0, 1], [c0.r, 0]); // ── 窗内视差散开:40→73f,Easing.out 保证 f65 穿窗完成后 8f 内速度归零 ── const spread = interpolate(frame, [40, 73], [0, 1], { extrapolateLeft: 'clamp', extrapolateRight: 'clamp', easing: Easing.out(Easing.cubic), }); // 窗内整体从缩略推到满幅 const innerScale = interpolate(t, [0, 1], [0.42, 1]); return ( {/* 旧场景:dashboard A */} {/* 窗(放大的卡)——内藏新场景 */} {/* 窗内 1920×1080 舞台,随穿窗从 0.42 推到 1 */} {/* 远景层(系数 0.08,不加 blur):新场景整页 dashboard 缩略 */} {/* 近景层(系数 0.3,不加 blur):只 2 张卡,向边缘让位 */} {/* 卡正面:放大初期渐隐,露出窗内新场景 */} ); }; // ===== demos/transition/shot-transitions/WhipBrakeReal.tsx ===== // E 式急刹款 whip-brake——真实卡片切片横向长廊,甩过 9 张卡后 // 在目标卡(card4-hires,高清纹理)前急刹长尾滑入。 // 速率:前 70% 路程用 12f(糊),后 30% 路程 ease-out 48f 长尾。 // 节拍:0–30 起点 hold → 30–42 全速甩 → 42–90 急刹滑入 → 90–130 真静止。 import { AbsoluteFill, Img, interpolate, staticFile, useCurrentFrame, Easing } from 'remotion'; import { CameraMotionBlur } from '@remotion/motion-blur'; import layout from '../../_textures/live-layout.json'; export const WHIPBRAKE_DUR = 130; const RAIL = layout.projects.cards.slice(0, 9); // 9 张真实卡依次排开 const GAP = 60; const CARD_W = 460; const TARGET_I = 9; // 第 10 位:card4-hires 目标卡 const TOTAL = (TARGET_I + 1) * (CARD_W + GAP); // 终点:目标卡居中(120 = 导轨起始偏移) const END = 120 + TARGET_I * (CARD_W + GAP) + CARD_W / 2 - 960; const Scene: React.FC = () => { const frame = useCurrentFrame(); // 前 70% 路程 12f,后 30% 路程 48f ease-out 长尾 const dx = interpolate(frame, [30, 42, 90], [0, END * 0.7, END], { extrapolateLeft: 'clamp', extrapolateRight: 'clamp', easing: Easing.out(Easing.cubic), }); return ( {RAIL.map((c, k) => ( ))} {/* 目标卡:高清纹理,急刹落点 */} ); }; export const WhipBrakeReal: React.FC = () => ( // 峰值 ~710px/f:采样密度按"回波间距 ≤ 字高"配,20 samples 拖影连续 ); // ===== demos/transition/shot-transitions/WhipPanReal.tsx ===== // E 式基本款 whip-pan——真实整页冻结纹理(demos/_textures/)。 // 世界:projects-full 顶部 section(A 景)与同页底部 section(B 景)横向并排 // ——模拟"功能段之间的区块交棒",相机 8f 甩 2880px(峰值 ~540px/f), // blur 只包甩动段。节拍:0–35 A hold → 35–43 甩(糊)→ 43–120 B hold(真静止)。 import { AbsoluteFill, Img, interpolate, staticFile, useCurrentFrame, Easing } from 'remotion'; import { CameraMotionBlur } from '@remotion/motion-blur'; export const WHIPPAN_DUR = 120; const SWING = 2880; // 1.5 屏 // 观察窗都对准 cards 网格区(页面空间 y≈247 起),viewport 顶对 y=180 const VIEW_Y = -180; const Scene: React.FC = () => { const frame = useCurrentFrame(); const dx = interpolate(frame, [35, 43], [0, SWING], { extrapolateLeft: 'clamp', extrapolateRight: 'clamp', easing: Easing.bezier(0.6, 0, 0.4, 1), }); return ( {/* A 景:项目板全满 */} {/* 缝隙纸色空场 960px——甩动中段一晃而过,全糊 */} {/* B 景:同页底部 section(PERCEPTION & SENSING 区),换景可辨且内容饱满 */} ); }; export const WhipPanReal: React.FC = () => ( // 峰值 ~540px/f:采样按"回波间距 ≤ 字高"配,20 samples 拖影连续 );