Motion · Intro

Text-as-video mask

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.

1920×108016:930 fps5.06s · 152 framesremotionLicense: Apache-2.0#shotcraft#remotion#intro#opening#typography

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

When to use

An opening or chapter card combining a brand word/slogan with product footage in one; the letters are a door, the product is inside it

Intention

Title 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).

Duration & energy

  • Duration: 5s (20f hold + 80f drift inside letters + 30f scale-and-takeover + 20f still)
  • Energy: Medium-high (the drift section is steady, the takeover section is one burst)

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

意图

标题和产品画面抢屏幕是老问题——先字后画慢,画上压字挤。这一式让 两者同体:字形就是遮罩,产品只透过笔画显形,观众同时读到词和画面 质感;结尾字形胀开溢出画幅,产品自然接管全屏——"从品牌词走进产品" 一条镜头说完,兼任章节转场(与 transition-travel C 字腔穿越同族: 那边是镜头钻进字腔,这边是字形自己胀开,前者相机动、后者门动)。

动效核心

  • 深底 + SVG 白字黑底 data-URI 交给 CSS mask-image,遮罩层套住产品画面
  • 字内漂移:产品层 scale 1.15 + translateX 匀速漂 ~220px/80f——

字形静止、内容流动,"窗外有世界"的错觉即手法本体

  • 放大接管 30f 单段 bezier(0.4,0,0.2,1):mask wrapper scale(1→26),

transform-origin 必须钉在实心笔画内(demo 取 L 竖笔 61.5% 处)—— 钉在字腔空洞里则放大后透出的是底色

  • 内容层反向补偿:套一层 scale(1/maskS) 抵消 mask 放大,产品画面

不随遮罩几何畸变(与 shot-transitions F 式"窗几何与窗内景同步驱动"同一命门的镜像)

  • 接管保险:另叠一个共享同一 translateX/scale 变换的无遮罩全屏层在

放大后段(endT 0.25–0.9)淡入——纯 mask 放大在字缝处会留深底残余

参数表

| 参数 | 典型值 | 调节手感 | |------|--------|----------| | 字号/字重 | ≥360px / 900 | 笔画就是视口——细字重等于把产品塞进门缝 | | 漂移速度 | ~2.75px/f 匀速 | 快了字内画面读不清;漂移方向选画面信息密的轴 | | 放大倍率 | 26x(按字形/画幅比配,洞撑满即可) | 不足则接管帧还看得到字边残影 | | 收尾 | 接管后真静止 ≥20f | R1;接管完成帧产品应落在标准构图 |

已知坑

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

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

  • 字内透出的产品画面全程被特写盯着看——真实素材先过

审美准则 Q2 的高分辨率栅格化技法,低倍截图在笔画里糊成一片

  • 换词/换字体后 origin 手推值必失效——渲后看放大方向,洞不对中就微调

origin(同 transition-travel C 的腔心漂移坑)

  • 与 transition-travel C 字腔穿越同片不并用——同为"字里长出世界",

观众读作同一招使两次(P4)

  • mask 层与内容层的 transform 必须分离驱动(wrapper 放大/内容反向补偿),

合在一层则产品跟着字形一起胀成 26 倍废片

参考实现

demos/opening/text-as-mask/ (TextAsMask.tsx)

Reference implementation (Remotion)

  • demos/opening/text-as-mask/TextAsMask.tsx — https://raw.githubusercontent.com/Vincentwei1021/video-shotcraft/main/demos/opening/text-as-mask/TextAsMask.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/text-as-mask.json
git clone https://github.com/Vincentwei1021/video-shotcraft
# component: demos/opening/text-as-mask/TextAsMask.tsx
npx remotion render <CompositionId> out.mp4
claude mcp add --transport http gimgs https://gimgs.net/mcp
# then: search_motion_templates / get_motion_template("text-as-mask")

Prompt

Use the video-shotcraft shot recipe "Text-as-video mask" (Remotion composition, Apache-2.0) published on gimgs.net.
Recipe JSON (prompt = timing/easing/parameter table/pitfalls, files = TSX source): https://gimgs.net/motion/text-as-mask.json
Raw TSX: https://gimgs.net/motion/text-as-mask/template.html
Repository: https://github.com/Vincentwei1021/video-shotcraft  (component: demos/opening/text-as-mask/TextAsMask.tsx; imports demos/_fixtures and assets/lib)
Spec: 1920x1080, 30 fps, 5.06s

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

3,359 chars · Apache-2.0Open raw template ↗
// ===== demos/opening/text-as-mask/TextAsMask.tsx =====
// 文字视频遮罩(text-as-mask)——kinetische typografie
// 深底上超粗大字 "SCALE",字形内部用 CSS alpha mask 套住 FakeDashboard:
// 0–20f hold 读布景;20–100f dashboard 在字内匀速 translateX +110→-110(scale 1.15);
// 100–130f 单段 bezier:mask 层 scale 1→26 放大溢出(内容层用 1/S 反向抵消不畸变),
// 同时无遮罩全屏层淡入接管,dashboard 1.15→1.0 归位;130–150f 全屏静止收尾。
import React from 'react';
import { useCurrentFrame, interpolate, Easing } from 'remotion';
import { FakeDashboard, G } from '../../_fixtures/Fixtures';

const 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>`;
const MASK_URL = `url("data:image/svg+xml,${encodeURIComponent(MASK_SVG)}")`;
// mask 放大原点:取字母 L 的竖笔位置(约 61.5% 处),保证放大时原点落在实心笔画内
const ORIGIN = '61.5% 50%';

export const TextAsMask: React.FC = () => {
  const f = useCurrentFrame();
  const clamp = { extrapolateLeft: 'clamp', extrapolateRight: 'clamp' } as const;

  // 结尾撤场进度:100–130f 单段 bezier
  const endT = interpolate(f, [100, 130], [0, 1], {
    ...clamp,
    easing: Easing.bezier(0.4, 0, 0.2, 1),
  });

  // dashboard 内容运动:20–100f 匀速漂移,100–130f 归位到全屏
  const driftX = interpolate(f, [20, 100], [110, -110], clamp);
  const dx = f < 100 ? driftX : interpolate(endT, [0, 1], [-110, 0]);
  const dashS = interpolate(endT, [0, 1], [1.15, 1]);

  // mask 层放大(内容层反向抵消,dashboard 不跟着几何畸变)
  const maskS = interpolate(endT, [0, 1], [1, 26]);
  // 无遮罩全屏层淡入,保证接管彻底
  const cover = interpolate(endT, [0.25, 0.9], [0, 1], clamp);
  // 底部小注释:撤场时淡出
  const caption = interpolate(f, [100, 114], [1, 0], clamp);

  const dashMotion: React.CSSProperties = {
    position: 'absolute',
    inset: 0,
    transform: `translateX(${dx}px) scale(${dashS})`,
    transformOrigin: '50% 50%',
  };

  return (
    <div style={{ width: 1920, height: 1080, background: G.ink, position: 'relative', overflow: 'hidden' }}>
      {/* 遮罩层:wrapper 负责 mask + 放大;inner 用 1/S 反向缩放抵消内容形变 */}
      <div
        style={{
          position: 'absolute',
          inset: 0,
          transform: `scale(${maskS})`,
          transformOrigin: ORIGIN,
          WebkitMaskImage: MASK_URL,
          maskImage: MASK_URL,
          WebkitMaskRepeat: 'no-repeat',
          maskRepeat: 'no-repeat',
          WebkitMaskSize: '1920px 1080px',
          maskSize: '1920px 1080px',
        }}
      >
        <div style={{ position: 'absolute', inset: 0, transform: `scale(${1 / maskS})`, transformOrigin: ORIGIN }}>
          <div style={dashMotion}>
            <FakeDashboard variant="A" />
          </div>
        </div>
      </div>

      {/* 接管层:同一运动变换的全屏 dashboard,撤场时淡入到 1 */}
      <div style={{ position: 'absolute', inset: 0, opacity: cover }}>
        <div style={dashMotion}>
          <FakeDashboard variant="A" />
        </div>
      </div>

      {/* 底部小注释 */}
      <div
        style={{
          position: 'absolute',
          bottom: 90,
          width: '100%',
          textAlign: 'center',
          fontFamily: 'Helvetica, Arial, sans-serif',
          fontWeight: 700,
          fontSize: 30,
          letterSpacing: 10,
          color: G.mid,
          opacity: caption,
        }}
      >
        TEXT AS MASK
      </div>
    </div>
  );
};


  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/opening/text-as-mask/TextAsMask.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