{
  "slug": "text-as-mask",
  "title": "Text-as-video mask",
  "tagline": "Text as video mask — extra-bold title letters reveal slowly panning product footage through them; at the end the letterforms scale up 26x and overflow, and the inner footage takes over the full screen.",
  "description": "Text as video mask — extra-bold title letters reveal slowly panning product footage through them; at the end the letterforms scale up 26x and overflow, and th",
  "category": "intro",
  "tags": [
    "shotcraft",
    "remotion",
    "intro",
    "opening",
    "typography"
  ],
  "tier": "free",
  "license": "Apache-2.0",
  "spec": {
    "width": 1920,
    "height": 1080,
    "fps": 30,
    "duration": 5.06,
    "aspect": "16:9"
  },
  "kind": "remotion",
  "params_schema": {
    "type": "object",
    "properties": {}
  },
  "default_params": {},
  "preview": {
    "poster": "https://gimgs.net/motion/media/text-as-mask/poster-662123bf9c2e.jpg",
    "video": "https://gimgs.net/motion/media/text-as-mask/video-149301bddf5a.mp4"
  },
  "variants": [
    {
      "key": "text-as-mask",
      "label": "text-as-mask",
      "description": "Text as video mask — extra-bold title letters reveal slowly panning product footage through them; at the end the letterforms scale up 26x and overflow, and the inner footage takes over the full screen",
      "use": "",
      "video": "https://gimgs.net/motion/media/text-as-mask/video-149301bddf5a.mp4",
      "poster": "https://gimgs.net/motion/media/text-as-mask/poster-662123bf9c2e.jpg"
    }
  ],
  "credits": {
    "author": "video-shotcraft (Vincent Wei)",
    "url": "https://github.com/Vincentwei1021/video-shotcraft"
  },
  "page_url": "https://gimgs.net/motion/text-as-mask.html",
  "json_url": "https://gimgs.net/motion/text-as-mask.json",
  "template_url": "https://gimgs.net/motion/text-as-mask/template.html",
  "play_url": null,
  "markdown_url": "https://gimgs.net/motion/text-as-mask.md",
  "updated_at": "2026-09-24T07:40:27.203Z",
  "prompt": "## When to use\n\nAn opening or chapter card combining a brand word/slogan with product footage in one; the letters are a door, the product is inside it\n\n## Intention\n\nTitle and product footage fighting for screen space is an old problem — text-first-then-footage is slow, footage-over-text is cramped. This move fuses the two into one body: the letterforms are the mask, the product only shows through the strokes, so the viewer reads the word and the footage's texture at once; at the end the letterforms swell and overflow the frame, and the product naturally takes over the full screen — \"walking from the brand word into the product\" told in one shot, doubling as a chapter transition (related to transition-travel C's letter-cavity pass-through: there the camera drills into the letter cavity, here the letterform itself swells — one moves the camera, the other moves the door).\n\n## Duration & energy\n\n- Duration: 5s (20f hold + 80f drift inside letters + 30f scale-and-takeover + 20f still)\n- Energy: Medium-high (the drift section is steady, the takeover section is one burst)\n\n## Shot recipe (original Chinese — timing, easing, parameter table, known pitfalls)\n\n## 意图\n标题和产品画面抢屏幕是老问题——先字后画慢，画上压字挤。这一式让\n两者同体：字形就是遮罩，产品只透过笔画显形，观众同时读到词和画面\n质感；结尾字形胀开溢出画幅，产品自然接管全屏——\"从品牌词走进产品\"\n一条镜头说完，兼任章节转场（与 transition-travel C 字腔穿越同族：\n那边是镜头钻进字腔，这边是字形自己胀开，前者相机动、后者门动）。\n\n## 动效核心\n- 深底 + SVG 白字黑底 data-URI 交给 CSS `mask-image`，遮罩层套住产品画面\n- 字内漂移：产品层 scale 1.15 + translateX 匀速漂 ~220px/80f——\n  字形静止、内容流动，\"窗外有世界\"的错觉即手法本体\n- 放大接管 30f 单段 bezier(0.4,0,0.2,1)：mask wrapper `scale(1→26)`，\n  **transform-origin 必须钉在实心笔画内**（demo 取 L 竖笔 61.5% 处）——\n  钉在字腔空洞里则放大后透出的是底色\n- 内容层反向补偿：套一层 `scale(1/maskS)` 抵消 mask 放大，产品画面\n  不随遮罩几何畸变（与 shot-transitions F 式\"窗几何与窗内景同步驱动\"同一命门的镜像）\n- 接管保险：另叠一个共享同一 translateX/scale 变换的无遮罩全屏层在\n  放大后段（endT 0.25–0.9）淡入——纯 mask 放大在字缝处会留深底残余\n\n## 参数表\n| 参数 | 典型值 | 调节手感 |\n|------|--------|----------|\n| 字号/字重 | ≥360px / 900 | 笔画就是视口——细字重等于把产品塞进门缝 |\n| 漂移速度 | ~2.75px/f 匀速 | 快了字内画面读不清；漂移方向选画面信息密的轴 |\n| 放大倍率 | 26x（按字形/画幅比配，洞撑满即可） | 不足则接管帧还看得到字边残影 |\n| 收尾 | 接管后真静止 ≥20f | R1；接管完成帧产品应落在标准构图 |\n\n## 已知坑\n- demo 在灰阶/占位素材上调校通过——参数是调校起点非实战定稿，\n  首次实战须以真实素材回验\n- 字内透出的产品画面全程被特写盯着看——真实素材先过\n  审美准则 Q2 的高分辨率栅格化技法，低倍截图在笔画里糊成一片\n- 换词/换字体后 origin 手推值必失效——渲后看放大方向，洞不对中就微调\n  origin（同 transition-travel C 的腔心漂移坑）\n- 与 transition-travel C 字腔穿越同片不并用——同为\"字里长出世界\"，\n  观众读作同一招使两次（P4）\n- mask 层与内容层的 transform 必须分离驱动（wrapper 放大/内容反向补偿），\n  合在一层则产品跟着字形一起胀成 26 倍废片\n\n## 参考实现\ndemos/opening/text-as-mask/\n（TextAsMask.tsx）\n\n## Reference implementation (Remotion)\n\n- demos/opening/text-as-mask/TextAsMask.tsx — https://raw.githubusercontent.com/Vincentwei1021/video-shotcraft/main/demos/opening/text-as-mask/TextAsMask.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/opening/text-as-mask/TextAsMask.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/opening/text-as-mask/TextAsMask.tsx",
      "url": "https://raw.githubusercontent.com/Vincentwei1021/video-shotcraft/main/demos/opening/text-as-mask/TextAsMask.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": 152,
    "repo": "https://github.com/Vincentwei1021/video-shotcraft",
    "files": [
      "https://raw.githubusercontent.com/Vincentwei1021/video-shotcraft/main/demos/opening/text-as-mask/TextAsMask.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/opening/text-as-mask/TextAsMask.tsx =====\n// 文字视频遮罩（text-as-mask）——kinetische typografie\n// 深底上超粗大字 \"SCALE\"，字形内部用 CSS alpha mask 套住 FakeDashboard：\n// 0–20f hold 读布景；20–100f dashboard 在字内匀速 translateX +110→-110（scale 1.15）；\n// 100–130f 单段 bezier：mask 层 scale 1→26 放大溢出（内容层用 1/S 反向抵消不畸变），\n// 同时无遮罩全屏层淡入接管，dashboard 1.15→1.0 归位；130–150f 全屏静止收尾。\nimport React from 'react';\nimport { useCurrentFrame, interpolate, Easing } from 'remotion';\nimport { FakeDashboard, G } from '../../_fixtures/Fixtures';\n\nconst MASK_SVG = `<svg xmlns=\"http://www.w3.org/2000/svg\" width=\"1920\" height=\"1080\"><text x=\"960\" y=\"666\" font-family=\"Helvetica, Arial, sans-serif\" font-size=\"360\" font-weight=\"900\" letter-spacing=\"-8\" text-anchor=\"middle\" fill=\"white\">SCALE</text></svg>`;\nconst MASK_URL = `url(\"data:image/svg+xml,${encodeURIComponent(MASK_SVG)}\")`;\n// mask 放大原点：取字母 L 的竖笔位置（约 61.5% 处），保证放大时原点落在实心笔画内\nconst ORIGIN = '61.5% 50%';\n\nexport const TextAsMask: React.FC = () => {\n  const f = useCurrentFrame();\n  const clamp = { extrapolateLeft: 'clamp', extrapolateRight: 'clamp' } as const;\n\n  // 结尾撤场进度：100–130f 单段 bezier\n  const endT = interpolate(f, [100, 130], [0, 1], {\n    ...clamp,\n    easing: Easing.bezier(0.4, 0, 0.2, 1),\n  });\n\n  // dashboard 内容运动：20–100f 匀速漂移，100–130f 归位到全屏\n  const driftX = interpolate(f, [20, 100], [110, -110], clamp);\n  const dx = f < 100 ? driftX : interpolate(endT, [0, 1], [-110, 0]);\n  const dashS = interpolate(endT, [0, 1], [1.15, 1]);\n\n  // mask 层放大（内容层反向抵消，dashboard 不跟着几何畸变）\n  const maskS = interpolate(endT, [0, 1], [1, 26]);\n  // 无遮罩全屏层淡入，保证接管彻底\n  const cover = interpolate(endT, [0.25, 0.9], [0, 1], clamp);\n  // 底部小注释：撤场时淡出\n  const caption = interpolate(f, [100, 114], [1, 0], clamp);\n\n  const dashMotion: React.CSSProperties = {\n    position: 'absolute',\n    inset: 0,\n    transform: `translateX(${dx}px) scale(${dashS})`,\n    transformOrigin: '50% 50%',\n  };\n\n  return (\n    <div style={{ width: 1920, height: 1080, background: G.ink, position: 'relative', overflow: 'hidden' }}>\n      {/* 遮罩层：wrapper 负责 mask + 放大；inner 用 1/S 反向缩放抵消内容形变 */}\n      <div\n        style={{\n          position: 'absolute',\n          inset: 0,\n          transform: `scale(${maskS})`,\n          transformOrigin: ORIGIN,\n          WebkitMaskImage: MASK_URL,\n          maskImage: MASK_URL,\n          WebkitMaskRepeat: 'no-repeat',\n          maskRepeat: 'no-repeat',\n          WebkitMaskSize: '1920px 1080px',\n          maskSize: '1920px 1080px',\n        }}\n      >\n        <div style={{ position: 'absolute', inset: 0, transform: `scale(${1 / maskS})`, transformOrigin: ORIGIN }}>\n          <div style={dashMotion}>\n            <FakeDashboard variant=\"A\" />\n          </div>\n        </div>\n      </div>\n\n      {/* 接管层：同一运动变换的全屏 dashboard，撤场时淡入到 1 */}\n      <div style={{ position: 'absolute', inset: 0, opacity: cover }}>\n        <div style={dashMotion}>\n          <FakeDashboard variant=\"A\" />\n        </div>\n      </div>\n\n      {/* 底部小注释 */}\n      <div\n        style={{\n          position: 'absolute',\n          bottom: 90,\n          width: '100%',\n          textAlign: 'center',\n          fontFamily: 'Helvetica, Arial, sans-serif',\n          fontWeight: 700,\n          fontSize: 30,\n          letterSpacing: 10,\n          color: G.mid,\n          opacity: caption,\n        }}\n      >\n        TEXT AS MASK\n      </div>\n    </div>\n  );\n};\n\n\n"
}