Motion · Transição
Shot transitions six
Six shot hand-off techniques — flash-cut, fly-through, focus relay, black card, whip-pan, mask-wipe — chosen by energy delta.
Fonte: video-shotcraft (Vincent Wei) ↗ · Apache-2.0 · Composição Remotion (React/TSX) — vídeo de prévia, sem player no navegador
Variantes
前镜相机推进,切点跨骑一层白闪盖住换页
交互穿透、能量相近的页面→页面(库内已有,收编)
相机顺运动方向推出前景→纯暗场滑行几帧→后景从景深迎面放大而来,一条 take 不切
高能量→高能量的场景跳转;暗色调全片的主力转场
前景滑出焦平面(blur 渐深)同时后景反向收焦入场,焦点当剪辑点
同页面内区块→区块;文档/长页游览的分段
前镜收尾淡入黑场,字卡打字机/压印出现,再交棒后镜
章节级分段 + 呼吸位二合一(paper-title-card 的暗场变体)
机位一拍横甩到下一场景,中段运动模糊糊到不可辨,借糊帧换景
快节奏功能段连打(高能量→高能量的轻快款,比 B 式省帧提速)
页面内真实元素(卡片)放大成全屏窗口,新场景从窗内长出接管
语义化接缝:功能总览→该功能详情("点开这张卡进入它的世界")
When to use
Any seam between two shots (a technique card: once shots are laid out at the storyboard stage, pick one style per seam)
Intention
Every other shot card in the library only handles its own entrance and exit — how shots actually "connect" has never had a vocabulary. A recognized, excellent launch film (Linear Releases/Agent, frame-by-frame reverse-engineered 2026-07-11) never uses a single hard cut throughout: every scene change relies on either a depth journey, a focus relay, or a black title card to build momentum.
Duration & energy
- Duration: n/a (a technique card; each style's frame cost comes from its parameter sheet, carved out of the neighboring shots' budget)
- Energy: n/a (a technique card, doesn't occupy an energy slot)
Shot recipe (original Chinese — timing, easing, parameter table, known pitfalls)
意图
库内各镜头卡只管自己的入场和收尾,镜与镜"怎么接"一直没有词汇—— 接缝裸切会把逐镜头攒出的电影感一次漏光。公认优秀的发布片 (Linear Releases/Agent,2026-07-11 抽帧逆向)全程没有一次裸切: 换景要么靠深度旅行、要么靠焦点接力、要么靠黑场字卡蓄力。
六式选型
| 式 | 做法 | 适用接缝 | |----|------|----------| | A 推进流白 flash-cut | 前镜相机推进,切点跨骑一层白闪盖住换页 | 交互穿透、能量相近的页面→页面(库内已有,收编)| | B 穿暗场直航 | 相机顺运动方向推出前景→纯暗场滑行几帧→后景从景深迎面放大而来,一条 take 不切 | 高能量→高能量的场景跳转;暗色调全片的主力转场 | | C 虚焦接力 | 前景滑出焦平面(blur 渐深)同时后景反向收焦入场,焦点当剪辑点 | 同页面内区块→区块;文档/长页游览的分段 | | D 黑场字卡 | 前镜收尾淡入黑场,字卡打字机/压印出现,再交棒后镜 | 章节级分段 + 呼吸位二合一(paper-title-card 的暗场变体)| | E whip-pan 甩镜 | 机位一拍横甩到下一场景,中段运动模糊糊到不可辨,借糊帧换景 | 快节奏功能段连打(高能量→高能量的轻快款,比 B 式省帧提速)| | F mask-wipe 穿窗 | 页面内真实元素(卡片)放大成全屏窗口,新场景从窗内长出接管 | 语义化接缝:功能总览→该功能详情("点开这张卡进入它的世界")|
参数表
| 参数 | 典型值 | 调节手感 | |------|--------|----------| | A flash-cut | assets/lib/FlashCut.tsx,from = 切点 − 5,两侧各 5f | 用法详见 type-and-filter 卡;白闪只盖硬切,不当装饰光效(Q4)| | B 暗场滑行 | 前镜推出 8–12f → 暗场 4–8f → 后景放大入场 10–14f;后景从 scale ~0.6 + blur 起步收焦 | 暗场段不能全黑死场——保留背景微渐变/尘点让"still moving"可感;滑行帧过长读作断片,>10f 须有元素在动;推出/迎入的快速运镜可包 <CameraMotionBlur>(参数见 deck-deal-flyin 运动模糊行) | | B 运动方向 | 出与入同向(前镜向右推出→后景从右迎来),速度曲线连续不回摆 | 方向不接读作两条片子硬拼;参考片 Releases 全程一条 take 的秘密就是方向连续 | | C 焦点交换 | 前景 blur 0→8px 同时后景 8px→0,交叉窗口 10–16f;两景错开 2–4f 起跑 | 同帧起跑读作整屏糊掉;浅景深要贯穿该段落而非突然出现(DoF 语言先立后用)| | D 黑场字卡 | 前镜淡出 6–10f → 字卡段(参数走 paper-title-card,底色换暗场)→ 淡入后镜 6–10f | 暗底字卡的字色用页面底色(浅色)/等宽字才不像报错弹窗;一支 30s 片 D 式 ≤2 次,多了节奏碎 | | E 甩镜基本款 | 两端各 hold ≥20f → 甩动 8f 跨约 1.5 屏(ease-in-out 陡曲线)→ 直接落位;<CameraMotionBlur shutterAngle={200} samples={8}> 只包甩动段 | 跨度按"峰值速度 ≥300px/f"配,不足则糊不透露出换景穿帮;两景同一水平线上拼接(页面空间同 y),甩完即停 | | E 甩镜急刹款 | 前 70% 路程用 30% 时间(糊段)→ 后 30% 路程 ease-out 长尾 ~48f 滑入落位 | 适合落点是"要读内容"的镜头——长尾给观众视线一个追上的坡;长尾 >60f 偷走后镜入场,宁短勿长 | | F 穿窗 | 元素 45f 放大成全屏(单段 bezier),窗内新景从 ~0.42 缩放反向补偿长到 1,元素脸前半段渐隐(opacity 1−t·2.2) | 窗几何与窗内景必须同步驱动(同一个 t),异步读作穿帮;被放大元素用高清纹理(Q2,全程被特写)| | F 穿窗纵深款 portal-wipe | 窗放大 40f 用 bezier(0.7,0,0.3,1) 先慢后快;窗内只 2 层视差(远景整页缩略系数 0.08 + 近景 2 卡 0.3,不加 blur),穿窗完成后 8f 缓停 → 静止 ≥30f 读清新景 | 层数/散开是可读性命门——3 层+0.85 散开+blur 版被裁,收敛后过;穿完必须真停,视差飘着不停偷走后镜阅读 |
已知坑
- 分镜表阶段就逐接缝标注转场式(写进"关键动效"列),逐镜头阶段才补
等于全线挪帧——转场占用的帧从相邻镜头预算划走,规矩同 hold 预算(R3)
- B/C 两式借鉴自 Linear 发布片抽帧逆向(2026-07-11,进化轮 #3),
非用户判例——是默认建议不是令,与判例冲突时判例优先
- B 式对源素材要求高:后景迎面放大 = 纹理被放大看,低倍截图先过
审美准则 Q2 的高分辨率栅格化技法再上
- 转场不叠加:一个接缝只用一式;B 式暗场里再加 flash 白闪读作穿帮
- E/F 式参数经占位素材调校+真实素材转正,非用户
实战判例——单例判例,与判例冲突时判例优先;E 基本款/急刹款按 后镜性质二选一(后镜自带入场动效→基本款;后镜靠内容说话→急刹款), 同片混用两款算两式各记一次
- F 式窗内新景语义应是被点开元素的"详情世界"(实战接真实详情页
截图)——窗里塞无关场景语义断裂,沦为花哨擦除
- F 纵深款参数系灰阶占位调校起点、未经真实素材验证;
参考实现 demos/transition/shot-transitions/PortalWipeV2.tsx
参考实现
A 式:template/src/aifl/Main.tsx flash-cut 层(组件 assets/lib/FlashCut.tsx); B 式(穿暗场直航):demos/transition/shot-transitions/DarkTunnelTransition.tsx; C 式(虚焦接力):demos/transition/shot-transitions/FocusHandoffTransition.tsx; D 式(黑场字卡):demos/transition/shot-transitions/BlackCardTransition.tsx; E 式(whip 甩镜):demos/transition/shot-transitions/WhipPanReal.tsx / WhipBrakeReal.tsx; F 式(遮罩擦除):demos/transition/shot-transitions/MaskWipeReal.tsx(纵深款 PortalWipeV2.tsx 同目录)。
Reference implementation (Remotion)
- demos/transition/shot-transitions/BlackCardTransition.tsx — https://raw.githubusercontent.com/Vincentwei1021/video-shotcraft/main/demos/transition/shot-transitions/BlackCardTransition.tsx
- demos/transition/shot-transitions/DarkTunnelTransition.tsx — https://raw.githubusercontent.com/Vincentwei1021/video-shotcraft/main/demos/transition/shot-transitions/DarkTunnelTransition.tsx
- demos/transition/shot-transitions/FocusHandoffTransition.tsx — https://raw.githubusercontent.com/Vincentwei1021/video-shotcraft/main/demos/transition/shot-transitions/FocusHandoffTransition.tsx
- demos/transition/shot-transitions/MaskWipeReal.tsx — https://raw.githubusercontent.com/Vincentwei1021/video-shotcraft/main/demos/transition/shot-transitions/MaskWipeReal.tsx
- demos/transition/shot-transitions/PortalWipeV2.tsx — https://raw.githubusercontent.com/Vincentwei1021/video-shotcraft/main/demos/transition/shot-transitions/PortalWipeV2.tsx
- demos/transition/shot-transitions/WhipBrakeReal.tsx — https://raw.githubusercontent.com/Vincentwei1021/video-shotcraft/main/demos/transition/shot-transitions/WhipBrakeReal.tsx
- demos/transition/shot-transitions/WhipPanReal.tsx — https://raw.githubusercontent.com/Vincentwei1021/video-shotcraft/main/demos/transition/shot-transitions/WhipPanReal.tsx
The component imports shared fixtures from demos/_fixtures and helpers from assets/lib in the repo; SKILL.md there documents the render workflow.
Tudo nesta página também é legível por máquina. Aponte seu agente para o JSON ou adicione o servidor MCP do gimgs e peça para buscar templates de motion.
- Baixe o JSON — contém a receita do plano (timing, easing, tabela de parâmetros, armadilhas) e o código TSX completo.
- Clone o repositório video-shotcraft; o componente importa fixtures de demos/_fixtures e helpers de assets/lib.
- Registre o componente como <Composition> em um projeto Remotion e renderize com `npx remotion render`, ou reimplemente a receita no seu próprio engine.
curl -s https://gimgs.net/motion/shot-transitions.jsongit clone https://github.com/Vincentwei1021/video-shotcraft
# component: demos/transition/shot-transitions/BlackCardTransition.tsx, demos/transition/shot-transitions/DarkTunnelTransition.tsx, demos/transition/shot-transitions/FocusHandoffTransition.tsx, demos/transition/shot-transitions/MaskWipeReal.tsx, demos/transition/shot-transitions/PortalWipeV2.tsx, demos/transition/shot-transitions/WhipBrakeReal.tsx, demos/transition/shot-transitions/WhipPanReal.tsx
npx remotion render <CompositionId> out.mp4claude mcp add --transport http gimgs https://gimgs.net/mcp
# then: search_motion_templates / get_motion_template("shot-transitions")Prompt
Use the video-shotcraft shot recipe "Shot transitions six" (Remotion composition, Apache-2.0) published on gimgs.net. Recipe JSON (prompt = timing/easing/parameter table/pitfalls, files = TSX source): https://gimgs.net/motion/shot-transitions.json Raw TSX: https://gimgs.net/motion/shot-transitions/template.html Repository: https://github.com/Vincentwei1021/video-shotcraft (component: demos/transition/shot-transitions/BlackCardTransition.tsx, demos/transition/shot-transitions/DarkTunnelTransition.tsx, demos/transition/shot-transitions/FocusHandoffTransition.tsx, demos/transition/shot-transitions/MaskWipeReal.tsx, demos/transition/shot-transitions/PortalWipeV2.tsx, demos/transition/shot-transitions/WhipBrakeReal.tsx, demos/transition/shot-transitions/WhipPanReal.tsx; imports demos/_fixtures and assets/lib) Spec: 960x540, 30 fps, 1.43s 1. Read the JSON and follow its "prompt" section (when to use, intention, parameter table, known pitfalls). 2. Adapt it for: <describe what you want>. 3. Either clone the repo, register the component as a <Composition> in a Remotion project and run npx remotion render <CompositionId> out.mp4 — or re-implement the recipe in your own engine (HTML/WAAPI works: keep the same timing and easing).
// ===== 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 (
<AbsoluteFill style={{ backgroundColor: '#0c0c10', overflow: 'hidden' }}>
{/* A 景 */}
{frame < 14 ? (
<div style={{ position: 'absolute', opacity: aOut }}>
<Img
src={staticFile('textures/live/projects-full.png')}
style={{ position: 'absolute', left: 0, top: A_VIEW_Y, width: 1920 }}
/>
</div>
) : null}
{/* 黑场字卡(18–74) */}
{frame >= 18 && frame < 74 ? (
<AbsoluteFill
style={{
justifyContent: 'center', alignItems: 'center',
opacity: cardOpacity * (1 - cardOut), pointerEvents: 'none',
}}
>
<div style={{ textAlign: 'center', maxWidth: 1500 }}>
<div
style={{
fontFamily: SERIF, fontSize: 96, fontWeight: 600, lineHeight: 1.16,
color: PAPER_LIGHT, letterSpacing: '-0.012em',
display: 'flex', flexWrap: 'wrap', justifyContent: 'center', columnGap: '0.26em',
}}
>
{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 (
<span
key={i}
style={{
opacity: t, transform: `scale(${1.3 - 0.3 * t})`,
filter: `blur(${(1 - t) * 7}px)`, display: 'inline-block',
fontStyle: w.accent ? 'italic' : 'normal',
color: w.accent ? AMBER : undefined,
}}
>
{w.text}
</span>
);
})}
</div>
<div
style={{
height: 4, width: 180, margin: '30px auto 0', borderRadius: 2,
background: AMBER, transform: `scaleX(${interpolate(frame, [32, 44], [0, 1], { extrapolateLeft: 'clamp', extrapolateRight: 'clamp', easing: Easing.bezier(0.3, 0, 0.2, 1) })})`,
}}
/>
<div
style={{
fontFamily: MONO, fontSize: 20, letterSpacing: '0.14em', color: DIM,
marginTop: 26, textTransform: 'uppercase',
opacity: interpolate(frame, [26, 36], [0, 1], { extrapolateLeft: 'clamp', extrapolateRight: 'clamp' }),
}}
>
Weekly Brief · 2026-W28
</div>
</div>
</AbsoluteFill>
) : null}
{/* B 景(70 起淡入) */}
{frame >= 70 ? (
<div style={{ position: 'absolute', opacity: bIn }}>
<Img
src={staticFile('textures/live/wbr-full.png')}
style={{ position: 'absolute', left: 0, top: 0, width: 1920, height: 1080 }}
/>
</div>
) : null}
</AbsoluteFill>
);
};
export const BlackCardTransition: React.FC = () => (
<Scene />
);
// ===== 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 (
<AbsoluteFill style={{ backgroundColor: '#0d0d10', overflow: 'hidden' }}>
{/* A 景(推出段) */}
{frame < 42 ? (
<div style={{ position: 'absolute', transform: `translateX(${pushX}px)` }}>
<Img
src={staticFile('textures/live/projects-full.png')}
style={{ position: 'absolute', left: 0, top: A_VIEW_Y, width: A_W }}
/>
</div>
) : null}
{/* 暗场:微渐变 + 尘点,非纯黑死场 */}
{frame >= 32 && frame < 50 ? (
<AbsoluteFill style={{ opacity: tunnelVis, pointerEvents: 'none' }}>
<div
style={{
position: 'absolute', inset: 0,
background: 'radial-gradient(900px 500px at 50% 45%, rgba(40,42,50,0.9), rgba(12,12,16,1) 70%)',
}}
/>
{DUST.map((d, i) => (
<div
key={i}
style={{
position: 'absolute', left: d.x - frame * d.drift, top: d.y,
width: 2, height: 2, borderRadius: '50%',
background: 'rgba(120,125,140,0.35)',
}}
/>
))}
</AbsoluteFill>
) : null}
{/* B 景:沿 A 的运动轴继续向右滑入(自左缘)+ 迎面放大 + 收焦 */}
{frame >= 42 ? (
<div
style={{
position: 'absolute', left: 0, top: 0, width: 1920, height: 1080,
transform: `translateX(${bShiftX}px) scale(${bScale})`, filter: `blur(${bBlur}px)`,
transformOrigin: '50% 45%',
}}
>
<Img
src={staticFile('textures/live/wbr-full.png')}
style={{ position: 'absolute', left: 0, top: 0, width: 1920, height: 1080 }}
/>
</div>
) : null}
</AbsoluteFill>
);
};
export const DarkTunnelTransition: React.FC = () => (
<Scene />
);
// ===== 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 (
<AbsoluteFill style={{ backgroundColor: '#faf7f2', overflow: 'hidden' }}>
{/* A 景:同一页的上半部(网格区) */}
<div style={{ position: 'absolute', opacity: 1 - aOut, filter: `blur(${aBlur}px)`, transform: `translateX(${-aDrift}px)` }}>
<Img
src={staticFile('textures/live/projects-full.png')}
style={{ position: 'absolute', left: 0, top: A_VIEW_Y, width: 1920, height: PAGE_H }}
/>
</div>
{/* B 景(上层):同一页的下半部(sections 区),交叉窗口内收焦 */}
<div style={{ position: 'absolute', opacity: bIn, filter: `blur(${bBlur}px)`, transform: `translateX(${bDrift}px)` }}>
<Img
src={staticFile('textures/live/projects-full.png')}
style={{ position: 'absolute', left: 0, top: B_VIEW_Y, width: 1920, height: PAGE_H }}
/>
</div>
</AbsoluteFill>
);
};
export const FocusHandoffTransition: React.FC = () => (
<Scene />
);
// ===== 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 (
<AbsoluteFill style={{ backgroundColor: '#f9f6f1', overflow: 'hidden' }}>
{/* 背景:projects 全景 */}
<Img
src={staticFile('textures/live/projects-full.png')}
style={{ position: 'absolute', left: 0, top: -VIEW_Y, width: 1920 }}
/>
{/* 卡片即窗口 */}
<div
style={{
position: 'absolute', left: x, top: y, width: w, height: h,
borderRadius: r, overflow: 'hidden',
boxShadow: t > 0 && t < 1 ? '0 16px 56px rgba(31,28,23,0.25)' : 'none',
}}
>
{/* 窗内新景:projects 页深部(detail 语义),反向补偿铺满 */}
<div
style={{
position: 'absolute', width: 1920, height: 1080,
left: '50%', top: '50%',
transform: `translate(-50%, -50%) scale(${innerScale})`,
overflow: 'hidden', background: '#f9f6f1',
}}
>
<Img
src={staticFile('textures/live/projects-full.png')}
style={{ position: 'absolute', left: 0, top: -846, width: 1920 }}
/>
</div>
{/* 卡片脸:随放大渐隐露出窗内景 */}
<Img
src={staticFile('textures/live/card4-hires.png')}
style={{
position: 'absolute', inset: 0, width: '100%', height: '100%',
opacity: Math.max(0, 1 - t * 2.2),
}}
/>
</div>
</AbsoluteFill>
);
};
// ===== 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 (
<AbsoluteFill style={{ background: G.bg, overflow: 'hidden' }}>
{/* 旧场景:dashboard A */}
<FakeDashboard variant="A" />
{/* 窗(放大的卡)——内藏新场景 */}
<div
style={{
position: 'absolute',
left: x,
top: y,
width: w,
height: h,
borderRadius: r,
overflow: 'hidden',
boxShadow: t < 1 ? '0 12px 48px rgba(0,0,0,0.22)' : 'none',
}}
>
{/* 窗内 1920×1080 舞台,随穿窗从 0.42 推到 1 */}
<div
style={{
position: 'absolute',
width: 1920,
height: 1080,
left: '50%',
top: '50%',
transform: `translate(-50%, -50%) scale(${innerScale})`,
background: '#e4e4e2',
}}
>
{/* 远景层(系数 0.08,不加 blur):新场景整页 dashboard 缩略 */}
<div
style={{
position: 'absolute',
inset: 0,
transform: `scale(${1 + spread * 0.08})`,
transformOrigin: '960px 540px',
}}
>
<div
style={{
position: 'absolute',
left: '50%',
top: '50%',
width: 1920,
height: 1080,
transform: 'translate(-50%, -50%) scale(0.82)',
transformOrigin: 'center',
borderRadius: 20,
overflow: 'hidden',
border: `2px solid ${G.border}`,
boxShadow: '0 8px 32px rgba(0,0,0,0.10)',
}}
>
<FakeDashboard variant="B" />
</div>
<div style={{ position: 'absolute', left: 250, top: 100 }}>
<TitleBlock text="Scene B" size={64} />
</div>
</div>
{/* 近景层(系数 0.3,不加 blur):只 2 张卡,向边缘让位 */}
<div
style={{
position: 'absolute',
inset: 0,
transform: `scale(${1 + spread * 0.3})`,
transformOrigin: '960px 540px',
}}
>
<div style={{ position: 'absolute', left: 150, top: 660 }}>
<Card w={380} h={250} seed={71} />
</div>
<div style={{ position: 'absolute', left: 1420, top: 160 }}>
<Card w={340} h={220} seed={72} />
</div>
</div>
</div>
{/* 卡正面:放大初期渐隐,露出窗内新场景 */}
<div style={{ position: 'absolute', inset: 0, opacity: Math.max(0, 1 - t * 2.4) }}>
<Card w={c0.w} h={c0.h} seed={5} style={{ width: '100%', height: '100%' }} />
</div>
</div>
</AbsoluteFill>
);
};
// ===== 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 (
<AbsoluteFill style={{ backgroundColor: '#f9f6f1', overflow: 'hidden' }}>
<div style={{ position: 'absolute', top: 340, transform: `translateX(${-dx}px)` }}>
{RAIL.map((c, k) => (
<Img
key={c.file + k}
src={staticFile(`textures/live/${c.file}`)}
style={{
position: 'absolute', left: 120 + k * (CARD_W + GAP), top: 0,
width: CARD_W, borderRadius: 12,
boxShadow: '0 4px 16px rgba(31,28,23,0.10)',
}}
/>
))}
{/* 目标卡:高清纹理,急刹落点 */}
<Img
src={staticFile('textures/live/card4-hires.png')}
style={{
position: 'absolute', left: 120 + TARGET_I * (CARD_W + GAP), top: -20,
width: CARD_W, borderRadius: 12,
boxShadow: '0 10px 32px rgba(31,28,23,0.18)',
}}
/>
</div>
</AbsoluteFill>
);
};
export const WhipBrakeReal: React.FC = () => (
// 峰值 ~710px/f:采样密度按"回波间距 ≤ 字高"配,20 samples 拖影连续
<CameraMotionBlur shutterAngle={200} samples={20}>
<Scene />
</CameraMotionBlur>
);
// ===== 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 (
<AbsoluteFill style={{ backgroundColor: '#f9f6f1', overflow: 'hidden' }}>
<div style={{ position: 'absolute', transform: `translateX(${-dx}px)` }}>
{/* A 景:项目板全满 */}
<Img
src={staticFile('textures/live/projects-full.png')}
style={{ position: 'absolute', left: 0, top: VIEW_Y, width: 1920 }}
/>
{/* 缝隙纸色空场 960px——甩动中段一晃而过,全糊 */}
{/* B 景:同页底部 section(PERCEPTION & SENSING 区),换景可辨且内容饱满 */}
<Img
src={staticFile('textures/live/projects-full.png')}
style={{ position: 'absolute', left: SWING, top: -666, width: 1920 }}
/>
</div>
</AbsoluteFill>
);
};
export const WhipPanReal: React.FC = () => (
// 峰值 ~540px/f:采样按"回波间距 ≤ 字高"配,20 samples 拖影连续
<CameraMotionBlur shutterAngle={200} samples={20}>
<Scene />
</CameraMotionBlur>
);
- Read the recipePrompt = when to use, intention, parameter table with typical values and how each one feels, known pitfalls. Treat values as calibrated starting points.
- Get the codetool: git
git clone https://github.com/Vincentwei1021/video-shotcraft && cd video-shotcraft && npm install - Register + rendertool: remotion
npx remotion render <CompositionId> out.mp4 # component: demos/transition/shot-transitions/BlackCardTransition.tsx, demos/transition/shot-transitions/DarkTunnelTransition.tsx, demos/transition/shot-transitions/FocusHandoffTransition.tsx, demos/transition/shot-transitions/MaskWipeReal.tsx, demos/transition/shot-transitions/PortalWipeV2.tsx, demos/transition/shot-transitions/WhipBrakeReal.tsx, demos/transition/shot-transitions/WhipPanReal.tsx - Or re-implementPort the timing/easing from the recipe to HTML + WAAPI (gimgs motion contract) or any engine; keep the same segment windows and easing.
| param | type | default | description |
|---|---|---|---|
| — | |||
Mais nesta categoria

White flash: logo simplifies
A colorful liquid-gradient wordmark holds with flowing light, then the frame flashes white and overexposes; a flat wordmark fades in on white and settles — one flash completes the downgrade to simplicity.

Geometric wipe transitions
Two geometric wipe transitions — clock-wipe, a radar-hand sweep changing the page, and blinds-slice, 12 vertical strips flipping in staggered waves.

Hidden-cut transitions
Three hidden-cut transitions — foreground occlusion invisible cut, versus-slam opener, and warm light-leak burn; the hard cut hides inside 1-3 frames of occlusion/impact/light peak, so the viewer never sees the scissors.

Travel-through transitions
Two travel-through transitions — shared-element homecoming and letter-cavity pass-through; the camera drills through a real element inside the frame to complete the scene change.