Motion · Chữ & title card

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.

1920×108016:930 fps5.1s · 153 frameremotionGiấy phép: Apache-2.0#shotcraft#remotion#typography

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

Cảnh tiết lộ code/cấu hình; demo "chỉ ba dòng" cho sản phẩm lập trình viên; cảnh tham chiếu chọn kiểu khi cần so sánh hai nhịp tiết lộ

Ý đồ

Tiết lộ code chỉ có hai giải pháp thực sự: cả dòng nổi lên (đọc cấu trúc) và gõ từng chữ một (đọc quá trình). Thẻ này đặt cả hai song song trên cùng một đoạn code làm cảnh tham chiếu để chọn kiểu: bên trái 4 dòng 4 nhịp là xong, hợp khi "code không phải trọng tâm, kết quả mới là trọng tâm"; bên phải 138 khung mới gõ xong 59 ký tự, hợp khi "xem tôi viết". Chi tiết mấu chốt là bên phải gõ không mất màu — lỗi thường gặp là gõ ra chữ trắng trước rồi mới tô màu, như vậy thoái hóa thành hiển thị lại của terminal, không phải trình soạn code.

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

  • Thời lượng: khoảng 4.6s (138f@30fps)
  • Năng lượng: Trung bình (bên phải liên tục gõ tiếp, bên trái thu gọn một lần)

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)

意图

代码揭示只有两种真解法:整行浮出来(读的是结构)和一个字一个字打 (读的是过程)。这卡把两者并排放在同一段代码上,是选型用的对照镜: 左侧 4 行 4 拍就完,适合"代码不是重点、结果才是";右侧 138 帧才打完 59 个字符,适合"看我写"。关键细节是右侧打字不丢色——常见错误是 打字时先出白字再着色,那就退化成终端回显,不是代码编辑器。

动效核心

  • token 模型:每行是 [[文本, 颜色], …],6 色板(关键字 #c792ea、

标识 #e8eaf0、函数 #82aaff、字符串 #c3e88d、标点 #89ddff、注释 #546e7a)

  • 左侧行级 stagger:seg(t, 0.08+i·0.14, +0.3, E.outCubic)——行间 0.14

(≈19f)、单行窗 0.3(≈41f),opacity = k、translateY = (1-k)·8px

  • 右侧逐字符:typed = floor(seg(t, 0.08, 0.9) · chars.length) 线性推进,

每个字符 span 建的时候就带 token 色,打到时只翻 opacity 0→1 —— 着色在 setup 期完成,render 期不碰颜色,所以字一出来就是对的色

  • 光标是背景块不是竖线:i === typed 的字符 background:#3a4468 且强制

opacity:1 —— 下一个待打字符先以色块占位,块往右挪即是光标移动

  • 两侧面板同规格(45% 宽、72% 高、#10121a 底 + #1c2030 边框、圆角 8),

顶部 8px 字距 2px 的小标签 LINE FADE-IN / CHAR TYPING 标注手法

  • 右侧行 min-height:1.9em 撑住空行——不撑的话未打的行高度为 0,

面板内容会随打字往下长(跳版)

参数表

| 参数 | 典型值 | 调节手感 | |------|--------|----------| | 代码量 | 4 行 / 约 59 字符 | 打字速度 = 字符数 / 113f;行数超过 6 行面板要缩字号,超过 100 字符右侧打不完 | | 行 stagger | 0.14(≈19f),单行窗 0.3(≈41f) | 19f 是"逐行"可读下限;<8f 四行齐亮没有 stagger,>28f 最后一行等太久 | | 行位移 | (1-k)·8px 上浮 | 8px 是代码块该有的克制幅度;>20px 行像卡片飞进来,抢了代码本身 | | 打字窗 | seg(t, 0.08, 0.9)(11f→124f) | 结束点留 14f 静置读全文;推到 1.0 打完即切,最后一行没人读 | | 光标块 | #3a4468 背景,仅当前字符 | 方块光标是编辑器语感;换成竖线要另加闪烁,代码块里反而更碎 | | 面板 | 45%×72%、#10121a 底、#1c2030 边框、圆角 8、12px/1.9 行高 | 1.9 行高是代码可读下限;两侧必须同规格,尺寸不一就不是对照而是两个镜头 | | 空行占位 | 右侧行 min-height:1.9em | 不设则面板随打字长高,整块跳版——这是本卡最容易漏的一处 |

已知坑

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

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

  • 换成项目代码时字符总数要贴近 59:右侧打字是把 113f 平摊到全部

字符上的,代码翻倍则每字符不到 1 帧(糊成整行刷出),代码减半则一字 一拍慢得像卡顿——改代码量必须同步调打字窗,别只换文案

  • 着色必须在 setup 期写进每个 span:render 期改色(先白后染)会把

"代码编辑器"降级成"终端回显",这是同类效果最常见的失手

  • 左侧行级淡入与右侧逐字符不同步(左侧 4 行 ~68f 就收完,右侧要打到

124f),这是对照镜的本意;若实战只留一侧,另一侧的时间窗要重新铺满全片

  • token 数组是手写的,没有真词法分析——换代码时得手动切 token;

切错色比不着色更糟(关键字被染成字符串色,懂代码的观众立刻出戏)

  • 实战里通常只留一侧:对照版是选型镜,正片里两块面板一起动会分散

注意力,选定手法后单面板居中放大

参考实现

demos/typography/typing-code-block/ (TypingCodeBlock.tsx)

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

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

Prompt

Use the video-shotcraft shot recipe "Hai cách tiết lộ code song song" (Remotion composition, Apache-2.0) published on gimgs.net.
Recipe JSON (prompt = timing/easing/parameter table/pitfalls, files = TSX source): https://gimgs.net/motion/typing-code-block.json
Raw TSX: https://gimgs.net/motion/typing-code-block/template.html
Repository: https://github.com/Vincentwei1021/video-shotcraft  (component: demos/typography/typing-code-block/TypingCodeBlock.tsx; imports demos/_fixtures and assets/lib)
Spec: 1920x1080, 30 fps, 5.1s

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

3,289 chars · Apache-2.0Mở template gốc ↗
// ===== demos/typography/typing-code-block/TypingCodeBlock.tsx =====
// typing-code-block — Code Block Reveal 代码块揭示(motion-lab 定稿转原生 Remotion)
// 同一段语法高亮代码的两种 reveal 对照:左侧逐行淡入上浮(行级 stagger),
// 右侧逐字符打字但保持 token 着色,当前字符位以方块底色块提示光标。
// 设计坐标 480×270(DesignStage 等比放大),参数表数值以此坐标系标定。
import React from 'react';
import { DesignStage, E, seg, useT } from '../../_fixtures/Motion';

export const TYPING_CODE_BLOCK_DURATION = 138; // 4600ms @30fps

// token 配色:关键字/标识符/函数/字符串/标点/注释
const K = '#c792ea';
const ID = '#e8eaf0';
const FN = '#82aaff';
const ST = '#c3e88d';
const PU = '#89ddff';
const CM = '#546e7a';
// 每行是 [text, color] token 序列
const LINES: [string, string][][] = [
  [['const ', K], ['app', ID], [' = ', PU], ['createApp', FN], ['();', ID]],
  [['app', ID], ['.', PU], ['use', FN], ['(', ID], ['router', ID], [');', ID]],
  [['app', ID], ['.', PU], ['mount', FN], ['(', ID], ["'#root'", ST], [');', ID]],
  [['// ready', CM]],
];
// 右侧打字用的逐字符扁平序列(保留 token 颜色 + 行号)
const FLAT: { ch: string; color: string; row: number }[] = [];
LINES.forEach((line, row) => {
  for (const [txt, color] of line) for (const ch of txt) FLAT.push({ ch, color, row });
});

// 代码面板容器:左右两块共用的外框 + 顶部小标签
const Panel: React.FC<{ x: number; label: string; children: React.ReactNode }> = ({
  x,
  label,
  children,
}) => (
  <div
    style={{
      position: 'absolute',
      left: `${x}%`,
      top: '14%',
      width: '45%',
      height: '72%',
      background: '#10121a',
      border: '1px solid #1c2030',
      borderRadius: 8,
      padding: '10px 12px',
      boxSizing: 'border-box',
      fontFamily: '"SF Mono",Menlo,monospace',
      fontSize: 12,
      lineHeight: 1.9,
    }}
  >
    <div style={{ fontSize: 8, letterSpacing: 2, color: '#4a5270', marginBottom: 6 }}>{label}</div>
    {children}
  </div>
);

export const TypingCodeBlock: React.FC = () => {
  const t = useT();
  // 右侧打字进度:t∈[0.08,0.9] 线性推进到全部字符
  const typed = Math.floor(seg(t, 0.08, 0.9) * FLAT.length);
  return (
    <DesignStage bg="#0a0b10">
      {/* 左:逐行淡入上浮(行级 stagger) */}
      <Panel x={3.5} label="LINE FADE-IN">
        {LINES.map((line, i) => {
          const k = seg(t, 0.08 + i * 0.14, 0.08 + i * 0.14 + 0.3, E.outCubic);
          return (
            <div key={i} style={{ opacity: k, transform: `translateY(${(1 - k) * 8}px)` }}>
              {line.map(([txt, color], j) => (
                <span key={j} style={{ color }}>
                  {txt}
                </span>
              ))}
            </div>
          );
        })}
      </Panel>
      {/* 右:逐字符打字(保色),当前字符位带方块光标底色 */}
      <Panel x={51.5} label="CHAR TYPING">
        {LINES.map((_, row) => (
          <div key={row} style={{ minHeight: '1.9em' }}>
            {FLAT.map((c, i) =>
              c.row === row ? (
                <span
                  key={i}
                  style={{
                    color: c.color,
                    // 光标位字符以底色块形式提示(保持可见)
                    opacity: i < typed || i === typed ? 1 : 0,
                    background: i === typed && typed < FLAT.length ? '#3a4468' : 'transparent',
                  }}
                >
                  {c.ch}
                </span>
              ) : null,
            )}
          </div>
        ))}
      </Panel>
    </DesignStage>
  );
};


  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/typing-code-block/TypingCodeBlock.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