{
  "slug": "type-assembly-moves",
  "title": "Type assembly: four moves",
  "tagline": "Four type-assembly moves — split-text-stagger, letters rising into place; drift-assembly, letters drifting together; tracking-expand, breathing letter-spacing; text-on-path, flowing in along a curve.",
  "description": "Four type-assembly moves — split-text-stagger, letters rising into place; drift-assembly, letters drifting together; tracking-expand, breathing letter-spacing",
  "category": "typography",
  "tags": [
    "shotcraft",
    "remotion",
    "typography"
  ],
  "tier": "free",
  "license": "Apache-2.0",
  "spec": {
    "width": 1920,
    "height": 1080,
    "fps": 30,
    "duration": 4.39,
    "aspect": "16:9"
  },
  "kind": "remotion",
  "params_schema": {
    "type": "object",
    "properties": {}
  },
  "default_params": {},
  "preview": {
    "poster": "https://gimgs.net/motion/media/type-assembly-moves/poster-ad7df6e5fe35.jpg",
    "video": "https://gimgs.net/motion/media/type-assembly-moves/video-1b1b81178680.mp4"
  },
  "variants": [
    {
      "key": "split-text-stagger",
      "label": "split-text-stagger · Stagger rise",
      "description": "Each letter sits in an overflow box translating Y from 115% to 0 with a 10% overshoot, delay i×2f, the baseline growing in sync",
      "use": "The general default; clean and rational",
      "video": "https://gimgs.net/motion/media/type-assembly-moves/video-1b1b81178680.mp4",
      "poster": "https://gimgs.net/motion/media/type-assembly-moves/poster-ad7df6e5fe35.jpg"
    },
    {
      "key": "drift-assembly",
      "label": "drift-assembly · Drift together",
      "description": "Letters seed a direction ±300px plus 8px blur, drifting in and settling in staggered order, a deepening pulse on lock, the whole word breathing at 1.04 once assembled",
      "use": "A brand-name/title-sequence-grade reveal",
      "video": "https://gimgs.net/motion/media/type-assembly-moves/video-a7a821032a27.mp4",
      "poster": "https://gimgs.net/motion/media/type-assembly-moves/poster-drift-assembly-a262fcfdbd68.jpg"
    },
    {
      "key": "tracking-expand",
      "label": "tracking-expand · Letter-spacing breathe",
      "description": "Letters expand from a stacked -0.42em to 0.14em, with blur going 10→0 on the same curve",
      "use": "A quiet, lyrical passage; a subtitle follow-on",
      "video": "https://gimgs.net/motion/media/type-assembly-moves/video-68b61c74840b.mp4",
      "poster": "https://gimgs.net/motion/media/type-assembly-moves/poster-tracking-expand-c7d1e8f888c1.jpg"
    },
    {
      "key": "text-on-path",
      "label": "text-on-path · Flow along a curve",
      "description": "Letters slide in single-file along a bezier curve (rotating with the tangent angle), straightening level 12f after arrival",
      "use": "When the curve itself carries meaning (a growth line / a flow line)",
      "video": "https://gimgs.net/motion/media/type-assembly-moves/video-3d96aebc33ce.mp4",
      "poster": "https://gimgs.net/motion/media/type-assembly-moves/poster-text-on-path-10262f55a8df.jpg"
    }
  ],
  "credits": {
    "author": "video-shotcraft (Vincent Wei)",
    "url": "https://github.com/Vincentwei1021/video-shotcraft"
  },
  "page_url": "https://gimgs.net/motion/type-assembly-moves.html",
  "json_url": "https://gimgs.net/motion/type-assembly-moves.json",
  "template_url": "https://gimgs.net/motion/type-assembly-moves/template.html",
  "play_url": null,
  "markdown_url": "https://gimgs.net/motion/type-assembly-moves.md",
  "updated_at": "2026-09-24T07:40:32.644Z",
  "prompt": "## When to use\n\nBig-title/tagline entrances; in the same title-entrance family as type-entrance-moves' two styles, split-flap-title, and document-typewriter-reveal — at most 2 per film\n\n## Intention\n\nTitle entrances already have a four-style \"in-place transform\" family (garbled decode / character drop / split-flap / typewriter — letters change form in place). This card adds the \"assembly\" family: letters gather **from elsewhere** into the title, and the motion path itself is the show. A is piano-key style — every letter slides up from a hidden crop line below, rational and clean, the most general-purpose; B is title-sequence style — letters scatter in with blur, locking in and deepening one by one, a Stranger Things kind of ceremony, good for a brand-name reveal; C is breathing style — letters expand out from a stacked clump as if taking a breath, the quietest of the four, good for lyrical passages; D is semantic style — letters flow in single-file along a rising curve then straighten out, the curve can trace exactly along a chart's growth line, telling text and data in the same frame.\n\n## Duration & energy\n\n- Duration: Single style 4–5s (action segment A ~56f / B ~104f / C ~58f / D ~99f, all including holds)\n- Energy: A medium / B medium-high / C low-medium / D medium\n\n## Shot recipe (original Chinese — timing, easing, parameter table, known pitfalls)\n\n## 意图\n标题入场已有\"原位变形系\"四式（乱码解码/字符坠落/翻牌/打字机——字\n在原地换形态）。本卡补\"集结系\"：字符从别处**汇聚成**标题，运动轨迹\n本身有戏。A 琴键式——每字从看不见的裁切线下滑升，理性利落，通用度\n最高；B 片头式——字符四散带模糊漂入、逐个锁定加深，Stranger Things\n的仪式感，适合品牌名亮相;C 呼吸式——字母从叠压一团吸一口气展开，\n最安静的一式，适合抒情段落；D 语义式——字符沿一条上升曲线鱼贯流入\n再摆正，曲线可以正好描着图表增长线走，文字与数据同框叙事。\n\n## 四式选型\n| 式 | 做法 | 适用 |\n|----|------|------|\n| A split-text-stagger 裂升 | 每字 overflow 盒内 translateY(115%→0) 带 10% 过冲，delay i×2f，基线同步生长 | 通用默认；利落理性 |\n| B drift-assembly 漂移合拢 | 字符 seed 方向 ±300px + blur 8px 漂入错峰归位，锁定帧加深脉冲，合体后整词呼吸 1.04 | 品牌名/片头级亮相 |\n| C tracking-expand 字距呼吸 | 字母从 −0.42em 叠压展开到 0.14em，blur 10→0 同曲线 | 安静抒情段；副标题跟进 |\n| D text-on-path 沿线流入 | 字符沿贝塞尔曲线鱼贯滑入（切线角旋转），到达后 12f 摆正水平 | 曲线有语义时（增长线/流程线） |\n\n## 参数表\n| 参数 | 典型值 | 调节手感 |\n|------|--------|----------|\n| A 过冲 | 10%（原案 6% 可测不可感，实渲加码） | 正常速度要看得见回落那一下 |\n| B 锁定脉冲 | 字色深到 #000 + 描边 0→3px→0 / 8f | 白底上加深+描边，不用发光（判例） |\n| B 漂移三绑定 | 位移 (1−p)、blur 8(1−p)、opacity 0.35→1 共用一条 p | 三者分曲线走，字会\"到了还糊着\" |\n| C 实现命门 | letter-spacing 恒为终态，逐字符 span 只做 translateX=(1−p)(i−词心)(−0.56em) | 直接动 letter-spacing 逐帧重排必抖 |\n| D 摆正 | 到达停 8f 再 12f lerp 到水平基线（y 拉平 rotate→0） | 不摆正就永远是\"挂在线上\"，读不出标题 |\n| D 曲线 | evolve（dashoffset）随最前字符同步生长 | 曲线先画完字再走，读作两个动画 |\n| 收尾 | 全员落定真静止 ≥30f | R1 |\n\n## 已知坑\n- demo 在灰阶/占位素材上调校通过——参数是调校起点非实战定稿，\n  首次实战须以真实素材回验\n- 标题入场大品类现已八式（本卡四式 + type-entrance-moves 两式 +\n  split-flap-title + document-typewriter-reveal），**同片 ≤2 种**（P4）铁律不变\n- B 式与 D 式都是高注意力入场，别再叠相机动作——字在飞的时候镜头必须稳\n- C 式幅度命门：起始叠压至少 −0.4em 级（每缝 80px+ 位移）才可感，\n  −0.1em 级\"微展开\"正常速度读不出（可感性判例）\n- D 式字符在曲线低段会短暂交叠，是鱼贯读感的一部分；但字号 >80px 时\n  交叠面积过大会糊成一团，大字改用更长曲线或减字数\n\n## 参考实现\ndemos/typography/type-assembly-moves/\n（LetterformDriftAssembly.tsx / SplitTextStagger.tsx / TextOnPath.tsx / TrackingExpandReveal.tsx）\n\n## Reference implementation (Remotion)\n\n- demos/typography/type-assembly-moves/LetterformDriftAssembly.tsx — https://raw.githubusercontent.com/Vincentwei1021/video-shotcraft/main/demos/typography/type-assembly-moves/LetterformDriftAssembly.tsx\n- demos/typography/type-assembly-moves/SplitTextStagger.tsx — https://raw.githubusercontent.com/Vincentwei1021/video-shotcraft/main/demos/typography/type-assembly-moves/SplitTextStagger.tsx\n- demos/typography/type-assembly-moves/TextOnPath.tsx — https://raw.githubusercontent.com/Vincentwei1021/video-shotcraft/main/demos/typography/type-assembly-moves/TextOnPath.tsx\n- demos/typography/type-assembly-moves/TrackingExpandReveal.tsx — https://raw.githubusercontent.com/Vincentwei1021/video-shotcraft/main/demos/typography/type-assembly-moves/TrackingExpandReveal.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/type-assembly-moves/LetterformDriftAssembly.tsx, demos/typography/type-assembly-moves/SplitTextStagger.tsx, demos/typography/type-assembly-moves/TextOnPath.tsx, demos/typography/type-assembly-moves/TrackingExpandReveal.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/type-assembly-moves/LetterformDriftAssembly.tsx",
      "url": "https://raw.githubusercontent.com/Vincentwei1021/video-shotcraft/main/demos/typography/type-assembly-moves/LetterformDriftAssembly.tsx"
    },
    {
      "path": "demos/typography/type-assembly-moves/SplitTextStagger.tsx",
      "url": "https://raw.githubusercontent.com/Vincentwei1021/video-shotcraft/main/demos/typography/type-assembly-moves/SplitTextStagger.tsx"
    },
    {
      "path": "demos/typography/type-assembly-moves/TextOnPath.tsx",
      "url": "https://raw.githubusercontent.com/Vincentwei1021/video-shotcraft/main/demos/typography/type-assembly-moves/TextOnPath.tsx"
    },
    {
      "path": "demos/typography/type-assembly-moves/TrackingExpandReveal.tsx",
      "url": "https://raw.githubusercontent.com/Vincentwei1021/video-shotcraft/main/demos/typography/type-assembly-moves/TrackingExpandReveal.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": 132,
    "repo": "https://github.com/Vincentwei1021/video-shotcraft",
    "files": [
      "https://raw.githubusercontent.com/Vincentwei1021/video-shotcraft/main/demos/typography/type-assembly-moves/LetterformDriftAssembly.tsx",
      "https://raw.githubusercontent.com/Vincentwei1021/video-shotcraft/main/demos/typography/type-assembly-moves/SplitTextStagger.tsx",
      "https://raw.githubusercontent.com/Vincentwei1021/video-shotcraft/main/demos/typography/type-assembly-moves/TextOnPath.tsx",
      "https://raw.githubusercontent.com/Vincentwei1021/video-shotcraft/main/demos/typography/type-assembly-moves/TrackingExpandReveal.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/type-assembly-moves/LetterformDriftAssembly.tsx =====\n// 字形漂移合拢（letterform-drift-assembly）——Stranger Things 片头式入场。\n// 标题 \"ASSEMBLE\" 拆 9 字符：各自从不同方向（h(i) seeded 随机向量，\n// 幅度 ±260–360px）带 blur 8px + opacity 0.35 缓慢漂入，错峰归位\n// （delay i×3f，45f 行程，Easing.out(cubic)）。每字锁定瞬间给一次\n// \"加深脉冲\"：字色 G.ink→#000→G.ink + 描边 0→3px→0（8f）——白底上\n// 不用发光用加深（库判例）。全部合体后整词 scale 1→1.04→1 收束呼吸\n// （判例：1.02 太弱，加码到 1.04）。\n// 关键帧：0–24 各字错峰启程 → i 字 [i*3, i*3+45] 漂入归位 →\n// 锁定帧 i*3+45 起 8f 加深脉冲（最后一字 69–77）→ 80–104 整词呼吸 →\n// 104–150 全静止（46f，无逐帧滤镜）。\nimport React from 'react';\nimport { useCurrentFrame, interpolate, Easing } from 'remotion';\nimport { G, TitleBlock } from '../../_fixtures/Fixtures';\n\nconst h = (n: number) => {\n  const s = Math.sin(n * 127.3) * 43758.5453;\n  return s - Math.floor(s);\n};\n\nconst WORD = 'ASSEMBLE';\nconst TRAVEL = 45; // 每字漂入行程帧数\nconst STAG = 3; // 错峰间隔\n\nexport const LetterformDriftAssembly: React.FC = () => {\n  const frame = useCurrentFrame();\n  const chars = WORD.split('');\n  const lastLock = (chars.length - 1) * STAG + TRAVEL; // 69\n\n  // 整词收束呼吸：80–92 放大到 1.04，92–104 回落，之后恒 1 → 帧确定\n  const breath =\n    frame < 92\n      ? interpolate(frame, [80, 92], [1, 1.04], {\n          extrapolateLeft: 'clamp',\n          extrapolateRight: 'clamp',\n          easing: Easing.inOut(Easing.cubic),\n        })\n      : interpolate(frame, [92, 104], [1.04, 1], {\n          extrapolateLeft: 'clamp',\n          extrapolateRight: 'clamp',\n          easing: Easing.inOut(Easing.cubic),\n        });\n\n  return (\n    <div style={{ width: 1920, height: 1080, background: G.bg, position: 'relative', overflow: 'hidden' }}>\n      <div style={{ position: 'absolute', left: 120, top: 96 }}>\n        <TitleBlock text=\"LETTERFORM DRIFT ASSEMBLY\" size={54} />\n      </div>\n      <div\n        style={{\n          position: 'absolute',\n          inset: 0,\n          display: 'flex',\n          alignItems: 'center',\n          justifyContent: 'center',\n          transform: `scale(${breath})`,\n        }}\n      >\n        {chars.map((c, i) => {\n          const start = i * STAG;\n          const lock = start + TRAVEL;\n          // 漂入进度\n          const p = interpolate(frame, [start, lock], [0, 1], {\n            extrapolateLeft: 'clamp',\n            extrapolateRight: 'clamp',\n            easing: Easing.out(Easing.cubic),\n          });\n          // seeded 起始向量：方向 h(i)，幅度 260–360px\n          const ang = h(i + 1) * Math.PI * 2;\n          const mag = 260 + h(i + 101) * 100;\n          const dx = Math.cos(ang) * mag * (1 - p);\n          const dy = Math.sin(ang) * mag * (1 - p);\n          const blur = 8 * (1 - p);\n          const op = interpolate(p, [0, 1], [0.35, 1]);\n          // 锁定加深脉冲：lock→lock+8，三角波 0→1→0\n          const pulse =\n            frame <= lock || frame >= lock + 8\n              ? 0\n              : frame < lock + 4\n                ? (frame - lock) / 4\n                : (lock + 8 - frame) / 4;\n          const shade = Math.round(47 * (1 - pulse)); // #2f(47)→#00\n          const color = `rgb(${shade},${shade},${shade})`;\n          const strokeW = 3 * pulse;\n          return (\n            <span\n              key={i}\n              style={{\n                fontFamily: 'Helvetica, Arial, sans-serif',\n                fontWeight: 800,\n                fontSize: 140,\n                letterSpacing: 8,\n                color,\n                display: 'inline-block',\n                transform: `translate(${dx}px, ${dy}px)`,\n                opacity: op,\n                filter: blur > 0.01 ? `blur(${blur}px)` : undefined,\n                WebkitTextStroke: strokeW > 0.01 ? `${strokeW}px #000` : undefined,\n              }}\n            >\n              {c}\n            </span>\n          );\n        })}\n      </div>\n    </div>\n  );\n};\n\n\n// ===== demos/typography/type-assembly-moves/SplitTextStagger.tsx =====\n// 逐字遮罩裂升（split-text-stagger）——GSAP SplitText 惯用入场。\n// 标题 \"MOTION SYSTEM\" 按字符拆 span，每字外包 overflow:hidden 的行高盒，\n// 内层从 translateY(115%) 升到 0，各 14f Easing.out(cubic)，带 10% 过冲\n// 再 6f 回落（原案 6% 过冲，按可感性红线加码到 10%）。delay = 字符索引×2f。\n// 底部基线细线（G.bar 2px）在首字起跳同帧从左向右生长，暗示裁切线存在。\n// 关键帧：0–12 空场 hold → 12 首字起跳 + 基线开始生长 → 每字 12+i*2 起跳，\n// 14f 升至 -10% 过冲 → 再 6f 回落归 0 → 末字(索引12)于 56f 落定 →\n// 56–130 全静止（74f ≥ 40f，无逐帧噪声层）。\nimport React from 'react';\nimport { useCurrentFrame, interpolate, Easing } from 'remotion';\nimport { G, TitleBlock } from '../../_fixtures/Fixtures';\n\nconst TEXT = 'MOTION SYSTEM';\nconst START = 12; // 首字起跳帧\nconst RISE = 14; // 升起时长\nconst SETTLE = 6; // 过冲回落时长\nconst OVERSHOOT = -10; // 过冲到 -10%（原案 6%，加码）\nconst FONT = 120;\n\n// 单字符纵向位移（%）：115 → -10（out cubic）→ 0（out quad），帧确定\nconst charY = (f: number, idx: number): number => {\n  const t0 = START + idx * 2;\n  if (f < t0 + RISE) {\n    return interpolate(f, [t0, t0 + RISE], [115, OVERSHOOT], {\n      extrapolateLeft: 'clamp',\n      extrapolateRight: 'clamp',\n      easing: Easing.out(Easing.cubic),\n    });\n  }\n  return interpolate(f, [t0 + RISE, t0 + RISE + SETTLE], [OVERSHOOT, 0], {\n    extrapolateLeft: 'clamp',\n    extrapolateRight: 'clamp',\n    easing: Easing.out(Easing.quad),\n  });\n};\n\nexport const SplitTextStagger: React.FC = () => {\n  const frame = useCurrentFrame();\n  const chars = TEXT.split('');\n  // 基线：首字起跳同帧开始，从左向右生长到 100%\n  const lineW = interpolate(frame, [START, START + 26], [0, 100], {\n    extrapolateLeft: 'clamp',\n    extrapolateRight: 'clamp',\n    easing: Easing.out(Easing.cubic),\n  });\n\n  return (\n    <div style={{ width: 1920, height: 1080, background: G.bg, position: 'relative', overflow: 'hidden' }}>\n      <div style={{ position: 'absolute', left: 120, top: 96 }}>\n        <TitleBlock text=\"SPLIT TEXT STAGGER\" size={54} />\n      </div>\n      <div\n        style={{\n          position: 'absolute',\n          left: 0,\n          right: 0,\n          top: 0,\n          bottom: 0,\n          display: 'flex',\n          alignItems: 'center',\n          justifyContent: 'center',\n          flexDirection: 'column',\n        }}\n      >\n        <div style={{ display: 'flex' }}>\n          {chars.map((c, i) => (\n            <div\n              key={i}\n              style={{\n                // 遮罩盒：上方留 0.3em 头部空间容纳过冲，底边即裁切线\n                overflow: 'hidden',\n                height: FONT * 1.35,\n                display: 'flex',\n                alignItems: 'flex-end',\n              }}\n            >\n              <span\n                style={{\n                  display: 'inline-block',\n                  fontFamily: 'Helvetica, Arial, sans-serif',\n                  fontWeight: 800,\n                  fontSize: FONT,\n                  lineHeight: 1.05,\n                  color: G.ink,\n                  letterSpacing: 2,\n                  transform: `translateY(${charY(frame, i)}%)`,\n                }}\n              >\n                {c === ' ' ? ' ' : c}\n              </span>\n            </div>\n          ))}\n        </div>\n        {/* 基线细线：宽度从 0 生长到全文宽 */}\n        <div style={{ width: 920, marginTop: 10, display: 'flex', justifyContent: 'flex-start' }}>\n          <div style={{ height: 2, width: `${lineW}%`, background: G.bar }} />\n        </div>\n      </div>\n    </div>\n  );\n};\n\n\n// ===== demos/typography/type-assembly-moves/TextOnPath.tsx =====\n// 文字沿曲线流入（text-on-path）——字符沿一条上升贝塞尔曲线（像图表增长线）\n// 鱼贯滑入，行进中按切线角旋转；到达各自终点后再从\"贴线姿态\"lerp 到水平基线位\n// 拼成正常标题。曲线本身随字符前进同步 evolve（dashoffset 生长）。\n// 关键帧：字符 i 于 i*2 起跑、45f 沿线到达 t_i（out cubic）→ 到达后停 8f →\n// 12f 摆正到水平基线（y 拉平、rotate→0）→ 全部落定约 f103 → 103–150 真静止。\nimport React from 'react';\nimport { useCurrentFrame, interpolate, Easing } from 'remotion';\nimport { G, TitleBlock } from '../../_fixtures/Fixtures';\n\n// 上升贝塞尔：左下 → 右上，先缓后陡（增长线形状）\nconst P0 = { x: 200, y: 820 };\nconst P1 = { x: 760, y: 810 };\nconst P2 = { x: 1240, y: 660 };\nconst P3 = { x: 1500, y: 320 };\n\nconst bez = (t: number) => {\n  const u = 1 - t;\n  return {\n    x: u * u * u * P0.x + 3 * u * u * t * P1.x + 3 * u * t * t * P2.x + t * t * t * P3.x,\n    y: u * u * u * P0.y + 3 * u * u * t * P1.y + 3 * u * t * t * P2.y + t * t * t * P3.y,\n  };\n};\n// 切线角（度）\nconst tangent = (t: number) => {\n  const u = 1 - t;\n  const dx = 3 * u * u * (P1.x - P0.x) + 6 * u * t * (P2.x - P1.x) + 3 * t * t * (P3.x - P2.x);\n  const dy = 3 * u * u * (P1.y - P0.y) + 6 * u * t * (P2.y - P1.y) + 3 * t * t * (P3.y - P2.y);\n  return (Math.atan2(dy, dx) * 180) / Math.PI;\n};\n\nconst TEXT = 'GROWTH ALL THE WAY';\nconst N = TEXT.length;\nconst CHAR_W = 46; // 64px fontWeight 800 的近似字宽\nconst FINAL_Y = 300; // 水平基线（标题最终落位）\nconst FINAL_X0 = 960 - (N * CHAR_W) / 2;\n\nexport const TextOnPath: React.FC = () => {\n  const frame = useCurrentFrame();\n  // 曲线 evolve：随最前字符推进同步生长\n  const evolve = interpolate(frame, [0, 82], [0, 1], {\n    extrapolateLeft: 'clamp', extrapolateRight: 'clamp',\n    easing: Easing.out(Easing.quad),\n  });\n\n  return (\n    <div style={{ width: 1920, height: 1080, background: G.bg, position: 'relative', overflow: 'hidden' }}>\n      <div style={{ position: 'absolute', left: 120, top: 96 }}>\n        <TitleBlock text=\"TEXT ON PATH\" size={54} />\n      </div>\n      <svg width={1920} height={1080} style={{ position: 'absolute', inset: 0 }}>\n        <path\n          d={`M ${P0.x} ${P0.y} C ${P1.x} ${P1.y}, ${P2.x} ${P2.y}, ${P3.x} ${P3.y}`}\n          fill=\"none\" stroke={G.bar} strokeWidth={3}\n          pathLength={1} strokeDasharray={1} strokeDashoffset={1 - evolve}\n        />\n      </svg>\n      {TEXT.split('').map((ch, i) => {\n        if (ch === ' ') return null;\n        const tEnd = 0.12 + 0.82 * (i / (N - 1)); // 各字符沿线终点\n        const start = i * 2;\n        // 沿线推进：start 起 45f 到达 tEnd\n        const t = interpolate(frame, [start, start + 45], [0, tEnd], {\n          extrapolateLeft: 'clamp', extrapolateRight: 'clamp',\n          easing: Easing.out(Easing.cubic),\n        });\n        const pOnCurve = bez(t);\n        const angOnCurve = tangent(t);\n        // 到达后停 8f，再 12f 摆正到水平基线\n        const settle = interpolate(frame, [start + 53, start + 65], [0, 1], {\n          extrapolateLeft: 'clamp', extrapolateRight: 'clamp',\n          easing: Easing.inOut(Easing.cubic),\n        });\n        const x = pOnCurve.x + (FINAL_X0 + i * CHAR_W - pOnCurve.x) * settle;\n        const y = pOnCurve.y + (FINAL_Y - pOnCurve.y) * settle;\n        const ang = angOnCurve * (1 - settle);\n        const op = interpolate(frame, [start, start + 8], [0, 1], {\n          extrapolateLeft: 'clamp', extrapolateRight: 'clamp',\n        });\n        return (\n          <div key={i} style={{\n            position: 'absolute', left: x, top: y, opacity: op,\n            transform: `translate(-50%, -50%) rotate(${ang}deg)`,\n            fontFamily: 'Helvetica, Arial, sans-serif', fontWeight: 800,\n            fontSize: 64, color: G.ink,\n          }}>\n            {ch}\n          </div>\n        );\n      })}\n    </div>\n  );\n};\n\n\n// ===== demos/typography/type-assembly-moves/TrackingExpandReveal.tsx =====\n// 字距呼吸展开（tracking-expand-reveal）——电影片头字幕惯用的 letter-spacing 入场。\n// 标题 \"BREATHE\"（150px）出场时字符几乎叠压（相当于 letter-spacing -0.42em），\n// 50 帧内展开到 0.14em，同步 blur 10px→0、opacity 0.6→1、scaleX 0.92→1。\n// 不动 letter-spacing 本身（逐帧重排会抖）：容器常量 letterSpacing 0.14em 定终位，\n// 逐字符 span 用 translateX 手动插值——第 i 字起始位移 = (i - 中心) × 每缝差值 84px，\n// 全部挤向词心，位移只与字缝有关、与字宽无关，天然对中。\n// 关键帧：0–50 展开（Easing.out(poly(5)))＋去糊提亮 → 35–58 副标题淡入（≈主词展开 70% 时点）\n// → 58–130 全静止（≥72f，滤镜彻底摘除）。\nimport React from 'react';\nimport { useCurrentFrame, interpolate, Easing } from 'remotion';\nimport { G, TitleBlock } from '../../_fixtures/Fixtures';\n\nconst WORD = 'BREATHE';\nconst FS = 150; // 主词字号\n// 每个字缝的起始-终点差：(-0.42em) - (0.14em) = -0.56em = -84px @150px\nconst GAP_DELTA = -0.56 * FS;\n\nexport const TrackingExpandReveal: React.FC = () => {\n  const frame = useCurrentFrame();\n  // 展开进度 0→1（0–50f，out poly(5)）\n  const p = interpolate(frame, [0, 50], [0, 1], {\n    extrapolateLeft: 'clamp',\n    extrapolateRight: 'clamp',\n    easing: Easing.out(Easing.poly(5)),\n  });\n  const blur = 10 * (1 - p);\n  const op = interpolate(p, [0, 1], [0.6, 1]);\n  const sx = interpolate(p, [0, 1], [0.92, 1]);\n  // 副标题：主词时间轴走到 70%（帧 35）起淡入\n  const subOp = interpolate(frame, [35, 58], [0, 1], {\n    extrapolateLeft: 'clamp',\n    extrapolateRight: 'clamp',\n    easing: Easing.out(Easing.quad),\n  });\n\n  const N = WORD.length;\n  const center = (N - 1) / 2;\n  const settled = frame >= 50; // 展开完成后摘掉一切滤镜/变换，保证逐帧完全相同\n\n  return (\n    <div style={{ width: 1920, height: 1080, background: G.bg, position: 'relative', overflow: 'hidden' }}>\n      <div style={{ position: 'absolute', left: 120, top: 96 }}>\n        <TitleBlock text=\"TRACKING EXPAND REVEAL\" size={54} />\n      </div>\n\n      {/* 主词：容器 letterSpacing 恒为 0.14em（终态），字符仅做 translateX */}\n      <div\n        style={{\n          position: 'absolute',\n          top: 430,\n          left: 0,\n          width: 1920,\n          display: 'flex',\n          justifyContent: 'center',\n          transform: settled ? undefined : `scaleX(${sx})`,\n          filter: settled ? undefined : `blur(${blur}px)`,\n          opacity: settled ? 1 : op,\n        }}\n      >\n        <div\n          style={{\n            fontFamily: 'Helvetica, Arial, sans-serif',\n            fontWeight: 800,\n            fontSize: FS,\n            color: G.ink,\n            letterSpacing: '0.14em',\n            whiteSpace: 'pre',\n            // letter-spacing 只加在字后，整体左移半个缝宽找回视觉对中\n            marginLeft: 0.14 * FS * 0.5,\n          }}\n        >\n          {WORD.split('').map((ch, i) => {\n            const tx = (1 - p) * (i - center) * GAP_DELTA;\n            return (\n              <span\n                key={i}\n                style={{\n                  display: 'inline-block',\n                  transform: settled ? undefined : `translateX(${tx}px)`,\n                }}\n              >\n                {ch}\n              </span>\n            );\n          })}\n        </div>\n      </div>\n\n      {/* 副标题：主词展开 70% 时点淡入 */}\n      <div\n        style={{\n          position: 'absolute',\n          top: 630,\n          left: 0,\n          width: 1920,\n          textAlign: 'center',\n          fontFamily: 'Helvetica, Arial, sans-serif',\n          fontWeight: 500,\n          fontSize: 34,\n          color: G.mid,\n          letterSpacing: '0.32em',\n          opacity: frame >= 58 ? 1 : subOp,\n        }}\n      >\n        A CINEMATIC TITLE ENTRANCE\n      </div>\n    </div>\n  );\n};\n\n\n"
}