Motion · Typography

Outline word fill-in

A hollow-outline word snaps in from 3.2x scale, a dashed circle and guidelines close in, then the outline instantly fills solid white.

1920×108016:930 fps3s · 90 framesremotionLicense: Apache-2.0#shotcraft#remotion#typography

Source: video-shotcraft (Vincent Wei) ↗ · Apache-2.0 · Remotion composition (React/TSX) — preview video, no in-browser player

When to use

A 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

Intention

Nailing 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.

Duration & energy

  • Duration: About 2.5s (75f@30fps, a short single beat)
  • Energy: High (3.2x contraction + instant light-up, high impact in a short time)

Shot recipe (original Chinese — timing, easing, parameter table, known pitfalls)

意图

一个词要打成钉子,靠的不是慢慢淡入而是两次落定:先是巨大空心字 急速收缩到位(这是尺寸的落定),再是描边在一帧内变实心白(这是质感的 落定)。中间那 1.5s 的虚线圆收缩与水平虚线内伸是"瞄准"过程——技术 制图语汇,让点亮那一下读作"确认"。填充必须是瞬时的:慢扫填充会把 钉子变成缓慢的美化动画。

动效核心

  • 空心字急缓收缩:zoom = seg(t, 0.05, 0.19, E.outCubic)(约 4f→14f,

仅 10f),scale(lerp(zoom, 3.2, 1));同期 born = seg(t, 0.05, 0.14) 淡入——收缩窗只 10f,快到几乎是一次冲击

  • 双层字:outline(color:transparent + -webkit-text-stroke:1px #565b63)

与 solid(纯白,初始 opacity 0)叠放,48px / 500 字重 (中等字重,不是标题的 800——空心字太粗会糊成块)

  • 虚线大圆(SVG r=88、dasharray:6 8):出现 cin = seg(0.16, 0.3),

收缩 shrink = seg(0.16, 0.76, E.outCubic) 从 2.8 倍收到 1—— 收缩窗 45f,横跨大半片,是全片最慢的一条曲线;同时 rotate(t·18) 全程缓慢自转

  • 左右水平虚线(dasharray:5 7):ext = seg(0.5, 0.72, E.outCubic),x2 从

±240 向内推进 144(画框边缘 → 圆边),透明度同步等于 ext;随后 瞬时填充 pop = seg(t, 0.742, 0.762)——窗仅 0.02(约 0.6 帧), solid 与 outline 透明度硬交叉,视觉上是硬切

  • 一闪即逝的辉光:flash = pop · (1 - seg(0.762, 0.86, E.outQuad)),

0 0 flash·16px rgba(255,255,255,.45),约 7f 内衰完后定格纯白

  • 描边预热:bright = seg(0.66, 0.73, E.outQuad),灰度 86→145 —— 点亮前

先亮一档,给那 0.6 帧的硬切一个"充能"前奏;底层 14 颗静态微尘 (不动,随机位置 + 0.06–0.2 透明度)

参数表

| 参数 | 典型值 | 调节手感 | |------|--------|----------| | 收缩 | scale 3.2 → 1,窗 10f,outCubic | 3.2 倍是"从脸前退开"的量;<2 倍读作普通缩放;窗 >20f 就成了缓慢 zoom,冲击全失 | | 字重字号 | 48px / 500 / letter-spacing:.5px,占画宽约三成 | 空心字上 700+ 会让 1px 描边包住一大块空白,读作气球字;500 是空心字的甜点 | | 描边 | 1px #565b63,点亮前预热到灰度 145 | 1px 细描边是"制图"语汇;2px+ 变涂鸦;预热那一档删了硬切显得没由来 | | 虚线圆 | r=88、dash 6 8、scale 2.8→1(45f)、自转 t·18° | 45f 慢收缩是全片的"呼吸",与 10f 的字收缩形成快慢对比;窗压到 20f 两者撞在一起 | | 水平虚线 | dash 5 7,从 ±240 内伸 144,窗 0.5→0.72 | 内伸终点要停在圆边(不进圆内);伸满全宽读作十字准星,过头了 | | 瞬时填充 | seg(0.742, 0.762)(≈0.6f) | 这一下是全卡的目的:任何形式的慢扫(>3f)都会把钉子降级成美化动画 | | 辉光 | 峰值 16px,rgba(255,255,255,.45),7f 衰完 | 一闪即逝才像"点亮";持续辉光读作发光字体,质感立刻廉价 | | 微尘 | 14 颗,静态不动 | 静态是对的(本卡只 2.5s,动尘会抢戏);数量 >25 底噪太脏 |

已知坑

  • demo 在灰阶/占位素材上调校通过——参数是调校起点非实战定稿,

首次实战须以真实素材回验

  • 只适合单个短词(demo Faster,6 字母):字号与虚线圆半径是写死的

一组坐标,字一长就顶穿圆、水平虚线内伸终点也会压到字上——换成 两个词或中文四字须同步调 font-size / r / 内伸距离三处

  • -webkit-text-stroke 是前缀属性,Chromium/WebKit 才生效;Firefox 下

outline 层是全透明(词直到 0.742 才突然出现)。Remotion 走 Chromium 无碍

  • 瞬时填充窗 0.02 在 75f 片长下不足一帧:这是故意的(要的就是硬切),

但如果把本卡拉长到 150f+,这 0.02 会变成 3 帧的可见渐变——拉长时长 必须把窗按比例压回去

  • 时间窗注释里的秒数(4.83→5.15s 等)是原片时间轴的残留,与本卡

0–2.5s 的 t 无关,读代码时别被误导

  • 深底专用:#050505 底 + 白描边 + 辉光是效果的全部;浅底上空心字

几乎不可见,需整套重配

  • 声音强依赖:点亮那一帧必须有一记短促重音(金属/开关/低频撞击),

这是本卡"钉子"感的一半,配法纪律见 sound-design.md;与 word-relay-geometry 分工:那卡是三词接力(本卡是它第一段的独立化 + 升级版),同片二者不并置

参考实现

demos/typography/outline-word-fill/ (OutlineWordFill.tsx)

Reference implementation (Remotion)

  • demos/typography/outline-word-fill/OutlineWordFill.tsx — https://raw.githubusercontent.com/Vincentwei1021/video-shotcraft/main/demos/typography/outline-word-fill/OutlineWordFill.tsx

The component imports shared fixtures from demos/_fixtures and helpers from assets/lib in the repo; SKILL.md there documents the render workflow.

Everything on this page is also machine-readable. Point your agent at the JSON, or add the gimgs MCP server and ask it to search motion templates.

  1. Fetch the JSON — it holds the shot recipe (timing, easing, parameter table, pitfalls) and the full TSX source.
  2. Clone the video-shotcraft repo; the component imports shared fixtures from demos/_fixtures and helpers from assets/lib.
  3. Register the component as a <Composition> in a Remotion project and render with `npx remotion render`, or re-implement the recipe in your own engine.
curl -s https://gimgs.net/motion/outline-word-fill.json
git clone https://github.com/Vincentwei1021/video-shotcraft
# component: demos/typography/outline-word-fill/OutlineWordFill.tsx
npx remotion render <CompositionId> out.mp4
claude mcp add --transport http gimgs https://gimgs.net/mcp
# then: search_motion_templates / get_motion_template("outline-word-fill")

Prompt

Use the video-shotcraft shot recipe "Outline word fill-in" (Remotion composition, Apache-2.0) published on gimgs.net.
Recipe JSON (prompt = timing/easing/parameter table/pitfalls, files = TSX source): https://gimgs.net/motion/outline-word-fill.json
Raw TSX: https://gimgs.net/motion/outline-word-fill/template.html
Repository: https://github.com/Vincentwei1021/video-shotcraft  (component: demos/typography/outline-word-fill/OutlineWordFill.tsx; imports demos/_fixtures and assets/lib)
Spec: 1920x1080, 30 fps, 3s

1. Read the JSON and follow its "prompt" section (when to use, intention, parameter table, known pitfalls).
2. Adapt it for: <describe what you want>.
3. Either clone the repo, register the component as a <Composition> in a Remotion project and run npx remotion render <CompositionId> out.mp4 — or re-implement the recipe in your own engine (HTML/WAAPI works: keep the same timing and easing).

JSON · Markdown · llms.txt

4,202 chars · Apache-2.0Open raw template ↗
// ===== demos/typography/outline-word-fill/OutlineWordFill.tsx =====
// outline-word-fill — Outline→Solid Fill 空心字辉光填充(motion-lab 定稿转原生 Remotion)
// 空心 "Faster"(细灰描边、中等字重)从约 3.2 倍巨大尺寸急缓收缩落位居中;
// 背后巨大虚线圆随后从画外持续收缩到字周围并缓慢自转,两侧水平虚线从画框边缘向内伸向圆;
// 描边先微微增亮,随即实心白色一帧内瞬间点亮(无慢扫),带一闪即逝的微辉光后定格纯白。
// 设计坐标 480×270(DesignStage 等比放大),参数表数值以此坐标系标定。
import React from 'react';
import { DesignStage, E, lerp, rand, seg, useT } from '../../_fixtures/Motion';

export const OUTLINE_WORD_FILL_DURATION = 75; // 2500ms @30fps

// 背景微尘:位置/尺寸/透明度全部种子驱动,跨帧静止
const DUST = Array.from({ length: 14 }, (_, i) => ({
  size: 1 + rand(i * 7 + 2) * 1.2,
  opacity: 0.06 + rand(i + 55) * 0.14,
  left: `${rand(i + 13) * 100}%`,
  top: `${rand(i + 29) * 100}%`,
}));

export const OutlineWordFill: React.FC = () => {
  const t = useT();
  // 空心字从 ~3.2x 急缓收缩落位(原片 4.83→5.15s)
  const born = seg(t, 0.05, 0.14, E.outCubic);
  const zoom = seg(t, 0.05, 0.19, E.outCubic);
  // 描边临近点亮前微微增亮(6.4→6.55s)
  const bright = seg(t, 0.66, 0.73, E.outQuad);
  const gv = Math.round(lerp(bright, 86, 145));
  // 虚线圆:字落位后出现,从画外一路收缩到字周围(5.1→6.6s)
  const cin = seg(t, 0.16, 0.3, E.outQuad);
  const shrink = seg(t, 0.16, 0.76, E.outCubic);
  // 水平虚线从画框边缘向内伸向圆(5.9→6.5s)
  const ext = seg(t, 0.5, 0.72, E.outCubic);
  // 实心白一帧内瞬间点亮(原片 6.54→6.56s 硬切,无慢扫)
  const pop = seg(t, 0.742, 0.762);
  // 一闪即逝的微辉光,随后定格纯白
  const flash = pop * (1 - seg(t, 0.762, 0.86, E.outQuad));
  // 注:原 effect.js 背景为 #050505,但样片 mp4 经 x264 编码后底色实测为 rgb(3,3,3)
  // (暗部量化损失),此处按样片实际电平取 #030303 保证帧一致性。
  return (
    <DesignStage bg="#030303">
      <div style={{ position: 'absolute', inset: 0, background: '#030303', overflow: 'hidden' }}>
        {/* 背景微尘 */}
        {DUST.map(({ size, opacity, left, top }, i) => (
          <div
            key={i}
            style={{
              position: 'absolute',
              width: size,
              height: size,
              borderRadius: '50%',
              background: '#fff',
              opacity,
              left,
              top,
            }}
          />
        ))}
        {/* 虚线大圆(SVG,从画外收缩落位 + 缓慢自转)+ 左右水平虚线 */}
        <svg
          viewBox="-240 -135 480 270"
          style={{ position: 'absolute', inset: 0, width: '100%', height: '100%' }}
        >
          <g transform={`scale(${lerp(shrink, 2.8, 1)}) rotate(${t * 18})`}>
            <circle
              cx={0}
              cy={0}
              r={88}
              fill="none"
              stroke="#7d838e"
              strokeWidth={1}
              strokeDasharray="6 8"
              opacity={cin * 0.85}
            />
          </g>
          <line
            y1={0}
            y2={0}
            x1={-240}
            x2={-240 + ext * 144}
            stroke="#7d838e"
            strokeWidth={1}
            strokeDasharray="5 7"
            opacity={ext}
          />
          <line
            y1={0}
            y2={0}
            x1={240}
            x2={240 - ext * 144}
            stroke="#7d838e"
            strokeWidth={1}
            strokeDasharray="5 7"
            opacity={ext}
          />
        </svg>
        {/* 双层文字(中等字重,占画宽约三成):底层描边、顶层实心白 */}
        <div
          style={{
            position: 'absolute',
            left: '50%',
            top: '50%',
            fontFamily: '-apple-system,"Helvetica Neue",sans-serif',
            fontSize: 48,
            fontWeight: 500,
            letterSpacing: 0.5,
            lineHeight: 1,
            opacity: born,
            transform: `translate(-50%,-53%) scale(${lerp(zoom, 3.2, 1)})`,
          }}
        >
          <div
            style={{
              color: 'transparent',
              WebkitTextStroke: `1px rgb(${gv - 4},${gv},${gv + 8})`,
              opacity: 1 - pop,
            }}
          >
            Faster
          </div>
          <div
            style={{
              position: 'absolute',
              inset: 0,
              color: '#fff',
              opacity: pop,
              textShadow:
                flash > 0.01 ? `0 0 ${flash * 16}px rgba(255,255,255,${flash * 0.45})` : 'none',
            }}
          >
            Faster
          </div>
        </div>
      </div>
    </DesignStage>
  );
};


  1. Read the recipe
    Prompt = when to use, intention, parameter table with typical values and how each one feels, known pitfalls. Treat values as calibrated starting points.
  2. Get the code
    tool: git
    git clone https://github.com/Vincentwei1021/video-shotcraft && cd video-shotcraft && npm install
  3. Register + render
    tool: remotion
    npx remotion render <CompositionId> out.mp4 # component: demos/typography/outline-word-fill/OutlineWordFill.tsx
  4. Or re-implement
    Port the timing/easing from the recipe to HTML + WAAPI (gimgs motion contract) or any engine; keep the same segment windows and easing.
paramtypedefaultdescription
—

More in this category