Motion · Chữ & title card
Chữ tập hợp: bốn kiểu
Bốn kiểu tập hợp chữ — split-text-stagger chữ dâng lên vào vị trí, drift-assembly chữ trôi tụ lại, tracking-expand giãn khoảng cách kiểu hơi thở, text-on-path chảy vào theo đường cong.
Nguồn: video-shotcraft (Vincent Wei) ↗ · Apache-2.0 · Composition Remotion (React/TSX) — có video preview, không có player trong trình duyệt
Biến thể
Mỗi chữ trong hộp overflow trượt translateY(115%→0) kèm 10% overshoot, delay i×2f, đường cơ sở lớn lên đồng bộ
Mặc định thông dụng; gọn gàng lý tính
Chữ khởi phát hướng seed ±300px + mờ 8px trôi vào lệch nhịp rồi vào vị trí, khung khóa đậm thêm một nhịp xung, hợp thể xong cả từ thở 1.04
Xuất hiện cấp tên thương hiệu/mở đầu phim
Chữ cái từ chồng lên nhau -0.42em giãn ra tới 0.14em, mờ chạy 10→0 cùng đường cong
Đoạn trữ tình yên tĩnh; phụ đề theo sau
Chữ trượt vào nối đuôi theo đường cong Bezier (xoay theo góc tiếp tuyến), đứng thẳng ngang sau 12f khi tới nơi
Khi đường cong mang ngữ nghĩa (đường tăng trưởng/đường quy trình)
Khi nào dùng
Mở đầu tiêu đề lớn/khẩu hiệu; cùng nhóm mở đầu tiêu đề lớn với hai kiểu của type-entrance-moves, split-flap-title, document-typewriter-reveal — cả phim dùng ≤2 kiểu
Ý đồ
Mở đầu tiêu đề đã có nhóm "biến đổi tại chỗ" bốn kiểu (giải mã ký tự lỗi/rơi từng chữ/bảng lật/máy đánh chữ — chữ đổi hình dạng tại chỗ). Thẻ này bổ sung nhóm "tập hợp": chữ từ nơi khác tụ lại thành tiêu đề, bản thân quỹ đạo chuyển động là điểm nhấn. A kiểu phím đàn — mỗi chữ trượt lên từ đường cắt ẩn bên dưới, lý tính gọn gàng, thông dụng nhất; B kiểu mở đầu phim — chữ tản ra kèm mờ trôi vào, khóa dần từng chữ và đậm lên, chất nghi thức kiểu Stranger Things, hợp cho tên thương hiệu xuất hiện; C kiểu hơi thở — chữ cái từ một khối chồng lên nhau như hít một hơi rồi giãn ra, yên tĩnh nhất trong bốn kiểu, hợp đoạn trữ tình; D kiểu ngữ nghĩa — chữ chảy nối đuôi theo một đường cong đi lên rồi đứng thẳng lại, đường cong có thể đúng theo đường tăng trưởng của biểu đồ, chữ và dữ liệu cùng khung kể chuyện.
Thời lượng & năng lượng
- Thời lượng: Mỗi kiểu 4–5s (đoạn động tác A khoảng 56f / B khoảng 104f / C khoảng 58f / D khoảng 99f, đều tính cả giữ khung)
- Năng lượng: A trung bình / B trung bình cao / C thấp trung bình / D trung bình
Công thức cảnh quay (bản gốc tiếng Trung — timing, easing, bảng tham số, lỗi hay gặp)
意图
标题入场已有"原位变形系"四式(乱码解码/字符坠落/翻牌/打字机——字 在原地换形态)。本卡补"集结系":字符从别处汇聚成标题,运动轨迹 本身有戏。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)
Mã nguồn tham chiếu (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
Component import fixture chung từ demos/_fixtures và helper từ assets/lib của repo; xem SKILL.md trong repo để biết quy trình render.
Mọi thứ trên trang này đều có bản máy đọc. Đưa agent link JSON, hoặc thêm MCP server gimgs rồi bảo nó tìm motion template.
- Tải JSON — trong đó có công thức cảnh quay (timing, easing, bảng tham số, lỗi hay gặp) và toàn bộ source TSX.
- Clone repo video-shotcraft; component import fixture chung từ demos/_fixtures và helper từ assets/lib.
- Đăng ký component thành <Composition> trong project Remotion rồi render bằng `npx remotion render`, hoặc dựng lại công thức bằng engine của bạn.
curl -s https://gimgs.net/motion/type-assembly-moves.jsongit 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.mp4claude 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 "Chữ tập hợp: bốn kiểu" (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).
// ===== 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>
);
};
- Read the recipePrompt = when to use, intention, parameter table with typical values and how each one feels, known pitfalls. Treat values as calibrated starting points.
- Get the codetool: git
git clone https://github.com/Vincentwei1021/video-shotcraft && cd video-shotcraft && npm install - Register + rendertool: 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 - Or re-implementPort the timing/easing from the recipe to HTML + WAAPI (gimgs motion contract) or any engine; keep the same segment windows and easing.
| param | type | default | description |
|---|---|---|---|
| — | |||
Cùng danh mục

Ba từ tiếp sức bằng hình học
Ba từ lợi ích mỗi từ mang riêng một bộ hình học tiếp sức — vòng tròn nét đứt tự xoay thu nhỏ → ba vòng tròn nét liền mọc lên lần lượt → ánh kim sheen quét qua rồi thu về trắng thuần; mỗi từ mới viền nét rồi tô đầy.

Tiếp sức từ với cuộn phim trang
Cột trái các thẻ trang đen trắng cuộn theo bước, bên phải từ lớn chữ serif tiếp sức tại chỗ (danh từ cố định + động từ luân phiên) — chỉ khi đổi từ mới cuộn một bước, khớp chính xác điểm giữa thẻ trang hiện tại.

Cuộn từ dọc theo nhịp
Từ cuối câu đổi trên cuộn dọc, 3 lần đổi từ mỗi lần 0.55s, nhanh trước chậm sau kèm hơi overshoot; các dòng lân cận nhòe dọc và xám dần theo khoảng cách, từ ở giữa vừa đậu liền nhuộm từ xám sang màu nhấn.

Hai cách tiết lộ code song song
Cùng một khối code tô cú pháp được tiết lộ hai cách song song — trái: stagger cấp dòng mờ vào nổi lên 8px, phải: gõ từng ký tự nhưng giữ nguyên màu token gốc, con trỏ vuông #3a4468 đệm dưới ký tự hiện tại.