{
  "slug": "line-carry-transition",
  "title": "Line carry transition",
  "tagline": "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.",
  "description": "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 anyw",
  "category": "transition",
  "tags": [
    "shotcraft",
    "remotion",
    "transition",
    "camera"
  ],
  "tier": "free",
  "license": "Apache-2.0",
  "spec": {
    "width": 1920,
    "height": 1080,
    "fps": 30,
    "duration": 5.38,
    "aspect": "16:9"
  },
  "kind": "remotion",
  "params_schema": {
    "type": "object",
    "properties": {}
  },
  "default_params": {},
  "preview": {
    "poster": "https://gimgs.net/motion/media/line-carry-transition/poster-3aeebed78e5a.jpg",
    "video": "https://gimgs.net/motion/media/line-carry-transition/video-b593fe16a284.mp4"
  },
  "variants": [
    {
      "key": "line-carry-transition",
      "label": "line-carry-transition · Line carry transition",
      "description": "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.",
      "use": "",
      "video": "https://gimgs.net/motion/media/line-carry-transition/video-b593fe16a284.mp4",
      "poster": "https://gimgs.net/motion/media/line-carry-transition/poster-3aeebed78e5a.jpg"
    }
  ],
  "credits": {
    "author": "video-shotcraft (Vincent Wei)",
    "url": "https://github.com/Vincentwei1021/video-shotcraft"
  },
  "page_url": "https://gimgs.net/motion/line-carry-transition.html",
  "json_url": "https://gimgs.net/motion/line-carry-transition.json",
  "template_url": "https://gimgs.net/motion/line-carry-transition/template.html",
  "play_url": null,
  "markdown_url": "https://gimgs.net/motion/line-carry-transition.md",
  "updated_at": "2026-09-24T07:40:05.457Z",
  "prompt": "## When to use\n\nBetween 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.\n\n## Intention\n\nThe 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.\n\n## Duration & energy\n\n- Duration: ~5.3s (progress bar fills + pan 60f + framing + content fade-in + 36f hold)\n- Energy: Medium\n\n## Shot recipe (original Chinese — timing, easing, parameter table, known pitfalls)\n\n## 意图\n转场库三大族（穿越/藏切/体块）都在回答\"场景怎么换\"；本卡的回答是\n**根本不换——一条线牵着你走过去**。进度条走满后末端延伸成长线冲出\n卡缘，镜头跟着线横移 1920px 进入新世界，线在移动中拐直角围出 B 场景\n的卡框，框闭合、内容淡入，从头到尾观众的眼睛没离开过这条线。图形\n连续性本身就是转场。与 match-cut 的区别：那是切点两侧形状对位——\n终究还是切；这是连续绘制——**没有切**。线的身份要有叙事逻辑（进度条\n\"走完了\"所以延伸出去），无来由的线只是装饰。\n\n## 动效核心\n- 3840 宽世界容器 translateX 当镜头，34–94f 移 1920px，\n  Easing.inOut(cubic)\n- 一条手写折线 path（进度条 560 + 冲出段 + 直角 + 560×330 矩形框，\n  总长 3980）全程 dasharray/dashoffset evolve，分段驱动 drawn 长度\n- **命门：横移段 drawn = 1100 + cam，线生长与镜头位移同速**——笔头\n  钉在画面 x≈1500 偏右，永不出画也永不落后；一旦失速，观众跟丢线\n  就等于跟丢了转场\n- 笔头 r=11 墨点随线走，框闭合后 112–118f 线性消散并条件卸载\n  （摘罩判例）；B 内容 124f 前淡入完，124–160f 真静止 36f\n\n## 参数表\n| 参数 | 典型值 | 调节手感 |\n|------|--------|----------|\n| 线体 | 6px ink 实线 | 太细横移中读丢；虚线读作未完成态 |\n| 横移配比 | 1920px / 60f（32px/f） | 更远的横移等比拉长时长，别提速——>40px/f 笔头开始拖影跟丢 |\n| 笔头位置 | 画面 x≈1500（约 78% 处偏右） | 居中读作\"线追镜头\"；贴边一颤就出画 |\n| 拐角 | 直角硬拐，不倒圆 | 圆角软拐丢掉制图感，Saul Bass 味就没了 |\n| 框闭合 | 闭合帧起 B 内容再淡入 | 框没闭合内容先出，线的悬念白搭 |\n| 收尾 | 淡入完 ≥36f 真静止 | 笔头墨点必须条件卸载，残留即毁静止 |\n\n## 已知坑\n- demo 在灰阶/占位素材上调校通过——参数是调校起点非实战定稿，\n  首次实战须以真实素材回验\n- 与 draw-svg-trace 的关系：那是元素级描边点名（线属于元素），这是\n  场景级线条接力（线属于镜头）——同片可共存，但线的语义要一致：\n  同为 ink 6px 手绘感，或同为几何硬线，混用读作两套美术\n- 全片 ≤1 次——这是招牌转场不是常规转场，第二次出现魔法就穿帮\n- 起点必须是已有图形构件的自然延伸（进度条走满、下划线画完），\n  凭空起线读作屏保\n- 声音：线生长一声持续笔擦、框闭合一声轻叩（sound-design §4.5）\n\n## 参考实现\ndemos/transition/line-carry-transition/\n（LineCarryTransition.tsx）\n\n## Reference implementation (Remotion)\n\n- demos/transition/line-carry-transition/LineCarryTransition.tsx — https://raw.githubusercontent.com/Vincentwei1021/video-shotcraft/main/demos/transition/line-carry-transition/LineCarryTransition.tsx\n\nThe component imports shared fixtures from `demos/_fixtures` and helpers from `assets/lib` in the repo; SKILL.md there documents the render workflow.\n",
  "flow": [
    {
      "step": "Read the recipe",
      "note": "Prompt = when to use, intention, parameter table with typical values and how each one feels, known pitfalls. Treat values as calibrated starting points."
    },
    {
      "step": "Get the code",
      "tool": "git",
      "cmd": "git clone https://github.com/Vincentwei1021/video-shotcraft && cd video-shotcraft && npm install"
    },
    {
      "step": "Register + render",
      "tool": "remotion",
      "cmd": "npx remotion render <CompositionId> out.mp4   # component: demos/transition/line-carry-transition/LineCarryTransition.tsx"
    },
    {
      "step": "Or re-implement",
      "note": "Port the timing/easing from the recipe to HTML + WAAPI (gimgs motion contract) or any engine; keep the same segment windows and easing."
    }
  ],
  "files": [
    {
      "path": "demos/transition/line-carry-transition/LineCarryTransition.tsx",
      "url": "https://raw.githubusercontent.com/Vincentwei1021/video-shotcraft/main/demos/transition/line-carry-transition/LineCarryTransition.tsx"
    }
  ],
  "contract": "React/Remotion composition (TSX) from the open-source video-shotcraft library (Apache-2.0). It is not a browser-playable template: clone the repo, register the component in a Remotion project and render with `npx remotion render`. The recipe (prompt) describes timing, easing and parameters so you can also re-implement the shot in any engine.",
  "render": {
    "engine": "remotion",
    "frames": 161,
    "repo": "https://github.com/Vincentwei1021/video-shotcraft",
    "files": [
      "https://raw.githubusercontent.com/Vincentwei1021/video-shotcraft/main/demos/transition/line-carry-transition/LineCarryTransition.tsx"
    ],
    "note": "Clone https://github.com/Vincentwei1021/video-shotcraft (npm install). The component lives under demos/{category}/{name}/ and imports shared fixtures from demos/_fixtures and helpers from assets/lib. Copy it (with those imports) into a Remotion project, register a <Composition> with the template's width/height/fps/duration in Root.tsx, then `npx remotion render <CompositionId> out.mp4`. SKILL.md in the repo documents the full Claude Code / Codex workflow. The prompt below is the shot recipe (timing, easing, parameter table, pitfalls) — enough to re-implement the shot in HTML/WAAPI or another engine if you prefer."
  },
  "source": "// ===== demos/transition/line-carry-transition/LineCarryTransition.tsx =====\n// line-carry-transition｜线条接力横移转场（Catch Me If You Can 图形接力）\n// 世界宽 3840（A 左半 / B 右半）。0–24f 卡 A 底部 6px ink 进度条走满；\n// 24–34f 进度条末端延伸成横线冲出卡右缘；34–94f 镜头整体左移 1920px\n// （Easing.inOut(cubic)，60f），线与镜头同速延伸，笔头始终在画面偏右；\n// 94–112f 线拐直角围出 560×330 卡框（一条 path 全程 evolve，dashoffset 生长）；\n// 112–124f 框闭合后 B 卡内容淡入 12f。124–160f 真静止 36f ≥ 35f。\n// 帧确定，无随机；笔头墨点 118f 起条件卸载（摘罩判例）。\nimport React from 'react';\nimport { AbsoluteFill, useCurrentFrame, interpolate, Easing } from 'remotion';\nimport { G, Card, TitleBlock } from '../../_fixtures/Fixtures';\n\n// ---- 世界几何（一条折线：进度条 + 横线 + 直角 + 矩形框）----\n// M 400,705 → 2600,705（进度 560 + 冲出 1640）→ 上 2600,375 → 右 3160,375\n// → 下 3160,705 → 左回 2600,705 闭合。总长 2200+330+560+330+560 = 3980。\nconst PATH = 'M 400 705 L 2600 705 L 2600 375 L 3160 375 L 3160 705 L 2600 705';\nconst SEGS: Array<[number, number, number, number, number]> = [\n  [400, 705, 2600, 705, 2200],\n  [2600, 705, 2600, 375, 330],\n  [2600, 375, 3160, 375, 560],\n  [3160, 375, 3160, 705, 330],\n  [3160, 705, 2600, 705, 560],\n];\nconst TOTAL = 3980;\n\n// 笔头坐标：按已画长度沿折线取点\nconst tipAt = (drawn: number): [number, number] => {\n  let d = Math.max(0, Math.min(drawn, TOTAL));\n  for (const [x1, y1, x2, y2, len] of SEGS) {\n    if (d <= len) {\n      const t = d / len;\n      return [x1 + (x2 - x1) * t, y1 + (y2 - y1) * t];\n    }\n    d -= len;\n  }\n  return [2600, 705];\n};\n\nexport const LineCarryTransition: React.FC = () => {\n  const frame = useCurrentFrame();\n\n  // 镜头：34–94f 左移 1920px，inOut cubic\n  const cam = interpolate(frame, [34, 94], [0, 1920], {\n    easing: Easing.inOut(Easing.cubic),\n    extrapolateLeft: 'clamp',\n    extrapolateRight: 'clamp',\n  });\n\n  // 已画长度：三段接力（进度条 → 冲出 → 与镜头同速 → 收框）\n  let drawn: number;\n  if (frame < 24) {\n    drawn = interpolate(frame, [0, 24], [0, 560], {\n      easing: Easing.out(Easing.cubic),\n      extrapolateLeft: 'clamp',\n      extrapolateRight: 'clamp',\n    });\n  } else if (frame < 34) {\n    drawn = interpolate(frame, [24, 34], [560, 1100], {\n      extrapolateRight: 'clamp',\n    });\n  } else if (frame < 94) {\n    drawn = 1100 + cam; // 与镜头同速延伸，笔头稳在画面偏右\n  } else {\n    drawn = interpolate(frame, [94, 112], [3020, TOTAL], {\n      easing: Easing.out(Easing.cubic),\n      extrapolateLeft: 'clamp',\n      extrapolateRight: 'clamp',\n    });\n  }\n\n  // B 卡内容：框闭合(112f)后淡入 12f\n  const contentOpacity = interpolate(frame, [112, 124], [0, 1], {\n    extrapolateLeft: 'clamp',\n    extrapolateRight: 'clamp',\n  });\n\n  // 笔头墨点：全程随笔走，112–118f 线性消散，118f 起条件卸载\n  const tipMounted = frame < 118;\n  const tipOpacity = interpolate(frame, [112, 118], [1, 0], {\n    extrapolateLeft: 'clamp',\n    extrapolateRight: 'clamp',\n  });\n  const [tx, ty] = tipAt(drawn);\n\n  return (\n    <AbsoluteFill style={{ background: G.bg, overflow: 'hidden' }}>\n      {/* 世界容器：3840 宽，整体横移 = 镜头跟线走 */}\n      <div\n        style={{\n          position: 'absolute',\n          width: 3840,\n          height: 1080,\n          transform: `translateX(${-cam}px)`,\n        }}\n      >\n        {/* B 半世界底色略浅，卖出\"新世界\" */}\n        <div style={{ position: 'absolute', left: 1920, top: 0, width: 1920, height: 1080, background: G.panel }} />\n\n        {/* 场景 A：标题 + 卡 + 进度条轨道（ink 填充即 path 本体） */}\n        <div style={{ position: 'absolute', left: 400, top: 250 }}>\n          <TitleBlock text=\"Scene A\" size={56} />\n        </div>\n        <Card w={560} h={330} seed={2} style={{ position: 'absolute', left: 400, top: 350 }} />\n        <div style={{ position: 'absolute', left: 400, top: 702, width: 560, height: 6, borderRadius: 3, background: G.line }} />\n\n        {/* 一条线全程 evolve：dasharray/dashoffset 生长 */}\n        <svg width={3840} height={1080} style={{ position: 'absolute', inset: 0, pointerEvents: 'none' }}>\n          <path\n            d={PATH}\n            fill=\"none\"\n            stroke={G.ink}\n            strokeWidth={6}\n            strokeLinecap=\"round\"\n            strokeLinejoin=\"round\"\n            strokeDasharray={TOTAL}\n            strokeDashoffset={TOTAL - drawn}\n          />\n          {tipMounted && <circle cx={tx} cy={ty} r={11} fill={G.ink} opacity={tipOpacity} />}\n        </svg>\n\n        {/* 场景 B：框(2600,375–3160,705)由线画成，内容淡入 */}\n        <div\n          style={{\n            position: 'absolute',\n            left: 2600,\n            top: 375,\n            width: 560,\n            height: 330,\n            boxSizing: 'border-box',\n            padding: 26,\n            display: 'flex',\n            flexDirection: 'column',\n            gap: 14,\n            opacity: contentOpacity,\n          }}\n        >\n          <div style={{ height: 18, width: '58%', background: G.bar, borderRadius: 9 }} />\n          <div style={{ height: 11, width: '86%', background: G.line, borderRadius: 5 }} />\n          <div style={{ height: 11, width: '72%', background: G.line, borderRadius: 5 }} />\n          <div style={{ height: 11, width: '64%', background: G.line, borderRadius: 5 }} />\n          <div style={{ marginTop: 'auto', display: 'flex', gap: 10, alignItems: 'center' }}>\n            <div style={{ width: 30, height: 30, borderRadius: 15, background: G.mid }} />\n            <div style={{ height: 11, width: 90, background: G.line, borderRadius: 5 }} />\n          </div>\n        </div>\n        <div style={{ position: 'absolute', left: 2600, top: 275, opacity: contentOpacity }}>\n          <TitleBlock text=\"Scene B\" size={56} />\n        </div>\n      </div>\n    </AbsoluteFill>\n  );\n};\n\n\n"
}