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.

1920×108016:930 fps4.72s · 142 frameremotionGiấy phép: Apache-2.0#shotcraft#remotion#typography#rhythm

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ể

font-weight-pumpfont-weight-pump · Đập độ đậm
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
karaoke-fill-synckaraoke-fill-sync · Tô sáng đọc theo
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.

  1. 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.
  2. Clone repo video-shotcraft; component import fixture chung từ demos/_fixtures và helper từ assets/lib.
  3. Đă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.json
git 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.mp4
claude 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).

JSON · Markdown · llms.txt

6,051 chars · Apache-2.0Mở template gốc ↗
// ===== 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>
  );
};


  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-rhythm-sync/FontWeightPump.tsx, demos/typography/type-rhythm-sync/KaraokeFillSync.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
—

Cùng danh mục