Motion · Transition

Shot transitions six

Six shot hand-off techniques — flash-cut, fly-through, focus relay, black card, whip-pan, mask-wipe — chosen by energy delta.

960×54016:930 fps1.43s · 43 framesremotionLicense: Apache-2.0#shotcraft#remotion#transition

Source: video-shotcraft (Vincent Wei) ↗ · Apache-2.0 · Remotion composition (React/TSX) — preview video, no in-browser player

Variants

flash-cutflash-cut · Push-in white flash
The outgoing shot's camera pushes in, and the cut point straddles a white flash that covers the page change.
Interaction pass-through, page→page with similar energy levels (already in the library, folded into this family)
shot-transitions-4shot-transitions-4 · Deep-space fly-through
The camera pushes out past the foreground along its direction of motion → glides through a few frames of pure darkness → the background zooms in to meet it from the depth of field — a single unbroken take, no cut.
High-energy → high-energy scene jumps; the workhorse transition for a dark-toned film
shot-transitions-5shot-transitions-5 · Rack-focus relay
The foreground slides out of the focal plane (blur deepening) while the background racks into focus from the opposite direction at the same time — focus itself is the cut point.
Section→section within the same page; segmenting a document/long-page tour
shot-transitions-6shot-transitions-6 · Black title card
The outgoing shot fades to black, a title card appears via typewriter/letterpress effect, then hands off to the next shot.
A two-in-one of chapter-level segmentation plus a breathing beat (a dark-mode variant of paper-title-card)
whip-panwhip-pan · Whip pan
The camera whips sideways in one beat to the next scene, blurring into total illegibility mid-motion, using the blurred frames to hide the scene change.
Fast-paced feature sections in rapid succession (a light, quick high-energy→high-energy option, cheaper in frames and faster than style B)
mask-wipemask-wipe · Window wipe
A real element on the page (a card) scales up into a full-screen window, and the new scene grows out of it to take over.
A semantic seam: feature overview → that feature's detail ("click this card to enter its world")

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.

Everything on this page is also machine-readable. Point your agent at the JSON, or add the gimgs MCP server and ask it to search motion templates.

  1. Fetch the JSON — it holds the shot recipe (timing, easing, parameter table, pitfalls) and the full TSX source.
  2. Clone the video-shotcraft repo; the component imports shared fixtures from demos/_fixtures and helpers from assets/lib.
  3. Register the component as a <Composition> in a Remotion project and render with `npx remotion render`, or re-implement the recipe in your own engine.
curl -s https://gimgs.net/motion/shot-transitions.json
git 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.mp4
claude 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).

JSON · Markdown · llms.txt

22,074 chars · Apache-2.0Open raw template ↗
// ===== 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>
);


  1. Read the recipe
    Prompt = when to use, intention, parameter table with typical values and how each one feels, known pitfalls. Treat values as calibrated starting points.
  2. Get the code
    tool: git
    git clone https://github.com/Vincentwei1021/video-shotcraft && cd video-shotcraft && npm install
  3. Register + render
    tool: 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
  4. Or re-implement
    Port the timing/easing from the recipe to HTML + WAAPI (gimgs motion contract) or any engine; keep the same segment windows and easing.
paramtypedefaultdescription
—

More in this category