{
  "slug": "word-relay-geometry",
  "title": "Word relay geometry",
  "tagline": "Three benefit words each carry their own dedicated geometry relay — a dashed circle spins and shrinks → three solid circles trim-grow in sequence → a metallic sheen sweeps then collapses to pure white; each new word outlines then fills in.",
  "description": "Three benefit words each carry their own dedicated geometry relay — a dashed circle spins and shrinks → three solid circles trim-grow in sequence → a metallic",
  "category": "typography",
  "tags": [
    "shotcraft",
    "remotion",
    "typography"
  ],
  "tier": "free",
  "license": "Apache-2.0",
  "spec": {
    "width": 1920,
    "height": 1080,
    "fps": 30,
    "duration": 6.5,
    "aspect": "16:9"
  },
  "kind": "remotion",
  "params_schema": {
    "type": "object",
    "properties": {}
  },
  "default_params": {},
  "preview": {
    "poster": "https://gimgs.net/motion/media/word-relay-geometry/poster-c58eb48607f9.jpg",
    "video": "https://gimgs.net/motion/media/word-relay-geometry/video-314b487d3e29.mp4"
  },
  "variants": [
    {
      "key": "word-relay-geometry",
      "label": "word-relay-geometry",
      "description": "Three benefit words each carry their own dedicated geometry relay — a dashed big circle spins and shrinks → three solid circles trim-grow in sequence (0.06 phase offset) → a metallic sheen sweeps then collapses to pure white; the old word shrinks to 0.86 and fades, the new word outlines then fills in",
      "use": "",
      "video": "https://gimgs.net/motion/media/word-relay-geometry/video-314b487d3e29.mp4",
      "poster": "https://gimgs.net/motion/media/word-relay-geometry/poster-c58eb48607f9.jpg"
    }
  ],
  "credits": {
    "author": "video-shotcraft (Vincent Wei)",
    "url": "https://github.com/Vincentwei1021/video-shotcraft"
  },
  "page_url": "https://gimgs.net/motion/word-relay-geometry.html",
  "json_url": "https://gimgs.net/motion/word-relay-geometry.json",
  "template_url": "https://gimgs.net/motion/word-relay-geometry/template.html",
  "play_url": null,
  "markdown_url": "https://gimgs.net/motion/word-relay-geometry.md",
  "updated_at": "2026-09-24T07:40:36.561Z",
  "prompt": "## When to use\n\nA three-point benefit statement (faster/better/stronger); a brand-values passage; a mid-film beat needing \"one word, one world\"\n\n## Intention\n\nA three-point benefit statement is easiest to do as three hard-cut text cards. The relay approach instead gives each word **its own exclusive geometry**: a dashed circle is \"spinning\" (fast), three interlocking circles are \"meshing together\" (better), a metallic sweep is \"hard material\" (stronger) — the geometry itself argues for the adjective. The third word gets an upgraded treatment (a sheen sweep plus collapsing to white) because it's the conclusion. 20 drifting background particles stitch the three beats into one continuous space, or it would still just be three cards.\n\n## Duration & energy\n\n- Duration: ~6.0s (180f@30fps: 3 segments each a 0.36 time window, with 0.04 overlap between segments)\n- Energy: Medium-high (a three-beat progression with continuous geometric motion and background particles throughout, no still frames)\n\n## Shot recipe (original Chinese — timing, easing, parameter table, known pitfalls)\n\n## 意图\n三点式利益陈述最容易做成三张字卡硬切。接力的做法是给每个词配一套\n**只属于它的几何**：虚线圆是\"在转\"（快）、三圆相扣是\"互相咬合\"（好）、\n金属扫光是\"材质硬\"（强）——几何本身就在替形容词做论证。第三个词\n留了升格待遇（sheen 扫光 + 收白），因为它是结论。背景 20 颗微尘上浮\n把三拍缝成一个连续空间，不然还是三张卡。\n\n## 动效核心\n- 三段时段带重叠：`[0, 0.36] / [0.32, 0.68] / [0.64, 1.0]`——每段入场窗\n  0.07（≈13f，outCubic），出场窗 0.05（≈9f，inQuad，落在段末），\n  重叠 0.04 是交接而非硬切；末段无出场（收在片尾）\n- 每词双层文字（56px/800）：`outline` 层 `-webkit-text-stroke:1px #6a7186`\n  + 透明填充，`fill` 层纯白——**描边先到位，填充后揭示**\n- 前两词的填充是横向擦入：`fill.clipPath = inset(-20% (1-fillp)·100% -20% 0)`，\n  `fillp = seg(t, t0+0.06, t0+0.18, E.inOutCubic)`，同时 outline 透明度\n  降到 0.25（`1 - fillp·0.75`，留一点描边做重影）\n- 第一词的虚线圆（r=100、`stroke-dasharray:5 7`）：`scale(lerp(grow,0.4,1))`\n  从 0.4 长到 1，同时 `rotate(-90 + t·30)`——**用全片 t 做自转**，所以它在\n  自己那一段里持续转，不是转完就停；第二词的三实线圆（r=62，x 轴\n  -110/0/110）走 SVG trim：`pathLength=1` + `dasharray=1` + `dashoffset`\n  从 1 收到 0，三圆延迟 `d = 0/0.06/0.12` 依次生长，出场时 trim 反向减回去\n- Stronger 的 sheen：`linear-gradient(100deg, …#fff 50%…)` + `background-size:280%`\n  + `background-clip:text`，`backgroundPosition` 从 100% 扫到 0%\n  （窗 t0+0.08→+0.26，inOutCubic），随后 `white = seg(t0+0.27, t0+0.34)`\n  把 sheen 换成纯白 fill 并打 18px 白色辉光\n- 出场统一 `scale(lerp(tout, 1, 0.86))` 缩小淡出（**缩小而非放大**，读作\n  \"退场\"不是\"冲出画面\"）；背景 20 颗微尘\n  `y = (1 - ((t·sp + ph) % 1))·110 - 5`（%），速度 `sp = 0.5 + rand·0.8` 各异\n\n## 参数表\n| 参数 | 典型值 | 调节手感 |\n|------|--------|----------|\n| 三段时段 | 各 0.36（≈65f），重叠 0.04（≈7f） | 每词约 2s 是\"读词 + 看几何\"的下限；重叠 <0.02 读作硬切，>0.08 两词同屏打架 |\n| 入/出场窗 | 入 0.07 outCubic / 出 0.05 inQuad | 出场比入场短是对的（退场要决绝）；出场用 out 曲线会读作\"舍不得走\" |\n| outline→fill | 窗 0.12（≈22f）inOutCubic，outline 留 0.25 | 留一点描边做重影是质感来源，降到 0 字变扁；窗 >0.25 擦入慢得像加载条 |\n| 虚线圆 | r=100、`dash 5 7`、scale 0.4→1、自转 `t·30`° | 自转绑全片 t 所以永不停——这是\"快\"的证据；scale 起点 >0.7 就没有\"长出来\" |\n| trim 生长 | `pathLength=1` + dashoffset 1→0，相位差 0.06 | 相位差是\"依次相扣\"的关键，同时生长读作三个独立圆；窗 0.12 与 fill 同步收敛 |\n| sheen 扫光 | `100deg` 渐变、`background-size:280%`、位置 100%→0% | 280% 保证白带只占字宽一小段（=金属高光）；100% 时整字一起变亮，不是扫光 |\n| 收白 | `seg(t0+0.27, t0+0.34)` ≈13f + 18px 辉光 | 这一下是全片唯一的\"结论\"待遇；删了三个词就是平级，第三点没有落点 |\n| 字号配色 | 56px/800、描边 `#6a7186`、几何 `#565e78`、底 `#07080c`，微尘 20 颗 | 几何色比描边略暗一档是层级，几何比字亮就抢主角；微尘是把三拍缝成一个空间的胶水，>40 颗读作雪 |\n\n## 已知坑\n- demo 在灰阶/占位素材上调校通过——参数是调校起点非实战定稿，\n  首次实战须以真实素材回验\n- **三个词必须是同一句法的三点（demo 都是比较级单词，6–8 字母）**：\n  时段是等分的，词长差大时几何（圆半径、间距）都是写死的坐标，\n  长词会顶出虚线圆或压到相扣圆上——换词表要同步调 `CIRC_R` 与 x 偏移\n- `-webkit-text-stroke` 是前缀属性：Chromium/WebKit 才有，Firefox 下描边层\n  直接是透明（词消失）。Remotion 渲染走 Chromium 无碍，但预览环境要注意\n- SVG trim 用的 `pathLength=1` 归一化在部分老版本浏览器上对 `<circle>`\n  支持不全，退化表现是圆一次性全画出（丢生长感）——移植时先在目标\n  渲染器上验一帧\n- 出场 trim 是\"入场 trim 减出场 seg\"直接相减：两段窗若靠得太近\n  （段长 <0.2）会出现还没长完就开始收，圆只画出一小段\n- sheen 层与 fill 层是叠着的两个 DOM：切换靠透明度交叉，若把 `white` 窗\n  与 `sh` 窗调成重叠不足，会有 1–2 帧两层都半透（字发灰）\n- 第三个词的升格待遇只能给一次：全片三个词都上 sheen 就没有结论感\n  （P4 手法去重精神）；声音上三次入场是三个节奏点，第三个词的收白值得\n  单独一记重音，配法纪律见 sound-design.md\n\n## 参考实现\ndemos/typography/word-relay-geometry/\n（WordRelayGeometry.tsx）\n\n## Reference implementation (Remotion)\n\n- demos/typography/word-relay-geometry/WordRelayGeometry.tsx — https://raw.githubusercontent.com/Vincentwei1021/video-shotcraft/main/demos/typography/word-relay-geometry/WordRelayGeometry.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/word-relay-geometry/WordRelayGeometry.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/word-relay-geometry/WordRelayGeometry.tsx",
      "url": "https://raw.githubusercontent.com/Vincentwei1021/video-shotcraft/main/demos/typography/word-relay-geometry/WordRelayGeometry.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": 195,
    "repo": "https://github.com/Vincentwei1021/video-shotcraft",
    "files": [
      "https://raw.githubusercontent.com/Vincentwei1021/video-shotcraft/main/demos/typography/word-relay-geometry/WordRelayGeometry.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/word-relay-geometry/WordRelayGeometry.tsx =====\n// word-relay-geometry — Word Relay Geometry 利益词几何接力（motion-lab 定稿转原生 Remotion）\n// 三个利益词接力：Faster（虚线大圆）→ Better（三实线圆相扣）→ Stronger（金属 sheen\n// 从左扫到右变纯白）。旧词与几何淡出缩小，新词描边→填充进场，圆路径带 trim 生长感；\n// 背景漂浮微尘粒子。设计坐标 480×270（DesignStage 等比放大），参数以此坐标系标定。\nimport React from 'react';\nimport { DesignStage, E, lerp, rand, seg, useT } from '../../_fixtures/Motion';\n\nexport const WORD_RELAY_GEOMETRY_DURATION = 180; // 6000ms @30fps\n\nconst CIRC_R = 78;\n\n// 微尘粒子（20 个，种子与原 effect.js 完全一致）\nconst PARTS = Array.from({ length: 20 }, (_, i) => ({\n  size: 1 + rand(i * 3) * 1.4,\n  x: rand(i) * 100,\n  ph: rand(i + 40),\n  sp: 0.5 + rand(i + 80) * 0.8,\n}));\n\n// 每个词一段 slot：几何配置 + 时段（in 0.07 + hold + out 0.05）\ntype Geom = { x: number; r: number; dash?: boolean; d?: number };\nconst SLOTS: { label: string; geom: Geom[]; t0: number; t1: number }[] = [\n  { label: 'Faster', geom: [{ x: 0, r: CIRC_R + 22, dash: true }], t0: 0.0, t1: 0.36 },\n  {\n    label: 'Better',\n    geom: [\n      { x: -110, r: 62, d: 0 },\n      { x: 0, r: 62, d: 0.06 },\n      { x: 110, r: 62, d: 0.12 },\n    ],\n    t0: 0.32,\n    t1: 0.68,\n  },\n  { label: 'Stronger', geom: [], t0: 0.64, t1: 1.0 },\n];\n\nexport const WordRelayGeometry: React.FC = () => {\n  const t = useT();\n  return (\n    <DesignStage bg=\"#07080c\">\n      {/* 微尘粒子：缓慢上浮 loop + 呼吸闪烁 */}\n      {PARTS.map((p, i) => {\n        const y = (1 - ((t * p.sp + p.ph) % 1)) * 110 - 5;\n        return (\n          <div\n            key={i}\n            style={{\n              position: 'absolute',\n              width: p.size,\n              height: p.size,\n              borderRadius: '50%',\n              background: '#fff',\n              left: `${p.x}%`,\n              top: `${y}%`,\n              opacity: 0.12 + 0.18 * Math.sin((t * 3 + p.ph) * Math.PI * 2) ** 2,\n            }}\n          />\n        );\n      })}\n\n      {SLOTS.map(({ label, geom, t0, t1 }, i) => {\n        const isLast = i === SLOTS.length - 1;\n        const tin = seg(t, t0, t0 + 0.07, E.outCubic);\n        const tout = isLast ? 0 : seg(t, t1 - 0.05, t1, E.inQuad);\n        const alive = tin > 0 && tout < 1;\n\n        // Faster/Better：outline→fill 左起 clip 揭示\n        const fillp = seg(t, t0 + 0.06, t0 + 0.18, E.inOutCubic);\n        // Stronger：描边→sheen 扫光→收纯白\n        const sh = seg(t, t0 + 0.05, t0 + 0.2, E.outQuad); // 出现（描边→sheen）\n        const sweep = seg(t, t0 + 0.08, t0 + 0.26, E.inOutCubic); // 扫光位置\n        const white = seg(t, t0 + 0.27, t0 + 0.34, E.outQuad); // 收为纯白\n\n        return (\n          <div key={label} style={{ position: 'absolute', inset: 0, opacity: alive ? tin * (1 - tout) : 0 }}>\n            <svg\n              viewBox=\"-240 -135 480 270\"\n              style={{ position: 'absolute', inset: 0, width: '100%', height: '100%' }}\n            >\n              {geom.map((g, k) => {\n                if (g.dash) {\n                  // 虚线大圆：整体从 0.4 生长到 1 并持续慢转\n                  const grow = seg(t, t0 + 0.01, t0 + 0.14, E.outCubic);\n                  return (\n                    <circle\n                      key={k}\n                      cx={g.x}\n                      cy={0}\n                      r={g.r}\n                      fill=\"none\"\n                      stroke=\"#565e78\"\n                      strokeWidth={1.1}\n                      strokeDasharray=\"5 7\"\n                      opacity={grow * (1 - tout)}\n                      transform={`rotate(${-90 + t * 30}) scale(${lerp(grow, 0.4, 1)})`}\n                    />\n                  );\n                }\n                // 实线圆：pathLength 归一化 trim 生长 / 出场反向消隐\n                const trim =\n                  seg(t, t0 + 0.02 + (g.d || 0), t0 + 0.14 + (g.d || 0), E.outCubic) -\n                  seg(t, t1 - 0.06, t1, E.inQuad) * (isLast ? 0 : 1);\n                return (\n                  <circle\n                    key={k}\n                    cx={g.x}\n                    cy={0}\n                    r={g.r}\n                    fill=\"none\"\n                    stroke=\"#565e78\"\n                    strokeWidth={1.1}\n                    pathLength={1}\n                    strokeDasharray=\"1\"\n                    strokeDashoffset={1 - Math.max(0, trim)}\n                    transform=\"rotate(-90)\"\n                  />\n                );\n              })}\n            </svg>\n            {/* 词组：outline 打底，fill/sheen 绝对层叠 */}\n            <div\n              style={{\n                position: 'absolute',\n                left: '50%',\n                top: '50%',\n                transform: `translate(-50%,-52%) scale(${lerp(tout, 1, 0.86)})`,\n                fontFamily: '-apple-system,\"Helvetica Neue\",sans-serif',\n                fontSize: 56,\n                fontWeight: 800,\n                lineHeight: 1,\n              }}\n            >\n              <div\n                style={{\n                  color: 'transparent',\n                  WebkitTextStroke: '1px #6a7186',\n                  opacity: isLast ? 1 - sh : 1 - fillp * 0.75,\n                }}\n              >\n                {label}\n              </div>\n              {isLast ? (\n                <>\n                  <div\n                    style={{\n                      position: 'absolute',\n                      inset: 0,\n                      color: '#fff',\n                      opacity: white,\n                      textShadow: `0 0 ${white * 18}px rgba(255,255,255,.3)`,\n                    }}\n                  >\n                    {label}\n                  </div>\n                  {/* 金属 sheen：渐变 background-clip 文字，背景位从右扫到左 */}\n                  <div\n                    style={{\n                      position: 'absolute',\n                      inset: 0,\n                      color: 'transparent',\n                      backgroundImage:\n                        'linear-gradient(100deg,#585f72 0%,#8d95aa 38%,#ffffff 50%,#8d95aa 62%,#585f72 100%)',\n                      backgroundSize: '280% 100%',\n                      WebkitBackgroundClip: 'text',\n                      backgroundClip: 'text',\n                      opacity: sh * (1 - white),\n                      backgroundPosition: `${lerp(sweep, 100, 0)}% 0`,\n                    }}\n                  >\n                    {label}\n                  </div>\n                </>\n              ) : (\n                <div\n                  style={{\n                    position: 'absolute',\n                    inset: 0,\n                    color: '#fff',\n                    clipPath: `inset(-20% ${(1 - fillp) * 100}% -20% 0)`,\n                  }}\n                >\n                  {label}\n                </div>\n              )}\n            </div>\n          </div>\n        );\n      })}\n    </DesignStage>\n  );\n};\n\n\n"
}