Motion · Chữ & title card
Chữ đồng bộ theo âm thanh
Hai kiểu đồng bộ chữ theo âm thanh — font-weight-pump nét chữ đập đậm theo nhịp rồi bật lại, và karaoke-fill-sync từng chữ tô sáng theo lời bình.
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ể
Tại khung trúng nhịp, nét chữ tức thời đậm lên (stroke + độ đậm nhảy vọt), suy giảm bật lại trong khoảng 10f; nhịp nhấn giãn thêm 8%
Đoạn năng lượng cao dẫn dắt bởi nhạc; tiêu đề làm loa trầm
Mỗi chữ tô màu đậm từ trái sang phải, tiến độ theo tốc độ nói, đọc xong giữ nguyên; chữ đang hoạt động có gạch chân đọc theo
Đoạn lời bình/nhấn mạnh khi nói; dẫn ánh nhìn cho khẩu hiệu nhiều chữ
Khi nào dùng
Đoạn tiêu đề/khẩu hiệu gắn chặt với âm thanh; A gắn theo nhịp (tiếng trống), B gắn theo giọng nói (lời bình từng chữ)
Ý đồ
Thư viện hiệu ứng chữ trước nay toàn nói về "vào hình thế nào", chưa có từ nào quản "chữ đang trên màn hình sống theo âm thanh ra sao". Hai kiểu này bổ sung: A biến tiêu đề thành bộ phận nhịp điệu — nét chữ đập đậm theo tiếng trống trầm rồi bật lại, chữ đang nhảy, hợp làm loa trầm thị giác cho đoạn năng lượng cao; B biến khẩu hiệu thành phụ đề đọc theo — lời bình đọc tới chữ nào chữ đó tô sáng từ trái sang phải, dẫn ánh nhìn đi theo, hợp đoạn lời bình/nhấn mạnh khi nói. Điểm chung là chữ không di chuyển, chỉ thuộc tính chuyển động: không dịch chuyển, không vào/ra, thuần túy độ đậm/màu sắc thở theo nhịp âm thanh.
Thời lượng & năng lượng
- Thời lượng: Mỗi kiểu 4–5s; A mỗi nhịp chiếm cửa sổ suy giảm 10f, B mỗi chữ theo tốc độ nói 15–35f
- Năng lượng: A cao (cảm giác sàn nhảy) / B trung bình (dẫn dắt đọc theo)
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 让标语变成跟读字幕—— 旁白读到哪个词哪个词从左到右填色点亮,视线被牵着走,适合口播/ 标语强调段。共同点是字不动、属性动:无位移、无入退场, 纯粹是字重/颜色在声音的拍点上呼吸。
两式选型
| 式 | 做法 | 适用 | |----|------|------| | A font-weight-pump 字重脉冲 | 命中帧笔画瞬间变粗(stroke+字重跳变),~10f 衰减弹回;重音拍额外撑宽 8% | 音乐驱动的高能段;标题当低音炮 | | B karaoke-fill-sync 填色随读 | 每词深色从左到右填亮,进度跟语速,读完保持;活跃词下带读指下划线 | 旁白/口播强调段;多词标语的视线引导 |
参数表
| 参数 | 典型值 | 调节手感 | |------|--------|----------| | A 脉冲包络 | env=(1−t/10)^0.8(t=命中后帧数,≥10f 精确归 0);-webkit-text-stroke: 10px·env + fontWeight 在 env>0.15 窗口 400↔900 跳变 | stroke 连续衰减 + 字重离散跳变叠加才读作"连续变粗";单用 fontWeight 只有两挡、单用 stroke 变化太细 | | A 节拍 | 每 20f 一拍;重音拍额外 scaleX = 1+0.08·env(transform 缩放不改排版) | 拍距必须 > 衰减窗(20>10),两拍重叠读作抽搐 | | A 节拍参照物 | 画面下方一排小节拍点,命中哪拍哪个点闪深并放大 | 静音环境(预览/静帧)没有这排点就读不出"在跟拍" | | B 词级时间表 | 逐词 [start,end] 帧区间,词内 linear 填充;词间留 4–10f 停顿(换气) | 时间戳实战来自旁白打点表;均匀分配读作机器朗读 | | B 填色实现 | 每词双层同文本叠放:底层浅灰 + 上层深色 clipPath: inset(0 X% 0 0),X=(1−p)·100 | 逐词独立叠层免量测词宽占比;整行单层 clip 需精确像素表、易漂 | | B 读指 | 活跃词下 8px 深色下划线,宽度 = p·100% | 4px 浅灰在 1080p 缩略下不可见(本批实渲踩坑);读完的词不留线 | | 收尾 | A 末拍衰减归零后 / B 末词填完后真静止 ≥20f | R1 呼吸判例 |
已知坑
- demo 在灰阶/占位素材上调校通过——参数是调校起点非实战定稿,
首次实战须以真实素材回验
- 两式都是声音强依赖手法:A 命中帧必须与鼓点同帧、B 填色区间必须
跟旁白词边界(钉帧走 sound-design §4.5 相对起点)——无声上片则 A 读作 抽风、B 读作莫名变色,别在无音轨段落用
- A 式实战想要真·可变字体连续字重(wght 轴逐帧插值)需先确认字体加载
与渲染器支持;demo 的 stroke+跳变方案是无字体风险的兜底,效果已可感
- A 式全片 ≤1 段(P4)——标题蹦迪是抢戏大户,两段同片互相拆台
- B 式词数 ≤6/屏:更多词时填色进度读不过来,拆行或拆镜
参考实现
demos/typography/type-rhythm-sync/ (FontWeightPump.tsx / KaraokeFillSync.tsx)
Mã nguồn tham chiếu (Remotion)
- demos/typography/type-rhythm-sync/FontWeightPump.tsx — https://raw.githubusercontent.com/Vincentwei1021/video-shotcraft/main/demos/typography/type-rhythm-sync/FontWeightPump.tsx
- demos/typography/type-rhythm-sync/KaraokeFillSync.tsx — https://raw.githubusercontent.com/Vincentwei1021/video-shotcraft/main/demos/typography/type-rhythm-sync/KaraokeFillSync.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-rhythm-sync.jsongit clone https://github.com/Vincentwei1021/video-shotcraft
# component: demos/typography/type-rhythm-sync/FontWeightPump.tsx, demos/typography/type-rhythm-sync/KaraokeFillSync.tsx
npx remotion render <CompositionId> out.mp4claude mcp add --transport http gimgs https://gimgs.net/mcp
# then: search_motion_templates / get_motion_template("type-rhythm-sync")Prompt
Use the video-shotcraft shot recipe "Chữ đồng bộ theo âm thanh" (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-rhythm-sync.json Raw TSX: https://gimgs.net/motion/type-rhythm-sync/template.html Repository: https://github.com/Vincentwei1021/video-shotcraft (component: demos/typography/type-rhythm-sync/FontWeightPump.tsx, demos/typography/type-rhythm-sync/KaraokeFillSync.tsx; imports demos/_fixtures and assets/lib) Spec: 1920x1080, 30 fps, 4.72s 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-rhythm-sync/FontWeightPump.tsx =====
// 字重脉冲(font-weight-pump)——标题笔画随节拍变粗又弹回,像文字跟着低音鼓蹦迪。
// 节拍每 20f 一拍:帧 30/50/70/90/110 命中。命中瞬间 -webkit-text-stroke 0→10px
// 跳满,随后 10f 幂衰减 (1-t/10)^0.8 弹回 0;fontWeight 在命中窗口 400→900 离散跳变
// (env>0.15),配合 stroke 连续衰减读作"连续变粗"。第 3、5 拍(帧 70/110)为重音,
// 额外 scaleX 1→1.08 同衰减(transform 缩放不改排版)。底部 5 个节拍点作节拍参照。
// 结构:0–29f 静止 hold;30–119f 五拍脉冲;120–139f 真静止收尾(20f,110+10=120 无残留)。
import React from 'react';
import { useCurrentFrame } from 'remotion';
import { G } from '../../_fixtures/Fixtures';
const BEATS = [30, 50, 70, 90, 110];
const ACCENTS = new Set([2, 4]); // 第 3、5 拍重音
const DECAY = 10; // 衰减帧数
// 命中后 10f 幂衰减包络:t=0 → 1,t>=10 → 精确 0(保证结尾真静止)
const envAt = (frame: number, beat: number) => {
const t = frame - beat;
if (t < 0 || t >= DECAY) return 0;
return Math.pow(1 - t / DECAY, 0.8);
};
export const FontWeightPump: React.FC = () => {
const frame = useCurrentFrame();
// 只可能有一个活跃拍(拍距 20f > 衰减 10f),取最大包络及其拍序号
let env = 0;
let activeBeat = -1;
BEATS.forEach((b, i) => {
const e = envAt(frame, b);
if (e > env) {
env = e;
activeBeat = i;
}
});
const strokeW = 10 * env; // 笔画粗细连续衰减
const weight = env > 0.15 ? 900 : 400; // 命中窗口离散跳字重
const accent = activeBeat >= 0 && ACCENTS.has(activeBeat);
const scaleX = accent ? 1 + 0.08 * env : 1; // 重音拍变宽一挡
return (
<div
style={{
width: 1920,
height: 1080,
background: G.bg,
position: 'relative',
overflow: 'hidden',
fontFamily: 'Helvetica, Arial, sans-serif',
}}
>
{/* 定宽居中容器,transform 缩放不改排版 */}
<div
style={{
position: 'absolute',
left: 0,
top: 0,
width: 1920,
height: 1080,
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
}}
>
<div
style={{
fontSize: 150,
fontWeight: weight,
color: G.ink,
letterSpacing: 6,
whiteSpace: 'nowrap',
WebkitTextStroke: `${strokeW}px ${G.ink}`,
transform: `scaleX(${scaleX})`,
transformOrigin: 'center center',
}}
>
PUMP IT UP
</div>
</div>
{/* 底部节拍点:5 个,命中哪拍哪个点闪 ink 并放大 */}
<div
style={{
position: 'absolute',
left: 0,
bottom: 130,
width: 1920,
display: 'flex',
justifyContent: 'center',
gap: 56,
}}
>
{BEATS.map((b, i) => {
const e = envAt(frame, b);
const dotOpacity = e > 0.02 ? Math.min(1, 0.3 + e * 1.2) : 0;
const dotScale = 1 + 0.8 * e;
return (
<div
key={i}
style={{
width: 26,
height: 26,
borderRadius: 13,
background: G.line,
position: 'relative',
transform: `scale(${dotScale})`,
}}
>
<div
style={{
position: 'absolute',
inset: 0,
borderRadius: 13,
background: G.ink,
opacity: dotOpacity,
}}
/>
</div>
);
})}
</div>
</div>
);
};
// ===== demos/typography/type-rhythm-sync/KaraokeFillSync.tsx =====
// 卡拉OK填色随读(karaoke-fill-sync)——旁白读到哪个词,哪个词就被深色从左到右
// 点亮。两行标语 "SHIP FASTER / BREAK NOTHING",每个词双层同文本叠放:底层 G.line
// 浅灰字,上层 G.ink 深字用 clip-path: inset(0 X% 0 0) 按词内进度线性填充(逐词独立
// 叠层,clip 百分比即词内进度,无需量测词在整行的像素占比)。词级时间表模拟语速:
// SHIP 20–38、FASTER 42–75(长词慢读)、BREAK 85–103、NOTHING 107–130,词间停顿。
// 正在填的词底下有 8px 深色下划线跟随填充右缘作读指。0–19f hold;130–149f 真静止。
import React from 'react';
import { useCurrentFrame, interpolate } from 'remotion';
import { G } from '../../_fixtures/Fixtures';
type Word = { text: string; start: number; end: number };
const LINES: Word[][] = [
[
{ text: 'SHIP', start: 20, end: 38 },
{ text: 'FASTER', start: 42, end: 75 },
],
[
{ text: 'BREAK', start: 85, end: 103 },
{ text: 'NOTHING', start: 107, end: 130 },
],
];
const KaraokeWord: React.FC<{ word: Word; frame: number }> = ({ word, frame }) => {
// 词内 linear 填充进度,clamp 保证读完保持
const p = interpolate(frame, [word.start, word.end], [0, 1], {
extrapolateLeft: 'clamp',
extrapolateRight: 'clamp',
});
const active = frame >= word.start && frame < word.end; // 正在读这个词
return (
<span style={{ position: 'relative', display: 'inline-block' }}>
{/* 底层:浅灰未读字 */}
<span style={{ color: G.line }}>{word.text}</span>
{/* 上层:深字按进度从左到右揭开 */}
<span
style={{
position: 'absolute',
inset: 0,
color: G.ink,
clipPath: `inset(0 ${(1 - p) * 100}% 0 0)`,
}}
>
{word.text}
</span>
{/* 读指下划线:只在正在填的词下出现,右缘跟随填充进度 */}
{active && (
<span
style={{
position: 'absolute',
left: 0,
bottom: -14,
width: `${p * 100}%`,
height: 8,
background: G.ink,
}}
/>
)}
</span>
);
};
export const KaraokeFillSync: React.FC = () => {
const frame = useCurrentFrame();
return (
<div
style={{
width: 1920,
height: 1080,
background: G.bg,
overflow: 'hidden',
display: 'flex',
flexDirection: 'column',
justifyContent: 'center',
paddingLeft: 240,
boxSizing: 'border-box',
fontFamily: 'Helvetica, Arial, sans-serif',
fontSize: 130,
fontWeight: 800,
letterSpacing: 2,
lineHeight: 1.45,
}}
>
{LINES.map((words, li) => (
<div key={li} style={{ display: 'flex', gap: 48 }}>
{words.map((w) => (
<KaraokeWord key={w.text} word={w} frame={frame} />
))}
</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-rhythm-sync/FontWeightPump.tsx, demos/typography/type-rhythm-sync/KaraokeFillSync.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.