{
  "slug": "text-column-converge",
  "title": "Two-word standoff converge",
  "tagline": "Two-word standoff converge — left \"NEW\" and a right feature word pinned at equal screen margins hard-cutting through rotation with zero shrink, until the last word eases to center once and locks into a phrase, with small text appearing almost as a hard cut below.",
  "description": "Two-word standoff converge — left \"NEW\" and a right feature word pinned at equal screen margins hard-cutting through rotation with zero shrink, until the last",
  "category": "typography",
  "tags": [
    "shotcraft",
    "remotion",
    "typography",
    "outro"
  ],
  "tier": "free",
  "license": "Apache-2.0",
  "spec": {
    "width": 1920,
    "height": 1080,
    "fps": 30,
    "duration": 6.72,
    "aspect": "16:9"
  },
  "kind": "remotion",
  "params_schema": {
    "type": "object",
    "properties": {}
  },
  "default_params": {},
  "preview": {
    "poster": "https://gimgs.net/motion/media/text-column-converge/poster-f0dfc37120a1.jpg",
    "video": "https://gimgs.net/motion/media/text-column-converge/video-805bbded049e.mp4"
  },
  "variants": [
    {
      "key": "text-column-converge",
      "label": "text-column-converge",
      "description": "Two-word standoff converge — left \"NEW\" and a right feature word pinned at equal screen margins hard-cutting through rotation with zero shrink, until the last word eases to center once and locks into a phrase, with small text appearing almost as a hard cut below",
      "use": "",
      "video": "https://gimgs.net/motion/media/text-column-converge/video-805bbded049e.mp4",
      "poster": "https://gimgs.net/motion/media/text-column-converge/poster-f0dfc37120a1.jpg"
    }
  ],
  "credits": {
    "author": "video-shotcraft (Vincent Wei)",
    "url": "https://github.com/Vincentwei1021/video-shotcraft"
  },
  "page_url": "https://gimgs.net/motion/text-column-converge.html",
  "json_url": "https://gimgs.net/motion/text-column-converge.json",
  "template_url": "https://gimgs.net/motion/text-column-converge/template.html",
  "play_url": null,
  "markdown_url": "https://gimgs.net/motion/text-column-converge.md",
  "updated_at": "2026-09-24T07:40:27.647Z",
  "prompt": "## When to use\n\nA passage that resolves a feature list into the product name/slogan (\"NEW × a string of features → NEW <product name>\"); launch-style recaps, version-number reveals\n\n## Intention\n\nTwo words camped left and right look like a table of contents facing off; the viewer assumes it's just a rotating caption — until the last word settles and they realize left and right were the same sentence's two halves all along, the single continuous converge retroactively reframes the whole rotation as \"suspense setup.\" The linchpin is **pinning**: during the rotation both words' positions stay fixed to the pixel, and the left/right screen margins stay exactly equal (measured at 412 vs 413 @1280 in the original film); any gradual shrink would spoil the converge early and dilute the reveal into a progress bar. The converge is allowed to happen only once, and must come after the last word — this is a closing reveal, not continuous motion.\n\n## Duration & energy\n\n- Duration: Rotation 7–16f/word × 8–9 words + converge ~36f + small text settles after; whole passage ~5–6s\n- Energy: Medium-low (a machine-like rhythm, a spec-sheet small-text temperament, not a slam)\n\n## Shot recipe (original Chinese — timing, easing, parameter table, known pitfalls)\n\n## 意图\n两词分踞左右像目录页对峙，观众以为只是轮换字幕；直到最后一词\n停稳才发现左右其实是同一句话的两半——唯一一次连续合拢把整段\n轮换追认为\"悬念铺垫\"。命门是**钉死**：轮换期两词位置一像素不动、\n左右屏边距完全相等（原片实测 412 vs 413@1280），任何渐进收缩都会\n提前剧透合拢、把揭晓稀释成进度条。合拢只许发生一次，且必须在\n最后一词之后——这是收尾揭晓，不是持续动效。\n\n## 动效核心\n- 左词左缘、右词右缘各自钉死在等屏边距处（原片实测 412/413px\n  @1280，×1.5 → 618/1302@1920）；右词换长换短一律右缘定位\n- 词轮换纯硬切，停留帧数不均（16/12/9/8/7/8/10/12f，机器节奏\n  带人味）；轮换全程间距零收缩（判例：渐进收缩版被用户驳回重做）\n- **唯一一次合拢**：末词停稳 ~10f 后，~1.2s（36f）ease-in-out\n  （cubic）连续滑动，两词滑到以屏幕中线居中定格\n- 合拢终点按等宽字体实际步进算（0.6em+letterSpacing × 字符数），\n  两词恰好一空格咬合，无重叠无缝隙；无 stamp 缩放、无二次补偿移动\n- 定格 ~18f 后斜体小字近乎硬切浮现（4f 快淡、零位移），与整行\n  同左缘、字号同正文\n\n## 参数表\n| 参数 | 典型值 | 调节手感 |\n|------|--------|----------|\n| 屏边距 | 618px@1920（**原片实测** 412/413@1280 ×1.5） | 左右差 >2px 读作没对齐；边距是本卡身份，别改成黄金分割 |\n| 词停留 | 7–16f 不均 | 均匀停留读作节拍器；<7f 读不完词 |\n| 合拢触发 | 末词停稳 +10f（原片 32.4→32.7s 实测） | 停稳期太短，观众没意识到\"这词不换了\"就开始滑 |\n| 合拢时长 | 36f ease-in-out cubic（**原片实测** ~1.2s 连续滑动） | <24f 读作弹跳归位；>50f 读作拖 |\n| 咬合步进 | 0.6em+letterSpacing/字符，等宽字体 | 非等宽字体步进不可预测，会重叠或漏缝 |\n| 字号 | 42@1080p（**原片实测**字高 ~20/720） | 这段是小字规格清单气质，放大到 60+ 就成了砸字（去用 cel-flash-stomp） |\n| 小字浮现 | 定格 +18f，4f 快淡零位移（原片 34.4→34.6s 间直接出现） | 加滑入/长淡入就抢了合拢的戏 |\n\n## 已知坑\n- demo 在灰阶/占位素材上调校通过——参数是调校起点非实战定稿，\n  首次实战须以真实素材回验\n- 与 type-rhythm-sync（字属性节拍）/beat-cut-moves（硬切节拍）撞\n  领域：那两张是\"轮换本身即内容\"；本卡命门是**位置钉死 + 末次合拢**\n  的收尾揭晓，轮换只是铺垫。若段落没有\"合拢揭晓\"就别用本卡\n- 合拢只许一次——每切一词收一档的渐进收缩版已被原片对照证伪并\n  删除，别复活\n- 必须等宽字体（demo 用 SF Mono）；比例字体下咬合终点算不准\n- 与原片残余差距：原片词换瞬间带 1 帧亮度闪烁（打字机换行感），\n  demo 是纯硬切；实战可选加回但闪烁 >1f 会读作故障\n\n## 参考实现\ndemos/typography/text-column-converge/\n（TextColumnConverge.tsx）\n原片出处：raycast-teams.mp4 28–36s\n\n## Reference implementation (Remotion)\n\n- demos/typography/text-column-converge/TextColumnConverge.tsx — https://raw.githubusercontent.com/Vincentwei1021/video-shotcraft/main/demos/typography/text-column-converge/TextColumnConverge.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/typography/text-column-converge/TextColumnConverge.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/typography/text-column-converge/TextColumnConverge.tsx",
      "url": "https://raw.githubusercontent.com/Vincentwei1021/video-shotcraft/main/demos/typography/text-column-converge/TextColumnConverge.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": 202,
    "repo": "https://github.com/Vincentwei1021/video-shotcraft",
    "files": [
      "https://raw.githubusercontent.com/Vincentwei1021/video-shotcraft/main/demos/typography/text-column-converge/TextColumnConverge.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/typography/text-column-converge/TextColumnConverge.tsx =====\n// text-column-converge —— raycast-teams（实测素材 28–36s 段）重做版：\n// 原片测量（1280 宽）：NEW 左缘钉死 x=412，特性词右缘钉死 x=867，\n// 两词到左右屏边距相等（412 vs 413），轮换期间间距完全不收缩；\n// 词换到 RAYCAST 后才发生唯一一次合拢——约 1.2s ease-in-out 连续滑动\n// （左缘 412→554 / 右缘 867→725），\"NEW RAYCAST\" 以屏幕中线居中定格；\n// 定格后约 0.6s，斜体 \"COMING 2026\" 在下方近乎硬切浮现。\nimport React from 'react';\nimport { AbsoluteFill, Easing, interpolate, useCurrentFrame } from 'remotion';\n\n// 词轮换表：停留帧数不均（机器节奏），全程钉在右缘，不做间距收缩\nconst STEPS: { word: string; dur: number }[] = [\n  { word: 'LAUNCHER DESIGN', dur: 16 },\n  { word: 'COMPACT MODE', dur: 12 },\n  { word: 'HOTKEY RECORDER', dur: 9 },\n  { word: 'HOTKEY TYPES', dur: 8 },\n  { word: 'VOICE FEATURES', dur: 7 },\n  { word: 'SETTINGS DESIGN', dur: 8 },\n  { word: 'AI CHAT', dur: 10 },\n  { word: 'FILE SEARCH', dur: 12 },\n  { word: 'RAYCAST', dur: 999 }, // 最后一词：停稳后触发唯一一次合拢\n];\n\nconst START = 8; // 开场黑场立静\n\n// 原片 1280 宽 → 1920 宽换算（×1.5）\nconst NEW_LEFT_EDGE = 618; // 412×1.5：NEW 左缘（= 左屏边距）\nconst WORD_RIGHT_EDGE = 1302; // 868×1.5：特性词右缘（= 右屏边距，1920-1302=618 对称）\n\nconst FS = 42; // 原片字高很小（720p 下 cap ~20px → 1080p ~30px → 字号 ~42）\nconst LSP = 3; // letterSpacing\n// 合拢终点按本字体实际步进计算（监视器等宽：0.6em + letterSpacing），\n// 保证 \"NEW RAYCAST\" 恰好一个空格咬合、整行居中于 960，不会重叠\nconst ADV = 0.6 * FS + LSP; // 每字符步进\nconst LINE_W = 11 * ADV; // \"NEW RAYCAST\" 共 11 字符\nconst MERGED_LEFT = 960 - LINE_W / 2; // 合拢后 NEW 左缘\nconst MERGED_RIGHT = 960 + LINE_W / 2; // 合拢后 RAYCAST 右缘\nconst CONVERGE_DUR = 36; // 合拢时长：原片 ~1.2s ≈ 36 帧\nconst CONVERGE_DELAY = 10; // RAYCAST 停稳后先静置 10 帧再合拢（原片 32.4→32.7s）\nconst SUB_DELAY = 18; // 合拢定格后 ~0.6s 出斜体小字\n\nexport const TextColumnConverge: React.FC = () => {\n  const f = useCurrentFrame();\n  const t = f - START;\n\n  // 定位当前步\n  let acc = 0;\n  let idx = 0;\n  let stepStart = 0;\n  for (let i = 0; i < STEPS.length; i++) {\n    if (t >= acc) { idx = i; stepStart = acc; }\n    acc += STEPS[i].dur;\n  }\n  const cur = STEPS[idx];\n  const isLast = idx === STEPS.length - 1;\n  const local = t - stepStart;\n\n  // 唯一一次合拢：RAYCAST 停稳 CONVERGE_DELAY 帧后，ease-in-out 连续滑动\n  const cvT = isLast ? local - CONVERGE_DELAY : -1;\n  const cv = interpolate(cvT, [0, CONVERGE_DUR], [0, 1], {\n    extrapolateLeft: 'clamp', extrapolateRight: 'clamp',\n    easing: Easing.inOut(Easing.cubic),\n  });\n\n  // NEW 左缘：618 → 831；特性词右缘：1302 → 1088\n  const newLeft = interpolate(cv, [0, 1], [NEW_LEFT_EDGE, MERGED_LEFT]);\n  const wordRight = interpolate(cv, [0, 1], [WORD_RIGHT_EDGE, MERGED_RIGHT]);\n\n  const converged = cv >= 1;\n\n  // 斜体小字：合拢定格后 SUB_DELAY 帧，近乎硬切（4 帧快速淡入，无位移）\n  const subT = converged ? cvT - CONVERGE_DUR - SUB_DELAY : -1;\n  const subOp = interpolate(subT, [0, 4], [0, 1], {\n    extrapolateLeft: 'clamp', extrapolateRight: 'clamp',\n  });\n\n  const visible = t >= 0;\n\n  const font: React.CSSProperties = {\n    fontFamily: '\"SF Mono\", Menlo, monospace',\n    fontWeight: 500,\n    fontSize: FS,\n    letterSpacing: 3,\n    color: '#f2f2f4',\n    whiteSpace: 'nowrap',\n    lineHeight: 1,\n  };\n\n  return (\n    <AbsoluteFill style={{ background: '#050506', overflow: 'hidden' }}>\n      {visible && (\n        <div style={{ position: 'absolute', inset: 0 }}>\n          {/* NEW：左缘定位（轮换期间钉死在左屏边距处） */}\n          <div style={{\n            ...font, position: 'absolute',\n            left: newLeft, top: 519,\n          }}>\n            NEW\n          </div>\n          {/* 特性词：右缘定位（词换长换短，右缘不动） */}\n          <div style={{\n            ...font, position: 'absolute',\n            right: 1920 - wordRight, top: 519,\n          }}>\n            {cur.word}\n          </div>\n\n          {/* 斜体小字：合拢后在整行正下方浮现，与整行同左缘 */}\n          <div style={{\n            ...font,\n            fontStyle: 'italic',\n            color: '#d8d8dc',\n            position: 'absolute',\n            left: MERGED_LEFT, top: 519 + FS + 14,\n            opacity: subOp,\n          }}>\n            COMING 2026\n          </div>\n        </div>\n      )}\n    </AbsoluteFill>\n  );\n};\n\n\n"
}