{
  "slug": "outline-word-fill",
  "title": "Outline word fill-in",
  "tagline": "A hollow-outline word snaps in from 3.2x scale, a dashed circle and guidelines close in, then the outline instantly fills solid white.",
  "description": "A hollow-outline word snaps in from 3.2x scale, a dashed circle and guidelines close in, then the outline instantly fills solid white.",
  "category": "typography",
  "tags": [
    "shotcraft",
    "remotion",
    "typography"
  ],
  "tier": "free",
  "license": "Apache-2.0",
  "spec": {
    "width": 1920,
    "height": 1080,
    "fps": 30,
    "duration": 3,
    "aspect": "16:9"
  },
  "kind": "remotion",
  "params_schema": {
    "type": "object",
    "properties": {}
  },
  "default_params": {},
  "preview": {
    "poster": "https://gimgs.net/motion/media/outline-word-fill/poster-362082b2e6ba.jpg",
    "video": "https://gimgs.net/motion/media/outline-word-fill/video-69472c261fe3.mp4"
  },
  "variants": [
    {
      "key": "outline-word-fill",
      "label": "outline-word-fill",
      "description": "A hollow word (1px gray outline, weight 500) snaps in from 3.2x scale with a sharp ease-out, then a large dashed circle contracts in from 2.8x around the letters and rotates slowly, horizontal dashed lines extend in from the frame edges; the outline brightens slightly, then solid white lights up instantly within 0.6 frames, and a single glow flash holds.",
      "use": "",
      "video": "https://gimgs.net/motion/media/outline-word-fill/video-69472c261fe3.mp4",
      "poster": "https://gimgs.net/motion/media/outline-word-fill/poster-362082b2e6ba.jpg"
    }
  ],
  "credits": {
    "author": "video-shotcraft (Vincent Wei)",
    "url": "https://github.com/Vincentwei1021/video-shotcraft"
  },
  "page_url": "https://gimgs.net/motion/outline-word-fill.html",
  "json_url": "https://gimgs.net/motion/outline-word-fill.json",
  "template_url": "https://gimgs.net/motion/outline-word-fill/template.html",
  "play_url": null,
  "markdown_url": "https://gimgs.net/motion/outline-word-fill.md",
  "updated_at": "2026-09-24T07:40:10.163Z",
  "prompt": "## When to use\n\nA hard hammer-beat for a single-word value prop/slogan; a \"nail-down\" shot on a rhythm cue point; an emphasis frame in dark-background brand films\n\n## Intention\n\nNailing down a word isn't done by fading it in slowly, it's done with **two landings**: first the giant hollow text snaps in fast from oversized scale (the size landing), then the outline turns solid white in a single frame (the material landing). The 1.5s in between, where the dashed circle contracts and the horizontal dashed guides extend inward, is an \"aiming\" process — technical-drawing vocabulary that makes the light-up read as \"confirmation.\" The fill **must be instantaneous** — a slow sweep fill turns the nail into a slow decorative animation.\n\n## Duration & energy\n\n- Duration: About 2.5s (75f@30fps, a short single beat)\n- Energy: High (3.2x contraction + instant light-up, high impact in a short time)\n\n## Shot recipe (original Chinese — timing, easing, parameter table, known pitfalls)\n\n## 意图\n一个词要打成钉子，靠的不是慢慢淡入而是**两次落定**：先是巨大空心字\n急速收缩到位（这是尺寸的落定），再是描边在一帧内变实心白（这是质感的\n落定）。中间那 1.5s 的虚线圆收缩与水平虚线内伸是\"瞄准\"过程——技术\n制图语汇，让点亮那一下读作\"确认\"。填充**必须是瞬时**的：慢扫填充会把\n钉子变成缓慢的美化动画。\n\n## 动效核心\n- 空心字急缓收缩：`zoom = seg(t, 0.05, 0.19, E.outCubic)`（约 4f→14f，\n  仅 10f），`scale(lerp(zoom, 3.2, 1))`；同期 `born = seg(t, 0.05, 0.14)`\n  淡入——**收缩窗只 10f**，快到几乎是一次冲击\n- 双层字：`outline`（`color:transparent` + `-webkit-text-stroke:1px #565b63`）\n  与 `solid`（纯白，初始 opacity 0）叠放，48px / **500 字重**\n  （中等字重，不是标题的 800——空心字太粗会糊成块）\n- 虚线大圆（SVG r=88、`dasharray:6 8`）：出现 `cin = seg(0.16, 0.3)`，\n  收缩 `shrink = seg(0.16, 0.76, E.outCubic)` 从 2.8 倍收到 1——\n  **收缩窗 45f，横跨大半片**，是全片最慢的一条曲线；同时 `rotate(t·18)`\n  全程缓慢自转\n- 左右水平虚线（`dasharray:5 7`）：`ext = seg(0.5, 0.72, E.outCubic)`，x2 从\n  ±240 向内推进 144（画框边缘 → 圆边），透明度同步等于 ext；随后\n  **瞬时填充** `pop = seg(t, 0.742, 0.762)`——窗仅 0.02（约 0.6 帧），\n  solid 与 outline 透明度硬交叉，视觉上是硬切\n- 一闪即逝的辉光：`flash = pop · (1 - seg(0.762, 0.86, E.outQuad))`，\n  `0 0 flash·16px rgba(255,255,255,.45)`，约 7f 内衰完后定格纯白\n- 描边预热：`bright = seg(0.66, 0.73, E.outQuad)`，灰度 86→145 —— 点亮前\n  先亮一档，给那 0.6 帧的硬切一个\"充能\"前奏；底层 14 颗静态微尘\n  （不动，随机位置 + 0.06–0.2 透明度）\n\n## 参数表\n| 参数 | 典型值 | 调节手感 |\n|------|--------|----------|\n| 收缩 | `scale 3.2 → 1`，窗 10f，outCubic | 3.2 倍是\"从脸前退开\"的量；<2 倍读作普通缩放；窗 >20f 就成了缓慢 zoom，冲击全失 |\n| 字重字号 | 48px / **500** / `letter-spacing:.5px`，占画宽约三成 | 空心字上 700+ 会让 1px 描边包住一大块空白，读作气球字；500 是空心字的甜点 |\n| 描边 | `1px #565b63`，点亮前预热到灰度 145 | 1px 细描边是\"制图\"语汇；2px+ 变涂鸦；预热那一档删了硬切显得没由来 |\n| 虚线圆 | r=88、`dash 6 8`、scale 2.8→1（45f）、自转 `t·18`° | 45f 慢收缩是全片的\"呼吸\"，与 10f 的字收缩形成快慢对比；窗压到 20f 两者撞在一起 |\n| 水平虚线 | `dash 5 7`，从 ±240 内伸 144，窗 0.5→0.72 | 内伸终点要停在圆边（不进圆内）；伸满全宽读作十字准星，过头了 |\n| 瞬时填充 | `seg(0.742, 0.762)`（≈0.6f） | **这一下是全卡的目的**：任何形式的慢扫（>3f）都会把钉子降级成美化动画 |\n| 辉光 | 峰值 16px，`rgba(255,255,255,.45)`，7f 衰完 | 一闪即逝才像\"点亮\"；持续辉光读作发光字体，质感立刻廉价 |\n| 微尘 | 14 颗，静态不动 | 静态是对的（本卡只 2.5s，动尘会抢戏）；数量 >25 底噪太脏 |\n\n## 已知坑\n- demo 在灰阶/占位素材上调校通过——参数是调校起点非实战定稿，\n  首次实战须以真实素材回验\n- **只适合单个短词（demo `Faster`，6 字母）**：字号与虚线圆半径是写死的\n  一组坐标，字一长就顶穿圆、水平虚线内伸终点也会压到字上——换成\n  两个词或中文四字须同步调 `font-size` / `r` / 内伸距离三处\n- `-webkit-text-stroke` 是前缀属性，Chromium/WebKit 才生效；Firefox 下\n  outline 层是全透明（词直到 0.742 才突然出现）。Remotion 走 Chromium 无碍\n- 瞬时填充窗 `0.02` 在 75f 片长下不足一帧：这是**故意的**（要的就是硬切），\n  但如果把本卡拉长到 150f+，这 0.02 会变成 3 帧的可见渐变——拉长时长\n  必须把窗按比例压回去\n- 时间窗注释里的秒数（4.83→5.15s 等）是原片时间轴的残留，与本卡\n  0–2.5s 的 t 无关，读代码时别被误导\n- 深底专用：`#050505` 底 + 白描边 + 辉光是效果的全部；浅底上空心字\n  几乎不可见，需整套重配\n- 声音强依赖：点亮那一帧必须有一记短促重音（金属/开关/低频撞击），\n  这是本卡\"钉子\"感的一半，配法纪律见 sound-design.md；与\n  word-relay-geometry 分工：那卡是三词接力（本卡是它第一段的独立化 +\n  升级版），同片二者不并置\n\n## 参考实现\ndemos/typography/outline-word-fill/\n（OutlineWordFill.tsx）\n\n## Reference implementation (Remotion)\n\n- demos/typography/outline-word-fill/OutlineWordFill.tsx — https://raw.githubusercontent.com/Vincentwei1021/video-shotcraft/main/demos/typography/outline-word-fill/OutlineWordFill.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/outline-word-fill/OutlineWordFill.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/outline-word-fill/OutlineWordFill.tsx",
      "url": "https://raw.githubusercontent.com/Vincentwei1021/video-shotcraft/main/demos/typography/outline-word-fill/OutlineWordFill.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": 90,
    "repo": "https://github.com/Vincentwei1021/video-shotcraft",
    "files": [
      "https://raw.githubusercontent.com/Vincentwei1021/video-shotcraft/main/demos/typography/outline-word-fill/OutlineWordFill.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/outline-word-fill/OutlineWordFill.tsx =====\n// outline-word-fill — Outline→Solid Fill 空心字辉光填充（motion-lab 定稿转原生 Remotion）\n// 空心 \"Faster\"（细灰描边、中等字重）从约 3.2 倍巨大尺寸急缓收缩落位居中；\n// 背后巨大虚线圆随后从画外持续收缩到字周围并缓慢自转，两侧水平虚线从画框边缘向内伸向圆；\n// 描边先微微增亮，随即实心白色一帧内瞬间点亮（无慢扫），带一闪即逝的微辉光后定格纯白。\n// 设计坐标 480×270（DesignStage 等比放大），参数表数值以此坐标系标定。\nimport React from 'react';\nimport { DesignStage, E, lerp, rand, seg, useT } from '../../_fixtures/Motion';\n\nexport const OUTLINE_WORD_FILL_DURATION = 75; // 2500ms @30fps\n\n// 背景微尘：位置/尺寸/透明度全部种子驱动，跨帧静止\nconst DUST = Array.from({ length: 14 }, (_, i) => ({\n  size: 1 + rand(i * 7 + 2) * 1.2,\n  opacity: 0.06 + rand(i + 55) * 0.14,\n  left: `${rand(i + 13) * 100}%`,\n  top: `${rand(i + 29) * 100}%`,\n}));\n\nexport const OutlineWordFill: React.FC = () => {\n  const t = useT();\n  // 空心字从 ~3.2x 急缓收缩落位（原片 4.83→5.15s）\n  const born = seg(t, 0.05, 0.14, E.outCubic);\n  const zoom = seg(t, 0.05, 0.19, E.outCubic);\n  // 描边临近点亮前微微增亮（6.4→6.55s）\n  const bright = seg(t, 0.66, 0.73, E.outQuad);\n  const gv = Math.round(lerp(bright, 86, 145));\n  // 虚线圆：字落位后出现，从画外一路收缩到字周围（5.1→6.6s）\n  const cin = seg(t, 0.16, 0.3, E.outQuad);\n  const shrink = seg(t, 0.16, 0.76, E.outCubic);\n  // 水平虚线从画框边缘向内伸向圆（5.9→6.5s）\n  const ext = seg(t, 0.5, 0.72, E.outCubic);\n  // 实心白一帧内瞬间点亮（原片 6.54→6.56s 硬切，无慢扫）\n  const pop = seg(t, 0.742, 0.762);\n  // 一闪即逝的微辉光，随后定格纯白\n  const flash = pop * (1 - seg(t, 0.762, 0.86, E.outQuad));\n  // 注：原 effect.js 背景为 #050505，但样片 mp4 经 x264 编码后底色实测为 rgb(3,3,3)\n  // （暗部量化损失），此处按样片实际电平取 #030303 保证帧一致性。\n  return (\n    <DesignStage bg=\"#030303\">\n      <div style={{ position: 'absolute', inset: 0, background: '#030303', overflow: 'hidden' }}>\n        {/* 背景微尘 */}\n        {DUST.map(({ size, opacity, left, top }, i) => (\n          <div\n            key={i}\n            style={{\n              position: 'absolute',\n              width: size,\n              height: size,\n              borderRadius: '50%',\n              background: '#fff',\n              opacity,\n              left,\n              top,\n            }}\n          />\n        ))}\n        {/* 虚线大圆（SVG，从画外收缩落位 + 缓慢自转）+ 左右水平虚线 */}\n        <svg\n          viewBox=\"-240 -135 480 270\"\n          style={{ position: 'absolute', inset: 0, width: '100%', height: '100%' }}\n        >\n          <g transform={`scale(${lerp(shrink, 2.8, 1)}) rotate(${t * 18})`}>\n            <circle\n              cx={0}\n              cy={0}\n              r={88}\n              fill=\"none\"\n              stroke=\"#7d838e\"\n              strokeWidth={1}\n              strokeDasharray=\"6 8\"\n              opacity={cin * 0.85}\n            />\n          </g>\n          <line\n            y1={0}\n            y2={0}\n            x1={-240}\n            x2={-240 + ext * 144}\n            stroke=\"#7d838e\"\n            strokeWidth={1}\n            strokeDasharray=\"5 7\"\n            opacity={ext}\n          />\n          <line\n            y1={0}\n            y2={0}\n            x1={240}\n            x2={240 - ext * 144}\n            stroke=\"#7d838e\"\n            strokeWidth={1}\n            strokeDasharray=\"5 7\"\n            opacity={ext}\n          />\n        </svg>\n        {/* 双层文字（中等字重，占画宽约三成）：底层描边、顶层实心白 */}\n        <div\n          style={{\n            position: 'absolute',\n            left: '50%',\n            top: '50%',\n            fontFamily: '-apple-system,\"Helvetica Neue\",sans-serif',\n            fontSize: 48,\n            fontWeight: 500,\n            letterSpacing: 0.5,\n            lineHeight: 1,\n            opacity: born,\n            transform: `translate(-50%,-53%) scale(${lerp(zoom, 3.2, 1)})`,\n          }}\n        >\n          <div\n            style={{\n              color: 'transparent',\n              WebkitTextStroke: `1px rgb(${gv - 4},${gv},${gv + 8})`,\n              opacity: 1 - pop,\n            }}\n          >\n            Faster\n          </div>\n          <div\n            style={{\n              position: 'absolute',\n              inset: 0,\n              color: '#fff',\n              opacity: pop,\n              textShadow:\n                flash > 0.01 ? `0 0 ${flash * 16}px rgba(255,255,255,${flash * 0.45})` : 'none',\n            }}\n          >\n            Faster\n          </div>\n        </div>\n      </div>\n    </DesignStage>\n  );\n};\n\n\n"
}