Motion · Chuyển cảnh
Sáu kiểu bàn giao cảnh quay
Sáu kỹ thuật bàn giao cảnh: flash trắng, xuyên bối cảnh tối, tiếp sức mất nét, thẻ chữ đen, whip-pan, mask-wipe — chọn theo năng lượng.
Nguồn: video-shotcraft (Vincent Wei) ↗ · Apache-2.0 · Composition Remotion (React/TSX) — có video preview, không có player trong trình duyệt
Biến thể
Camera cảnh trước đẩy vào, điểm cắt cưỡi lên một lớp lóe trắng để che việc đổi trang.
Xuyên qua tương tác, trang→trang có năng lượng gần nhau (đã có sẵn trong kho, được thu nạp vào đây)
Camera đẩy qua tiền cảnh theo hướng chuyển động → trượt vài khung hình qua bối cảnh tối thuần túy → hậu cảnh từ chiều sâu phóng to lao tới đón — một take liền mạch, không cắt.
Chuyển cảnh năng lượng cao → năng lượng cao; chuyển cảnh chủ lực cho phim tông tối
Tiền cảnh trượt ra khỏi mặt phẳng lấy nét (blur đậm dần) trong khi hậu cảnh lấy nét ngược chiều tiến vào — tiêu điểm chính là điểm cắt.
Khu vực→khu vực trong cùng một trang; phân đoạn cho hành trình tài liệu/trang dài
Cảnh trước kết thúc mờ vào nền đen, thẻ chữ xuất hiện kiểu máy đánh chữ/in ép, rồi bàn giao cho cảnh sau.
Hai trong một: phân đoạn cấp chương + nhịp thở (biến thể nền tối của paper-title-card)
Góc máy quét ngang một nhịp sang cảnh tiếp theo, giữa chừng mờ chuyển động tới mức không nhận ra, mượn khung hình mờ để đổi cảnh.
Chuỗi đoạn tính năng nhịp nhanh liên tiếp (phiên bản nhẹ nhàng năng lượng cao → năng lượng cao, tiết kiệm khung hình và nhanh hơn kiểu B)
Một yếu tố thật trên trang (thẻ) phóng to thành cửa sổ toàn màn hình, cảnh mới mọc lên từ bên trong cửa sổ và tiếp quản.
Mối nối mang ngữ nghĩa: tổng quan tính năng → chi tiết tính năng đó ("bấm vào thẻ này để bước vào thế giới của nó")
Khi nào dùng
Bất kỳ mối nối nào giữa hai cảnh quay (thẻ kỹ thuật: sau khi xếp xong cảnh ở giai đoạn storyboard, chọn một kiểu cho từng mối nối)
Ý đồ
Mọi thẻ cảnh quay khác trong kho chỉ lo phần vào cảnh và kết cảnh của riêng nó — "nối cảnh với cảnh" thế nào thì chưa từng có từ vựng. Một phim ra mắt xuất sắc được công nhận (Linear Releases/Agent, bóc tách từng khung hình ngày 2026-07-11) không hề có một cú cắt cứng nào suốt phim: đổi cảnh luôn dựa vào hành trình chiều sâu, tiếp sức tiêu điểm, hoặc thẻ chữ nền đen để tích lũy động lượng.
Thời lượng & năng lượng
- Thời lượng: n/a (thẻ kỹ thuật; số khung hình mỗi kiểu chiếm xem bảng tham số, trích từ ngân sách của các cảnh liền kề)
- Năng lượng: n/a (thẻ kỹ thuật, không chiếm vị trí năng lượng)
Công thức cảnh quay (bản gốc tiếng Trung — timing, easing, bảng tham số, lỗi hay gặp)
意图
库内各镜头卡只管自己的入场和收尾,镜与镜"怎么接"一直没有词汇—— 接缝裸切会把逐镜头攒出的电影感一次漏光。公认优秀的发布片 (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 同目录)。
Mã nguồn tham chiếu (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
Component import fixture chung từ demos/_fixtures và helper từ assets/lib của repo; xem SKILL.md trong repo để biết quy trình render.
Mọi thứ trên trang này đều có bản máy đọc. Đưa agent link JSON, hoặc thêm MCP server gimgs rồi bảo nó tìm motion template.
- Tải JSON — trong đó có công thức cảnh quay (timing, easing, bảng tham số, lỗi hay gặp) và toàn bộ source TSX.
- Clone repo video-shotcraft; component import fixture chung từ demos/_fixtures và helper từ assets/lib.
- Đăng ký component thành <Composition> trong project Remotion rồi render bằng `npx remotion render`, hoặc dựng lại công thức bằng engine của bạn.
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 "Sáu kiểu bàn giao cảnh quay" (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 |
|---|---|---|---|
| — | |||
Cùng danh mục

Chớp trắng: logo giản lược
Chữ hiệu gradient lỏng nhiều màu đứng yên chảy sáng, màn hình chớp trắng cháy sáng một nhịp, chữ hiệu phiên bản phẳng trên nền trắng mờ vào định hình — một lần chớp trắng hoàn tất bước giản lược chất liệu.

Chuyển cảnh quét hình học
Hai kiểu chuyển cảnh quét hình học — clock-wipe kim đồng hồ radar quét đổi trang và blinds-slice 12 dải dọc lật lệch nhịp thành sóng.

Chuyển cảnh giấu điểm cắt
Ba kiểu chuyển cảnh giấu điểm cắt — che khuất tiền cảnh cắt vô hình, đối đầu va chạm mở màn, và cháy sáng rò rỉ ánh sáng ấm; cắt cứng giấu trong 1-3 khung che khuất/va chạm/đỉnh sáng, khán giả không thấy kéo cắt.

Chuyển cảnh xuyên qua
Hai kiểu chuyển cảnh xuyên qua — trở về vị trí bằng phần tử chung và xuyên khoang chữ, máy quay khoan xuyên phần tử thực trong khung hình để hoàn tất đổi cảnh.