Motion · Transition

Line carry transition

Scene A's progress bar extends off-frame, the camera pans with the line, and the line turns a corner mid-move to frame scene B's card border, with no cut anywhere.

1920×108016:930 fps5.38s · 161 framesremotionLicense: Apache-2.0#shotcraft#remotion#transition#camera

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

When to use

Between two scenes with graphic kinship (a progress bar → a card frame, an underline → a chart axis); the signature transition slot of a film, the graphic-relay opening of Catch Me If You Can.

Intention

The three transition families in the library all answer "how does the scene change"; this card's answer is it fundamentally doesn't change — one line leads you across. Once the progress bar fills, its end extends into a long line that shoots off the card's edge, the camera pans with the line into the new world, the line turns right angles mid-move to frame scene B's card, the frame closes, content fades in — the audience's eye never leaves the line from start to finish. Graphic continuity is itself the transition. The difference from a match-cut: that aligns shapes on either side of a cut — it's still a cut; this is continuous drawing — there is no cut. The line needs a narrative logic for its identity; a line with no justification is mere decoration.

Duration & energy

  • Duration: ~5.3s (progress bar fills + pan 60f + framing + content fade-in + 36f hold)
  • Energy: Medium

Shot recipe (original Chinese — timing, easing, parameter table, known pitfalls)

意图

转场库三大族(穿越/藏切/体块)都在回答"场景怎么换";本卡的回答是 根本不换——一条线牵着你走过去。进度条走满后末端延伸成长线冲出 卡缘,镜头跟着线横移 1920px 进入新世界,线在移动中拐直角围出 B 场景 的卡框,框闭合、内容淡入,从头到尾观众的眼睛没离开过这条线。图形 连续性本身就是转场。与 match-cut 的区别:那是切点两侧形状对位—— 终究还是切;这是连续绘制——没有切。线的身份要有叙事逻辑(进度条 "走完了"所以延伸出去),无来由的线只是装饰。

动效核心

  • 3840 宽世界容器 translateX 当镜头,34–94f 移 1920px,

Easing.inOut(cubic)

  • 一条手写折线 path(进度条 560 + 冲出段 + 直角 + 560×330 矩形框,

总长 3980)全程 dasharray/dashoffset evolve,分段驱动 drawn 长度

  • 命门:横移段 drawn = 1100 + cam,线生长与镜头位移同速——笔头

钉在画面 x≈1500 偏右,永不出画也永不落后;一旦失速,观众跟丢线 就等于跟丢了转场

  • 笔头 r=11 墨点随线走,框闭合后 112–118f 线性消散并条件卸载

(摘罩判例);B 内容 124f 前淡入完,124–160f 真静止 36f

参数表

| 参数 | 典型值 | 调节手感 | |------|--------|----------| | 线体 | 6px ink 实线 | 太细横移中读丢;虚线读作未完成态 | | 横移配比 | 1920px / 60f(32px/f) | 更远的横移等比拉长时长,别提速——>40px/f 笔头开始拖影跟丢 | | 笔头位置 | 画面 x≈1500(约 78% 处偏右) | 居中读作"线追镜头";贴边一颤就出画 | | 拐角 | 直角硬拐,不倒圆 | 圆角软拐丢掉制图感,Saul Bass 味就没了 | | 框闭合 | 闭合帧起 B 内容再淡入 | 框没闭合内容先出,线的悬念白搭 | | 收尾 | 淡入完 ≥36f 真静止 | 笔头墨点必须条件卸载,残留即毁静止 |

已知坑

  • demo 在灰阶/占位素材上调校通过——参数是调校起点非实战定稿,

首次实战须以真实素材回验

  • 与 draw-svg-trace 的关系:那是元素级描边点名(线属于元素),这是

场景级线条接力(线属于镜头)——同片可共存,但线的语义要一致: 同为 ink 6px 手绘感,或同为几何硬线,混用读作两套美术

  • 全片 ≤1 次——这是招牌转场不是常规转场,第二次出现魔法就穿帮
  • 起点必须是已有图形构件的自然延伸(进度条走满、下划线画完),

凭空起线读作屏保

  • 声音:线生长一声持续笔擦、框闭合一声轻叩(sound-design §4.5)

参考实现

demos/transition/line-carry-transition/ (LineCarryTransition.tsx)

Reference implementation (Remotion)

  • demos/transition/line-carry-transition/LineCarryTransition.tsx — https://raw.githubusercontent.com/Vincentwei1021/video-shotcraft/main/demos/transition/line-carry-transition/LineCarryTransition.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/line-carry-transition.json
git clone https://github.com/Vincentwei1021/video-shotcraft
# component: demos/transition/line-carry-transition/LineCarryTransition.tsx
npx remotion render <CompositionId> out.mp4
claude mcp add --transport http gimgs https://gimgs.net/mcp
# then: search_motion_templates / get_motion_template("line-carry-transition")

Prompt

Use the video-shotcraft shot recipe "Line carry transition" (Remotion composition, Apache-2.0) published on gimgs.net.
Recipe JSON (prompt = timing/easing/parameter table/pitfalls, files = TSX source): https://gimgs.net/motion/line-carry-transition.json
Raw TSX: https://gimgs.net/motion/line-carry-transition/template.html
Repository: https://github.com/Vincentwei1021/video-shotcraft  (component: demos/transition/line-carry-transition/LineCarryTransition.tsx; imports demos/_fixtures and assets/lib)
Spec: 1920x1080, 30 fps, 5.38s

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

5,441 chars · Apache-2.0Open raw template ↗
// ===== demos/transition/line-carry-transition/LineCarryTransition.tsx =====
// line-carry-transition|线条接力横移转场(Catch Me If You Can 图形接力)
// 世界宽 3840(A 左半 / B 右半)。0–24f 卡 A 底部 6px ink 进度条走满;
// 24–34f 进度条末端延伸成横线冲出卡右缘;34–94f 镜头整体左移 1920px
// (Easing.inOut(cubic),60f),线与镜头同速延伸,笔头始终在画面偏右;
// 94–112f 线拐直角围出 560×330 卡框(一条 path 全程 evolve,dashoffset 生长);
// 112–124f 框闭合后 B 卡内容淡入 12f。124–160f 真静止 36f ≥ 35f。
// 帧确定,无随机;笔头墨点 118f 起条件卸载(摘罩判例)。
import React from 'react';
import { AbsoluteFill, useCurrentFrame, interpolate, Easing } from 'remotion';
import { G, Card, TitleBlock } from '../../_fixtures/Fixtures';

// ---- 世界几何(一条折线:进度条 + 横线 + 直角 + 矩形框)----
// M 400,705 → 2600,705(进度 560 + 冲出 1640)→ 上 2600,375 → 右 3160,375
// → 下 3160,705 → 左回 2600,705 闭合。总长 2200+330+560+330+560 = 3980。
const PATH = 'M 400 705 L 2600 705 L 2600 375 L 3160 375 L 3160 705 L 2600 705';
const SEGS: Array<[number, number, number, number, number]> = [
  [400, 705, 2600, 705, 2200],
  [2600, 705, 2600, 375, 330],
  [2600, 375, 3160, 375, 560],
  [3160, 375, 3160, 705, 330],
  [3160, 705, 2600, 705, 560],
];
const TOTAL = 3980;

// 笔头坐标:按已画长度沿折线取点
const tipAt = (drawn: number): [number, number] => {
  let d = Math.max(0, Math.min(drawn, TOTAL));
  for (const [x1, y1, x2, y2, len] of SEGS) {
    if (d <= len) {
      const t = d / len;
      return [x1 + (x2 - x1) * t, y1 + (y2 - y1) * t];
    }
    d -= len;
  }
  return [2600, 705];
};

export const LineCarryTransition: React.FC = () => {
  const frame = useCurrentFrame();

  // 镜头:34–94f 左移 1920px,inOut cubic
  const cam = interpolate(frame, [34, 94], [0, 1920], {
    easing: Easing.inOut(Easing.cubic),
    extrapolateLeft: 'clamp',
    extrapolateRight: 'clamp',
  });

  // 已画长度:三段接力(进度条 → 冲出 → 与镜头同速 → 收框)
  let drawn: number;
  if (frame < 24) {
    drawn = interpolate(frame, [0, 24], [0, 560], {
      easing: Easing.out(Easing.cubic),
      extrapolateLeft: 'clamp',
      extrapolateRight: 'clamp',
    });
  } else if (frame < 34) {
    drawn = interpolate(frame, [24, 34], [560, 1100], {
      extrapolateRight: 'clamp',
    });
  } else if (frame < 94) {
    drawn = 1100 + cam; // 与镜头同速延伸,笔头稳在画面偏右
  } else {
    drawn = interpolate(frame, [94, 112], [3020, TOTAL], {
      easing: Easing.out(Easing.cubic),
      extrapolateLeft: 'clamp',
      extrapolateRight: 'clamp',
    });
  }

  // B 卡内容:框闭合(112f)后淡入 12f
  const contentOpacity = interpolate(frame, [112, 124], [0, 1], {
    extrapolateLeft: 'clamp',
    extrapolateRight: 'clamp',
  });

  // 笔头墨点:全程随笔走,112–118f 线性消散,118f 起条件卸载
  const tipMounted = frame < 118;
  const tipOpacity = interpolate(frame, [112, 118], [1, 0], {
    extrapolateLeft: 'clamp',
    extrapolateRight: 'clamp',
  });
  const [tx, ty] = tipAt(drawn);

  return (
    <AbsoluteFill style={{ background: G.bg, overflow: 'hidden' }}>
      {/* 世界容器:3840 宽,整体横移 = 镜头跟线走 */}
      <div
        style={{
          position: 'absolute',
          width: 3840,
          height: 1080,
          transform: `translateX(${-cam}px)`,
        }}
      >
        {/* B 半世界底色略浅,卖出"新世界" */}
        <div style={{ position: 'absolute', left: 1920, top: 0, width: 1920, height: 1080, background: G.panel }} />

        {/* 场景 A:标题 + 卡 + 进度条轨道(ink 填充即 path 本体) */}
        <div style={{ position: 'absolute', left: 400, top: 250 }}>
          <TitleBlock text="Scene A" size={56} />
        </div>
        <Card w={560} h={330} seed={2} style={{ position: 'absolute', left: 400, top: 350 }} />
        <div style={{ position: 'absolute', left: 400, top: 702, width: 560, height: 6, borderRadius: 3, background: G.line }} />

        {/* 一条线全程 evolve:dasharray/dashoffset 生长 */}
        <svg width={3840} height={1080} style={{ position: 'absolute', inset: 0, pointerEvents: 'none' }}>
          <path
            d={PATH}
            fill="none"
            stroke={G.ink}
            strokeWidth={6}
            strokeLinecap="round"
            strokeLinejoin="round"
            strokeDasharray={TOTAL}
            strokeDashoffset={TOTAL - drawn}
          />
          {tipMounted && <circle cx={tx} cy={ty} r={11} fill={G.ink} opacity={tipOpacity} />}
        </svg>

        {/* 场景 B:框(2600,375–3160,705)由线画成,内容淡入 */}
        <div
          style={{
            position: 'absolute',
            left: 2600,
            top: 375,
            width: 560,
            height: 330,
            boxSizing: 'border-box',
            padding: 26,
            display: 'flex',
            flexDirection: 'column',
            gap: 14,
            opacity: contentOpacity,
          }}
        >
          <div style={{ height: 18, width: '58%', background: G.bar, borderRadius: 9 }} />
          <div style={{ height: 11, width: '86%', background: G.line, borderRadius: 5 }} />
          <div style={{ height: 11, width: '72%', background: G.line, borderRadius: 5 }} />
          <div style={{ height: 11, width: '64%', background: G.line, borderRadius: 5 }} />
          <div style={{ marginTop: 'auto', display: 'flex', gap: 10, alignItems: 'center' }}>
            <div style={{ width: 30, height: 30, borderRadius: 15, background: G.mid }} />
            <div style={{ height: 11, width: 90, background: G.line, borderRadius: 5 }} />
          </div>
        </div>
        <div style={{ position: 'absolute', left: 2600, top: 275, opacity: contentOpacity }}>
          <TitleBlock text="Scene B" size={56} />
        </div>
      </div>
    </AbsoluteFill>
  );
};


  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/line-carry-transition/LineCarryTransition.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