Motion · Tipografia

Type assembly: four moves

Four type-assembly moves — split-text-stagger, letters rising into place; drift-assembly, letters drifting together; tracking-expand, breathing letter-spacing; text-on-path, flowing in along a curve.

1920×108016:930 fps4.39s · 132 quadrosremotionLicença: Apache-2.0#shotcraft#remotion#typography

Fonte: video-shotcraft (Vincent Wei) ↗ · Apache-2.0 · Composição Remotion (React/TSX) — vídeo de prévia, sem player no navegador

Variantes

split-text-staggersplit-text-stagger 裂升
每字 overflow 盒内 translateY(115%→0) 带 10% 过冲,delay i×2f,基线同步生长
通用默认;利落理性
drift-assemblydrift-assembly 漂移合拢
字符 seed 方向 ±300px + blur 8px 漂入错峰归位,锁定帧加深脉冲,合体后整词呼吸 1.04
品牌名/片头级亮相
tracking-expandtracking-expand 字距呼吸
字母从 −0.42em 叠压展开到 0.14em,blur 10→0 同曲线
安静抒情段;副标题跟进
text-on-pathtext-on-path 沿线流入
字符沿贝塞尔曲线鱼贯滑入(切线角旋转),到达后 12f 摆正水平
曲线有语义时(增长线/流程线)

When to use

Big-title/tagline entrances; in the same title-entrance family as type-entrance-moves' two styles, split-flap-title, and document-typewriter-reveal — at most 2 per film

Intention

Title entrances already have a four-style "in-place transform" family (garbled decode / character drop / split-flap / typewriter — letters change form in place). This card adds the "assembly" family: letters gather from elsewhere into the title, and the motion path itself is the show. A is piano-key style — every letter slides up from a hidden crop line below, rational and clean, the most general-purpose; B is title-sequence style — letters scatter in with blur, locking in and deepening one by one, a Stranger Things kind of ceremony, good for a brand-name reveal; C is breathing style — letters expand out from a stacked clump as if taking a breath, the quietest of the four, good for lyrical passages; D is semantic style — letters flow in single-file along a rising curve then straighten out, the curve can trace exactly along a chart's growth line, telling text and data in the same frame.

Duration & energy

  • Duration: Single style 4–5s (action segment A ~56f / B ~104f / C ~58f / D ~99f, all including holds)
  • Energy: A medium / B medium-high / C low-medium / D medium

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

意图

标题入场已有"原位变形系"四式(乱码解码/字符坠落/翻牌/打字机——字 在原地换形态)。本卡补"集结系":字符从别处汇聚成标题,运动轨迹 本身有戏。A 琴键式——每字从看不见的裁切线下滑升,理性利落,通用度 最高;B 片头式——字符四散带模糊漂入、逐个锁定加深,Stranger Things 的仪式感,适合品牌名亮相;C 呼吸式——字母从叠压一团吸一口气展开, 最安静的一式,适合抒情段落;D 语义式——字符沿一条上升曲线鱼贯流入 再摆正,曲线可以正好描着图表增长线走,文字与数据同框叙事。

四式选型

| 式 | 做法 | 适用 | |----|------|------| | A split-text-stagger 裂升 | 每字 overflow 盒内 translateY(115%→0) 带 10% 过冲,delay i×2f,基线同步生长 | 通用默认;利落理性 | | B drift-assembly 漂移合拢 | 字符 seed 方向 ±300px + blur 8px 漂入错峰归位,锁定帧加深脉冲,合体后整词呼吸 1.04 | 品牌名/片头级亮相 | | C tracking-expand 字距呼吸 | 字母从 −0.42em 叠压展开到 0.14em,blur 10→0 同曲线 | 安静抒情段;副标题跟进 | | D text-on-path 沿线流入 | 字符沿贝塞尔曲线鱼贯滑入(切线角旋转),到达后 12f 摆正水平 | 曲线有语义时(增长线/流程线) |

参数表

| 参数 | 典型值 | 调节手感 | |------|--------|----------| | A 过冲 | 10%(原案 6% 可测不可感,实渲加码) | 正常速度要看得见回落那一下 | | B 锁定脉冲 | 字色深到 #000 + 描边 0→3px→0 / 8f | 白底上加深+描边,不用发光(判例) | | B 漂移三绑定 | 位移 (1−p)、blur 8(1−p)、opacity 0.35→1 共用一条 p | 三者分曲线走,字会"到了还糊着" | | C 实现命门 | letter-spacing 恒为终态,逐字符 span 只做 translateX=(1−p)(i−词心)(−0.56em) | 直接动 letter-spacing 逐帧重排必抖 | | D 摆正 | 到达停 8f 再 12f lerp 到水平基线(y 拉平 rotate→0) | 不摆正就永远是"挂在线上",读不出标题 | | D 曲线 | evolve(dashoffset)随最前字符同步生长 | 曲线先画完字再走,读作两个动画 | | 收尾 | 全员落定真静止 ≥30f | R1 |

已知坑

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

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

  • 标题入场大品类现已八式(本卡四式 + type-entrance-moves 两式 +

split-flap-title + document-typewriter-reveal),同片 ≤2 种(P4)铁律不变

  • B 式与 D 式都是高注意力入场,别再叠相机动作——字在飞的时候镜头必须稳
  • C 式幅度命门:起始叠压至少 −0.4em 级(每缝 80px+ 位移)才可感,

−0.1em 级"微展开"正常速度读不出(可感性判例)

  • D 式字符在曲线低段会短暂交叠,是鱼贯读感的一部分;但字号 >80px 时

交叠面积过大会糊成一团,大字改用更长曲线或减字数

参考实现

demos/typography/type-assembly-moves/ (LetterformDriftAssembly.tsx / SplitTextStagger.tsx / TextOnPath.tsx / TrackingExpandReveal.tsx)

Reference implementation (Remotion)

  • demos/typography/type-assembly-moves/LetterformDriftAssembly.tsx — https://raw.githubusercontent.com/Vincentwei1021/video-shotcraft/main/demos/typography/type-assembly-moves/LetterformDriftAssembly.tsx
  • demos/typography/type-assembly-moves/SplitTextStagger.tsx — https://raw.githubusercontent.com/Vincentwei1021/video-shotcraft/main/demos/typography/type-assembly-moves/SplitTextStagger.tsx
  • demos/typography/type-assembly-moves/TextOnPath.tsx — https://raw.githubusercontent.com/Vincentwei1021/video-shotcraft/main/demos/typography/type-assembly-moves/TextOnPath.tsx
  • demos/typography/type-assembly-moves/TrackingExpandReveal.tsx — https://raw.githubusercontent.com/Vincentwei1021/video-shotcraft/main/demos/typography/type-assembly-moves/TrackingExpandReveal.tsx

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

Tudo nesta página também é legível por máquina. Aponte seu agente para o JSON ou adicione o servidor MCP do gimgs e peça para buscar templates de motion.

  1. Baixe o JSON — contém a receita do plano (timing, easing, tabela de parâmetros, armadilhas) e o código TSX completo.
  2. Clone o repositório video-shotcraft; o componente importa fixtures de demos/_fixtures e helpers de assets/lib.
  3. Registre o componente como <Composition> em um projeto Remotion e renderize com `npx remotion render`, ou reimplemente a receita no seu próprio engine.
curl -s https://gimgs.net/motion/type-assembly-moves.json
git clone https://github.com/Vincentwei1021/video-shotcraft
# component: demos/typography/type-assembly-moves/LetterformDriftAssembly.tsx, demos/typography/type-assembly-moves/SplitTextStagger.tsx, demos/typography/type-assembly-moves/TextOnPath.tsx, demos/typography/type-assembly-moves/TrackingExpandReveal.tsx
npx remotion render <CompositionId> out.mp4
claude mcp add --transport http gimgs https://gimgs.net/mcp
# then: search_motion_templates / get_motion_template("type-assembly-moves")

Prompt

Use the video-shotcraft shot recipe "Type assembly: four moves" (Remotion composition, Apache-2.0) published on gimgs.net.
Recipe JSON (prompt = timing/easing/parameter table/pitfalls, files = TSX source): https://gimgs.net/motion/type-assembly-moves.json
Raw TSX: https://gimgs.net/motion/type-assembly-moves/template.html
Repository: https://github.com/Vincentwei1021/video-shotcraft  (component: demos/typography/type-assembly-moves/LetterformDriftAssembly.tsx, demos/typography/type-assembly-moves/SplitTextStagger.tsx, demos/typography/type-assembly-moves/TextOnPath.tsx, demos/typography/type-assembly-moves/TrackingExpandReveal.tsx; imports demos/_fixtures and assets/lib)
Spec: 1920x1080, 30 fps, 4.39s

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

14,188 chars · Apache-2.0Abrir template bruto ↗
// ===== demos/typography/type-assembly-moves/LetterformDriftAssembly.tsx =====
// 字形漂移合拢(letterform-drift-assembly)——Stranger Things 片头式入场。
// 标题 "ASSEMBLE" 拆 9 字符:各自从不同方向(h(i) seeded 随机向量,
// 幅度 ±260–360px)带 blur 8px + opacity 0.35 缓慢漂入,错峰归位
// (delay i×3f,45f 行程,Easing.out(cubic))。每字锁定瞬间给一次
// "加深脉冲":字色 G.ink→#000→G.ink + 描边 0→3px→0(8f)——白底上
// 不用发光用加深(库判例)。全部合体后整词 scale 1→1.04→1 收束呼吸
// (判例:1.02 太弱,加码到 1.04)。
// 关键帧:0–24 各字错峰启程 → i 字 [i*3, i*3+45] 漂入归位 →
// 锁定帧 i*3+45 起 8f 加深脉冲(最后一字 69–77)→ 80–104 整词呼吸 →
// 104–150 全静止(46f,无逐帧滤镜)。
import React from 'react';
import { useCurrentFrame, interpolate, Easing } from 'remotion';
import { G, TitleBlock } from '../../_fixtures/Fixtures';

const h = (n: number) => {
  const s = Math.sin(n * 127.3) * 43758.5453;
  return s - Math.floor(s);
};

const WORD = 'ASSEMBLE';
const TRAVEL = 45; // 每字漂入行程帧数
const STAG = 3; // 错峰间隔

export const LetterformDriftAssembly: React.FC = () => {
  const frame = useCurrentFrame();
  const chars = WORD.split('');
  const lastLock = (chars.length - 1) * STAG + TRAVEL; // 69

  // 整词收束呼吸:80–92 放大到 1.04,92–104 回落,之后恒 1 → 帧确定
  const breath =
    frame < 92
      ? interpolate(frame, [80, 92], [1, 1.04], {
          extrapolateLeft: 'clamp',
          extrapolateRight: 'clamp',
          easing: Easing.inOut(Easing.cubic),
        })
      : interpolate(frame, [92, 104], [1.04, 1], {
          extrapolateLeft: 'clamp',
          extrapolateRight: 'clamp',
          easing: Easing.inOut(Easing.cubic),
        });

  return (
    <div style={{ width: 1920, height: 1080, background: G.bg, position: 'relative', overflow: 'hidden' }}>
      <div style={{ position: 'absolute', left: 120, top: 96 }}>
        <TitleBlock text="LETTERFORM DRIFT ASSEMBLY" size={54} />
      </div>
      <div
        style={{
          position: 'absolute',
          inset: 0,
          display: 'flex',
          alignItems: 'center',
          justifyContent: 'center',
          transform: `scale(${breath})`,
        }}
      >
        {chars.map((c, i) => {
          const start = i * STAG;
          const lock = start + TRAVEL;
          // 漂入进度
          const p = interpolate(frame, [start, lock], [0, 1], {
            extrapolateLeft: 'clamp',
            extrapolateRight: 'clamp',
            easing: Easing.out(Easing.cubic),
          });
          // seeded 起始向量:方向 h(i),幅度 260–360px
          const ang = h(i + 1) * Math.PI * 2;
          const mag = 260 + h(i + 101) * 100;
          const dx = Math.cos(ang) * mag * (1 - p);
          const dy = Math.sin(ang) * mag * (1 - p);
          const blur = 8 * (1 - p);
          const op = interpolate(p, [0, 1], [0.35, 1]);
          // 锁定加深脉冲:lock→lock+8,三角波 0→1→0
          const pulse =
            frame <= lock || frame >= lock + 8
              ? 0
              : frame < lock + 4
                ? (frame - lock) / 4
                : (lock + 8 - frame) / 4;
          const shade = Math.round(47 * (1 - pulse)); // #2f(47)→#00
          const color = `rgb(${shade},${shade},${shade})`;
          const strokeW = 3 * pulse;
          return (
            <span
              key={i}
              style={{
                fontFamily: 'Helvetica, Arial, sans-serif',
                fontWeight: 800,
                fontSize: 140,
                letterSpacing: 8,
                color,
                display: 'inline-block',
                transform: `translate(${dx}px, ${dy}px)`,
                opacity: op,
                filter: blur > 0.01 ? `blur(${blur}px)` : undefined,
                WebkitTextStroke: strokeW > 0.01 ? `${strokeW}px #000` : undefined,
              }}
            >
              {c}
            </span>
          );
        })}
      </div>
    </div>
  );
};


// ===== demos/typography/type-assembly-moves/SplitTextStagger.tsx =====
// 逐字遮罩裂升(split-text-stagger)——GSAP SplitText 惯用入场。
// 标题 "MOTION SYSTEM" 按字符拆 span,每字外包 overflow:hidden 的行高盒,
// 内层从 translateY(115%) 升到 0,各 14f Easing.out(cubic),带 10% 过冲
// 再 6f 回落(原案 6% 过冲,按可感性红线加码到 10%)。delay = 字符索引×2f。
// 底部基线细线(G.bar 2px)在首字起跳同帧从左向右生长,暗示裁切线存在。
// 关键帧:0–12 空场 hold → 12 首字起跳 + 基线开始生长 → 每字 12+i*2 起跳,
// 14f 升至 -10% 过冲 → 再 6f 回落归 0 → 末字(索引12)于 56f 落定 →
// 56–130 全静止(74f ≥ 40f,无逐帧噪声层)。
import React from 'react';
import { useCurrentFrame, interpolate, Easing } from 'remotion';
import { G, TitleBlock } from '../../_fixtures/Fixtures';

const TEXT = 'MOTION SYSTEM';
const START = 12; // 首字起跳帧
const RISE = 14; // 升起时长
const SETTLE = 6; // 过冲回落时长
const OVERSHOOT = -10; // 过冲到 -10%(原案 6%,加码)
const FONT = 120;

// 单字符纵向位移(%):115 → -10(out cubic)→ 0(out quad),帧确定
const charY = (f: number, idx: number): number => {
  const t0 = START + idx * 2;
  if (f < t0 + RISE) {
    return interpolate(f, [t0, t0 + RISE], [115, OVERSHOOT], {
      extrapolateLeft: 'clamp',
      extrapolateRight: 'clamp',
      easing: Easing.out(Easing.cubic),
    });
  }
  return interpolate(f, [t0 + RISE, t0 + RISE + SETTLE], [OVERSHOOT, 0], {
    extrapolateLeft: 'clamp',
    extrapolateRight: 'clamp',
    easing: Easing.out(Easing.quad),
  });
};

export const SplitTextStagger: React.FC = () => {
  const frame = useCurrentFrame();
  const chars = TEXT.split('');
  // 基线:首字起跳同帧开始,从左向右生长到 100%
  const lineW = interpolate(frame, [START, START + 26], [0, 100], {
    extrapolateLeft: 'clamp',
    extrapolateRight: 'clamp',
    easing: Easing.out(Easing.cubic),
  });

  return (
    <div style={{ width: 1920, height: 1080, background: G.bg, position: 'relative', overflow: 'hidden' }}>
      <div style={{ position: 'absolute', left: 120, top: 96 }}>
        <TitleBlock text="SPLIT TEXT STAGGER" size={54} />
      </div>
      <div
        style={{
          position: 'absolute',
          left: 0,
          right: 0,
          top: 0,
          bottom: 0,
          display: 'flex',
          alignItems: 'center',
          justifyContent: 'center',
          flexDirection: 'column',
        }}
      >
        <div style={{ display: 'flex' }}>
          {chars.map((c, i) => (
            <div
              key={i}
              style={{
                // 遮罩盒:上方留 0.3em 头部空间容纳过冲,底边即裁切线
                overflow: 'hidden',
                height: FONT * 1.35,
                display: 'flex',
                alignItems: 'flex-end',
              }}
            >
              <span
                style={{
                  display: 'inline-block',
                  fontFamily: 'Helvetica, Arial, sans-serif',
                  fontWeight: 800,
                  fontSize: FONT,
                  lineHeight: 1.05,
                  color: G.ink,
                  letterSpacing: 2,
                  transform: `translateY(${charY(frame, i)}%)`,
                }}
              >
                {c === ' ' ? ' ' : c}
              </span>
            </div>
          ))}
        </div>
        {/* 基线细线:宽度从 0 生长到全文宽 */}
        <div style={{ width: 920, marginTop: 10, display: 'flex', justifyContent: 'flex-start' }}>
          <div style={{ height: 2, width: `${lineW}%`, background: G.bar }} />
        </div>
      </div>
    </div>
  );
};


// ===== demos/typography/type-assembly-moves/TextOnPath.tsx =====
// 文字沿曲线流入(text-on-path)——字符沿一条上升贝塞尔曲线(像图表增长线)
// 鱼贯滑入,行进中按切线角旋转;到达各自终点后再从"贴线姿态"lerp 到水平基线位
// 拼成正常标题。曲线本身随字符前进同步 evolve(dashoffset 生长)。
// 关键帧:字符 i 于 i*2 起跑、45f 沿线到达 t_i(out cubic)→ 到达后停 8f →
// 12f 摆正到水平基线(y 拉平、rotate→0)→ 全部落定约 f103 → 103–150 真静止。
import React from 'react';
import { useCurrentFrame, interpolate, Easing } from 'remotion';
import { G, TitleBlock } from '../../_fixtures/Fixtures';

// 上升贝塞尔:左下 → 右上,先缓后陡(增长线形状)
const P0 = { x: 200, y: 820 };
const P1 = { x: 760, y: 810 };
const P2 = { x: 1240, y: 660 };
const P3 = { x: 1500, y: 320 };

const bez = (t: number) => {
  const u = 1 - t;
  return {
    x: u * u * u * P0.x + 3 * u * u * t * P1.x + 3 * u * t * t * P2.x + t * t * t * P3.x,
    y: u * u * u * P0.y + 3 * u * u * t * P1.y + 3 * u * t * t * P2.y + t * t * t * P3.y,
  };
};
// 切线角(度)
const tangent = (t: number) => {
  const u = 1 - t;
  const dx = 3 * u * u * (P1.x - P0.x) + 6 * u * t * (P2.x - P1.x) + 3 * t * t * (P3.x - P2.x);
  const dy = 3 * u * u * (P1.y - P0.y) + 6 * u * t * (P2.y - P1.y) + 3 * t * t * (P3.y - P2.y);
  return (Math.atan2(dy, dx) * 180) / Math.PI;
};

const TEXT = 'GROWTH ALL THE WAY';
const N = TEXT.length;
const CHAR_W = 46; // 64px fontWeight 800 的近似字宽
const FINAL_Y = 300; // 水平基线(标题最终落位)
const FINAL_X0 = 960 - (N * CHAR_W) / 2;

export const TextOnPath: React.FC = () => {
  const frame = useCurrentFrame();
  // 曲线 evolve:随最前字符推进同步生长
  const evolve = interpolate(frame, [0, 82], [0, 1], {
    extrapolateLeft: 'clamp', extrapolateRight: 'clamp',
    easing: Easing.out(Easing.quad),
  });

  return (
    <div style={{ width: 1920, height: 1080, background: G.bg, position: 'relative', overflow: 'hidden' }}>
      <div style={{ position: 'absolute', left: 120, top: 96 }}>
        <TitleBlock text="TEXT ON PATH" size={54} />
      </div>
      <svg width={1920} height={1080} style={{ position: 'absolute', inset: 0 }}>
        <path
          d={`M ${P0.x} ${P0.y} C ${P1.x} ${P1.y}, ${P2.x} ${P2.y}, ${P3.x} ${P3.y}`}
          fill="none" stroke={G.bar} strokeWidth={3}
          pathLength={1} strokeDasharray={1} strokeDashoffset={1 - evolve}
        />
      </svg>
      {TEXT.split('').map((ch, i) => {
        if (ch === ' ') return null;
        const tEnd = 0.12 + 0.82 * (i / (N - 1)); // 各字符沿线终点
        const start = i * 2;
        // 沿线推进:start 起 45f 到达 tEnd
        const t = interpolate(frame, [start, start + 45], [0, tEnd], {
          extrapolateLeft: 'clamp', extrapolateRight: 'clamp',
          easing: Easing.out(Easing.cubic),
        });
        const pOnCurve = bez(t);
        const angOnCurve = tangent(t);
        // 到达后停 8f,再 12f 摆正到水平基线
        const settle = interpolate(frame, [start + 53, start + 65], [0, 1], {
          extrapolateLeft: 'clamp', extrapolateRight: 'clamp',
          easing: Easing.inOut(Easing.cubic),
        });
        const x = pOnCurve.x + (FINAL_X0 + i * CHAR_W - pOnCurve.x) * settle;
        const y = pOnCurve.y + (FINAL_Y - pOnCurve.y) * settle;
        const ang = angOnCurve * (1 - settle);
        const op = interpolate(frame, [start, start + 8], [0, 1], {
          extrapolateLeft: 'clamp', extrapolateRight: 'clamp',
        });
        return (
          <div key={i} style={{
            position: 'absolute', left: x, top: y, opacity: op,
            transform: `translate(-50%, -50%) rotate(${ang}deg)`,
            fontFamily: 'Helvetica, Arial, sans-serif', fontWeight: 800,
            fontSize: 64, color: G.ink,
          }}>
            {ch}
          </div>
        );
      })}
    </div>
  );
};


// ===== demos/typography/type-assembly-moves/TrackingExpandReveal.tsx =====
// 字距呼吸展开(tracking-expand-reveal)——电影片头字幕惯用的 letter-spacing 入场。
// 标题 "BREATHE"(150px)出场时字符几乎叠压(相当于 letter-spacing -0.42em),
// 50 帧内展开到 0.14em,同步 blur 10px→0、opacity 0.6→1、scaleX 0.92→1。
// 不动 letter-spacing 本身(逐帧重排会抖):容器常量 letterSpacing 0.14em 定终位,
// 逐字符 span 用 translateX 手动插值——第 i 字起始位移 = (i - 中心) × 每缝差值 84px,
// 全部挤向词心,位移只与字缝有关、与字宽无关,天然对中。
// 关键帧:0–50 展开(Easing.out(poly(5)))+去糊提亮 → 35–58 副标题淡入(≈主词展开 70% 时点)
// → 58–130 全静止(≥72f,滤镜彻底摘除)。
import React from 'react';
import { useCurrentFrame, interpolate, Easing } from 'remotion';
import { G, TitleBlock } from '../../_fixtures/Fixtures';

const WORD = 'BREATHE';
const FS = 150; // 主词字号
// 每个字缝的起始-终点差:(-0.42em) - (0.14em) = -0.56em = -84px @150px
const GAP_DELTA = -0.56 * FS;

export const TrackingExpandReveal: React.FC = () => {
  const frame = useCurrentFrame();
  // 展开进度 0→1(0–50f,out poly(5))
  const p = interpolate(frame, [0, 50], [0, 1], {
    extrapolateLeft: 'clamp',
    extrapolateRight: 'clamp',
    easing: Easing.out(Easing.poly(5)),
  });
  const blur = 10 * (1 - p);
  const op = interpolate(p, [0, 1], [0.6, 1]);
  const sx = interpolate(p, [0, 1], [0.92, 1]);
  // 副标题:主词时间轴走到 70%(帧 35)起淡入
  const subOp = interpolate(frame, [35, 58], [0, 1], {
    extrapolateLeft: 'clamp',
    extrapolateRight: 'clamp',
    easing: Easing.out(Easing.quad),
  });

  const N = WORD.length;
  const center = (N - 1) / 2;
  const settled = frame >= 50; // 展开完成后摘掉一切滤镜/变换,保证逐帧完全相同

  return (
    <div style={{ width: 1920, height: 1080, background: G.bg, position: 'relative', overflow: 'hidden' }}>
      <div style={{ position: 'absolute', left: 120, top: 96 }}>
        <TitleBlock text="TRACKING EXPAND REVEAL" size={54} />
      </div>

      {/* 主词:容器 letterSpacing 恒为 0.14em(终态),字符仅做 translateX */}
      <div
        style={{
          position: 'absolute',
          top: 430,
          left: 0,
          width: 1920,
          display: 'flex',
          justifyContent: 'center',
          transform: settled ? undefined : `scaleX(${sx})`,
          filter: settled ? undefined : `blur(${blur}px)`,
          opacity: settled ? 1 : op,
        }}
      >
        <div
          style={{
            fontFamily: 'Helvetica, Arial, sans-serif',
            fontWeight: 800,
            fontSize: FS,
            color: G.ink,
            letterSpacing: '0.14em',
            whiteSpace: 'pre',
            // letter-spacing 只加在字后,整体左移半个缝宽找回视觉对中
            marginLeft: 0.14 * FS * 0.5,
          }}
        >
          {WORD.split('').map((ch, i) => {
            const tx = (1 - p) * (i - center) * GAP_DELTA;
            return (
              <span
                key={i}
                style={{
                  display: 'inline-block',
                  transform: settled ? undefined : `translateX(${tx}px)`,
                }}
              >
                {ch}
              </span>
            );
          })}
        </div>
      </div>

      {/* 副标题:主词展开 70% 时点淡入 */}
      <div
        style={{
          position: 'absolute',
          top: 630,
          left: 0,
          width: 1920,
          textAlign: 'center',
          fontFamily: 'Helvetica, Arial, sans-serif',
          fontWeight: 500,
          fontSize: 34,
          color: G.mid,
          letterSpacing: '0.32em',
          opacity: frame >= 58 ? 1 : subOp,
        }}
      >
        A CINEMATIC TITLE ENTRANCE
      </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/typography/type-assembly-moves/LetterformDriftAssembly.tsx, demos/typography/type-assembly-moves/SplitTextStagger.tsx, demos/typography/type-assembly-moves/TextOnPath.tsx, demos/typography/type-assembly-moves/TrackingExpandReveal.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
—

Mais nesta categoria