Motion · Chữ & title card

Thẻ tiêu đề giấy

Một câu in ép từng chữ lên giấy, một từ được đánh dấu nghiêng màu nhấn, khép lại bằng một gạch ngắn.

960×54016:930 fps1.83s · 55 frameremotionGiấy phép: Apache-2.0#shotcraft#remotion#typography#transition#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

Khi nào dùng

Thẻ tiêu đề chuyển chương/tuyên bố giá trị; thẻ dẫn dắt trước khi một tính năng quan trọng xuất hiện; nhịp thở của cả phim

Ý đồ

Cho người xem một câu để thở giữa hai đoạn hình ảnh sản phẩm: nói rõ "tiếp theo xem gì, nó đáng giá gì". Chất liệu in ép letterpress khiến thẻ tiêu đề và hình ảnh sản phẩm phong cách giấy/mực thuộc cùng một thế giới.

Thời lượng & năng lượng

  • Thời lượng: 1.7–1.8s (50–55f)
  • Năng lượng: Thấp (nhịp thở, ngăn cách hai cảnh năng lượng cao)

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)

意图

在两段产品画面之间给观众一句话的喘息:说清"接下来看什么、它值什么"。letterpress 压印质感让字卡与纸墨风格的产品画面同属一个世界。

动效核心

  • 单句文案逐词入场:scale 从大压到 1 + blur→0 + opacity(letterpress 压印配方)
  • 每句恰好一个重点词:italic + 强调色(模板片为琥珀)
  • 强调色短下划线 scaleX 0→1 收束
  • 尾部整卡淡出交棒;可挂 mono 小字副行 + DigitRoll 数字

参数表

| 参数 | 典型值 | 调节手感 | |------|--------|----------| | 逐词入场 | 第 i 词 delay = 4 + i·4、9f,bezier(0.2,0.75,0.3,1);scale 1.28→1 + blur 7px→0 + opacity | scale 起点 1.28 是"压印"读感的甜点——对照暗场版 TitleCard 用 40px 上移+blur 10,同配方明暗两版 | | 重点词 | italic + 强调色 oklch(52% 0.115 65),一句恰好一个 accent | 两个 accent 等于没有 accent;重点词选功能名/收益词(C2) | | 下划线 | 16→34f scaleX 0→1,220×6px 强调色 | 下划线是收束信号,出现即预告本卡快结束 | | 时长 | 全片 4 张字卡均 50–55f(≈1.8s) | 字卡时长定式化——观众读一句话的时间,长了拖节奏短了读不完 | | 版式 | 字号 116/serif;纸底 oklch(97.5% 0.008 82) + 中心暖光 radial;尾部 8f 淡出 | 纸底+暖光与产品画面同色系,字卡才不像广告插播 | | 副行 | mono 小字 + DigitRoll(如 "5 of 31 fetched today");digit-roll 参数见 list-stack-press 卡计数器行 | 副行给具体数字最有说服力;数字滚动必须在本卡淡出前落定(教训)——排节拍从本卡淡出帧倒推 |

声音

字卡出场统一钉 swoosh-quick(模板片四张字卡钉在 f220/565/725/885 同音)——同类元素同音是词汇表纪律(S2);字卡本身无落地重音,能量留给前后镜头。

已知坑

  • 文案要具体:带产品功能名+具体收益,抽象隐喻词被用户逐字改掉(C2——"one board"→"one place to go"、加 "Paper Radar");文案在画面锁定后按最终镜头重写一遍(C1)
  • 重要功能出场前要有引导字卡(C2 判例:文档镜头前应加一句引导,如 "Every project, linked to your weekly report")——字卡不是装饰,是章节路标

参考实现

demos/typography/paper-title-card/PaperTitleCard.tsx(原 template/src/aifl/PaperTitleCard.tsx,参数化:words/sub/subDigits 可换文案)

Mã nguồn tham chiếu (Remotion)

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

Prompt

Use the video-shotcraft shot recipe "Thẻ tiêu đề giấy" (Remotion composition, Apache-2.0) published on gimgs.net.
Recipe JSON (prompt = timing/easing/parameter table/pitfalls, files = TSX source): https://gimgs.net/motion/paper-title-card.json
Raw TSX: https://gimgs.net/motion/paper-title-card/template.html
Repository: https://github.com/Vincentwei1021/video-shotcraft  (component: demos/typography/paper-title-card/PaperTitleCard.tsx; imports demos/_fixtures and assets/lib)
Spec: 960x540, 30 fps, 1.83s

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

5,573 chars · Apache-2.0Mở template gốc ↗
// ===== demos/typography/paper-title-card/PaperTitleCard.tsx =====
// paper-title-card —— 一句话逐词 letterpress 压印上纸字卡
// 章节转场/价值主张呼吸位:单句文案逐词压印入场(scale 大→1 + blur→0),
// 每句恰一个强调色斜体重点词,强调色下划线 scaleX 收束,尾部整卡淡出。
// 全片多张字卡统一 50–55f;纸底+中心暖光与纸墨风格产品画面同色系。
// 参数化:words/sub/subDigits 可换成目标产品文案;subDigits 走 DigitRoll
// 数字滚动(必须在本卡淡出前落定)。渲染本 demo 前把 text 复制进项目即可。
import React from 'react';
import { AbsoluteFill, interpolate, useCurrentFrame, Easing } from 'remotion';

export const PAPER_TITLE_CARD_DURATION = 55; // ≈1.8s @30fps

const SERIF = 'ui-serif, Georgia, "Times New Roman", serif';
const MONO = 'ui-monospace, SFMono-Regular, Menlo, monospace';

// 每句恰好一个 accent:功能名/收益词(C2)
const WORDS: { text: string; accent?: boolean }[] = [
  { text: 'All' },
  { text: 'your' },
  { text: 'team’s' },
  { text: 'research,' },
  { text: 'one', accent: true },
  { text: 'place' },
  { text: 'to' },
  { text: 'go.' },
];
const SUB = 'of 31 fetched today';
const SUB_DIGITS = '5';

// 数字滚动列(odometer 风格,复制自 template DigitRoll,数字带双份拼接保证
// 任何目标位都有滚感;tabular-nums 防横向抖)
const DIGITS = '0123456789';
const DigitColumn: React.FC<{ ch: string; delay: number; lineH: number; color: string }> = ({ ch, delay, lineH, color }) => {
  const frame = useCurrentFrame();
  if (ch < '0' || ch > '9') {
    return <span style={{ fontSize: 26, lineHeight: `${lineH}px`, color }}>{ch}</span>;
  }
  const target = DIGITS.indexOf(ch);
  const t = interpolate(frame, [delay, delay + 22], [0, 1], {
    extrapolateLeft: 'clamp',
    extrapolateRight: 'clamp',
    easing: Easing.bezier(0.25, 0.8, 0.25, 1),
  });
  const offset = (10 + target) * t * lineH;
  return (
    <span style={{ display: 'inline-block', height: lineH }}>
      <span style={{ display: 'block', transform: `translateY(${-offset}px)` }}>
        {(DIGITS + DIGITS).split('').map((d, j) => (
          <span
            key={j}
            style={{
              display: 'block',
              fontSize: 26,
              lineHeight: `${lineH}px`,
              color,
              fontVariantNumeric: 'tabular-nums',
            }}
          >
            {d}
          </span>
        ))}
      </span>
    </span>
  );
};

const DigitRoll: React.FC<{ value: string; delay: number; color: string }> = ({ value, delay, color }) => {
  const lineH = 26 * 1.15;
  return (
    <span style={{ display: 'inline-flex', overflow: 'hidden', height: lineH, verticalAlign: 'bottom' }}>
      {value.split('').map((c, i) => (
        <DigitColumn key={i} ch={c} delay={delay + i * 4} lineH={lineH} color={color} />
      ))}
    </span>
  );
};

export const PaperTitleCard: React.FC = () => {
  const frame = useCurrentFrame();
  const duration = PAPER_TITLE_CARD_DURATION;
  const fadeOut = interpolate(frame, [duration - 8, duration], [1, 0], {
    extrapolateLeft: 'clamp',
    extrapolateRight: 'clamp',
  });
  const underline = interpolate(frame, [16, 34], [0, 1], {
    extrapolateLeft: 'clamp',
    extrapolateRight: 'clamp',
    easing: Easing.bezier(0.3, 0, 0.2, 1),
  });
  const subT = interpolate(frame, [10, 22], [0, 1], {
    extrapolateLeft: 'clamp',
    extrapolateRight: 'clamp',
  });

  return (
    <AbsoluteFill
      style={{
        backgroundColor: 'oklch(97.5% 0.008 82)',
        justifyContent: 'center',
        alignItems: 'center',
        opacity: fadeOut,
        backgroundImage:
          'radial-gradient(1100px 750px at 50% 42%, oklch(99.3% 0.014 88 / 0.85), transparent 65%)',
      }}
    >
      <div style={{ textAlign: 'center', maxWidth: 1500 }}>
        <div
          style={{
            fontFamily: SERIF,
            fontSize: 116,
            fontWeight: 600,
            lineHeight: 1.14,
            color: 'oklch(18% 0.006 82)',
            letterSpacing: '-0.012em',
            display: 'flex',
            flexWrap: 'wrap',
            justifyContent: 'center',
            columnGap: '0.26em',
          }}
        >
          {WORDS.map((w, i) => {
            const delay = 4 + i * 4;
            const t = interpolate(frame, [delay, delay + 9], [0, 1], {
              extrapolateLeft: 'clamp',
              extrapolateRight: 'clamp',
              easing: Easing.bezier(0.2, 0.75, 0.3, 1),
            });
            return (
              <span
                key={i}
                style={{
                  opacity: t,
                  transform: `scale(${1.28 - 0.28 * t})`,
                  filter: `blur(${(1 - t) * 7}px)`,
                  display: 'inline-block',
                  fontStyle: w.accent ? 'italic' : 'normal',
                  color: w.accent ? 'oklch(52% 0.115 65)' : undefined,
                }}
              >
                {w.text}
              </span>
            );
          })}
        </div>
        <div
          style={{
            height: 6,
            width: 220,
            margin: '38px auto 0',
            borderRadius: 3,
            background: 'oklch(52% 0.115 65)',
            transform: `scaleX(${underline})`,
          }}
        />
        <div
          style={{
            fontFamily: MONO,
            fontSize: 26,
            letterSpacing: '0.12em',
            color: 'oklch(50% 0.006 82)',
            marginTop: 34,
            opacity: subT,
            textTransform: 'uppercase',
            display: 'flex',
            justifyContent: 'center',
            alignItems: 'baseline',
            gap: '0.5em',
          }}
        >
          <DigitRoll value={SUB_DIGITS} delay={12} color="oklch(52% 0.115 65)" />
          <span>{SUB}</span>
        </div>
      </div>
    </AbsoluteFill>
  );
};


  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/paper-title-card/PaperTitleCard.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