Motion · Chữ & title card

Máy đánh chữ: hai tâm trạng

Hai kiểu máy đánh chữ — terminal-typewriter gõ lệnh xong kích nổ đổi cảnh, và error-retype xóa gõ lại thành vở kịch ba hồi "đổi ý".

1920×108016:930 fps4.89s · 147 frameremotionGiấy phép: Apache-2.0#shotcraft#remotion#typography#opening

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ể

terminal-typewriter
Gõ lệnh 2f/ký tự, con trỏ chớp theo sóng vuông (f%12<6); khung Enter cả cảnh đẩy gấp trong 6f, scale 1→3.2 (gốc khóa tại tâm dòng lệnh) + mờ 10px ở 2f cuối, cắt cứng sang dashboard đứng vững 1.06→1
Mở đầu công cụ lập trình viên; sản phẩm bán điểm CLI
error-retype
Gõ 2f/ký tự → dừng 16f con trỏ chớp hai lần → xóa lùi 1.5f/ký tự → gõ lại 1.5f/ký tự không do dự; con trỏ sáng liên tục lúc gõ/xóa, chỉ chớp lúc dừng
Cảnh "đổi ý" của thẻ khẩu hiệu; văn án kiểu phủ định

Khi nào dùng

Mở đầu sản phẩm cho lập trình viên (A), thẻ chữ khẩu hiệu/điểm bán hàng (B); kiểu vào chữ tự mang tính thời gian

Ý đồ

Gõ chữ là kiểu vào chữ duy nhất tự mang tính thời gian — chữ đến từng cái một tự nó đã là nhịp điệu, không cần thêm hiệu ứng. A coi việc gõ chữ là ngòi nổ: cửa sổ terminal tối gõ từng chữ ra lệnh, con trỏ vuông chớp theo sóng vuông, khung Enter máy quay đẩy gấp vào dòng lệnh rồi cắt cứng sang giao diện sản phẩm — dòng lệnh là ngòi nổ của cảnh, chuẩn mực cho sản phẩm lập trình viên; B coi việc gõ chữ là độc thoại: gõ ra một từ tầm thường ("just a dashboard") → dừng lại do dự → xóa lùi đi → gõ nhanh hơn ra điểm bán hàng ("your command center") — vở kịch ba hồi do dự-phủ định-tuyên ngôn, toàn bộ kịch tính nằm trong ba tốc độ khác nhau. Khác với scramble-decode: giải mã ký tự lỗi là máy đang đoán, gõ chữ là người đang nói.

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

  • Thời lượng: A khoảng 5s / B khoảng 5.5s
  • Năng lượng: A trung bình cao / B trung bình thấp

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 把打字当独白:逐字打出平庸词 ("just a dashboard")→ 停顿犹豫 → 退格删掉 → 更快打出卖点词 ("your command center"),犹豫-否定-宣言的三幕剧,戏全在三档 速度差里。与 scramble-decode 的区别:乱码解码是机器在猜, 打字是人在说。

两式选型

| 式 | 做法 | 适用 | |----|------|------| | A terminal-typewriter | 2f/字符敲出命令,光标 f%12<6 方波闪;回车帧整场景 6f 急推 scale 1→3.2(origin 锁命令行中心)+ 末 2f blur 10px,硬切 dashboard 1.06→1 回稳 | 开发者工具开场;CLI 卖点产品 | | B error-retype | 打 2f/字符 → 停顿 16f 光标闪两下 → 退 1.5f/字符 → 重打 1.5f/字符无犹豫;光标打删常亮、停顿才闪 | slogan 字卡的"改口"戏;否定式文案 |

参数表

| 参数 | 典型值 | 调节手感 | |------|--------|----------| | 字符节奏 | substring(0, floor((f-起点)/步长)),帧确定无插值 | 任何缓动打字都读作加载动画 | | A 光标 | 方块,f%12<6 方波 | 淡入淡出的光标不是终端是网页 | | A 急推 | f58–64 scale 1→3.2 Easing.in(cubic),origin 钉命令行文字中心 | 幅度小于 3x 读作抖了一下,不是"冲进去" | | A 硬切 | f64 条件挂载切换(终端整棵卸载,blur 随之摘罩),落定回稳 4f | 交叉溶解毁掉引信-爆炸因果 | | B 三档速度 | 打 2f/字符、删 1.5f、重打 1.5f 且零犹豫 | 三档差必须可感——等速改口读作 bug 不是戏,命门 | | B 光标三态 | 打删常亮=果断;停顿 4f 半周期闪两下=犹豫;完稿闪两下后条件摘除 | 光标就是演员的脸,全程等速闪就没有"犹豫" | | B 排版 | 逐字符固定宽 span(58px 等宽),左缘锚定 | 变宽字体/整串测宽会逐帧重排抖动 | | 收尾 | A 静止 ≥77f / B 静止 ≥50f | 光标残留 0.05 opacity 都毁真静止 |

已知坑

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

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

  • 声音强依赖:键击声逐字符对齐、B 退格音调更低(sound-design

§4.5)——无声打字机只剩字幕机

  • A 回车急推是与 crash-zoom 的交棒衔接:急推末帧动势最猛处硬切,

切后新场景不得再推(一棒只交一次)

  • B 全片 ≤1 次——改口是一次性的戏剧转折,改两次口是口吃
  • 等宽字体或逐字符 span 二选一必须占一个,否则字符到来时

整行重排,打字变成抽搐

参考实现

demos/typography/typewriter-moves/ (TerminalTypewriter.tsx / TypewriterErrorRetype.tsx)

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

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

Prompt

Use the video-shotcraft shot recipe "Máy đánh chữ: hai tâm trạng" (Remotion composition, Apache-2.0) published on gimgs.net.
Recipe JSON (prompt = timing/easing/parameter table/pitfalls, files = TSX source): https://gimgs.net/motion/typewriter-moves.json
Raw TSX: https://gimgs.net/motion/typewriter-moves/template.html
Repository: https://github.com/Vincentwei1021/video-shotcraft  (component: demos/typography/typewriter-moves/TerminalTypewriter.tsx, demos/typography/typewriter-moves/TypewriterErrorRetype.tsx; imports demos/_fixtures and assets/lib)
Spec: 1920x1080, 30 fps, 4.89s

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

7,648 chars · Apache-2.0Mở template gốc ↗
// ===== demos/typography/typewriter-moves/TerminalTypewriter.tsx =====
// terminal-typewriter —— 终端打字机触发
// 深色终端窗居中,"$ acme deploy --prod" 逐字符敲出(2f/字符,帧确定
// substring),方块光标 12f 方波闪 → 敲完停 12f → 回车帧:整场景 6f
// Easing.in(cubic) 急推 scale 1→3.2 向命令行推入(末 2f 加 blur)硬切到
// FakeDashboard A 全屏,1.06→1 回稳 4f 落定。收尾真静止 ≥40f。
import React from 'react';
import { useCurrentFrame, interpolate, Easing } from 'remotion';
import { G, FakeDashboard } from '../../_fixtures/Fixtures';

const CMD = 'acme deploy --prod';

// 时间轴(30fps,共 145f)
const T = {
  typeStart: 10,                       // 开始敲字
  typeEnd: 10 + CMD.length * 2,        // 46:18 字符 × 2f
  enter: 58,                           // 敲完停 12f 后回车
  pushEnd: 64,                         // 6f 急推结束,硬切帧
  settleEnd: 68,                       // dashboard 1.06→1 回稳 4f
  total: 145,                          // f68 起真静止 77f
};

// 终端窗几何
const TW = 1100;
const TH = 620;
const TL = (1920 - TW) / 2; // 410
const TT = (1080 - TH) / 2; // 230
const TITLEBAR = 52;
const PAD = 34;
// 命令行基线(推入焦点):标题栏下第二行文字中心
const FOCUS_X = 960;
const FOCUS_Y = TT + TITLEBAR + PAD + 92; // ≈ 408

const TerminalWindow: React.FC<{ chars: number; cursorOn: boolean }> = ({ chars, cursorOn }) => (
  <div style={{
    width: TW, height: TH, background: '#1e1e1c', borderRadius: 14,
    boxShadow: '0 24px 64px rgba(0,0,0,0.35)',
    overflow: 'hidden', boxSizing: 'border-box',
  }}>
    {/* 标题栏:三圆点窗控(灰阶) */}
    <div style={{
      height: TITLEBAR, background: '#2a2a28', borderBottom: '1px solid #3a3a38',
      display: 'flex', alignItems: 'center', gap: 12, padding: '0 22px',
      boxSizing: 'border-box',
    }}>
      {['#6a6a68', '#8f8f8d', '#b5b5b3'].map((c, i) => (
        <div key={i} style={{ width: 16, height: 16, borderRadius: 8, background: c }} />
      ))}
      <div style={{ margin: '0 auto', height: 10, width: 200, background: '#4a4a48', borderRadius: 5 }} />
      <div style={{ width: 72 }} />
    </div>
    {/* 内容区 */}
    <div style={{
      padding: PAD, fontFamily: 'Menlo, Consolas, monospace', fontSize: 40,
      color: '#d8d8d6', lineHeight: 1.5,
    }}>
      {/* 一行历史输出做上下文 */}
      <div style={{ color: '#7a7a78', fontSize: 32, marginBottom: 18 }}>~/acme-app (main)</div>
      <div style={{ display: 'flex', alignItems: 'center', whiteSpace: 'pre' }}>
        <span style={{ color: '#9f9f9d' }}>{'$ '}</span>
        <span>{CMD.substring(0, chars)}</span>
        <span style={{
          display: 'inline-block', width: 24, height: 48, marginLeft: 4,
          background: '#d8d8d6', opacity: cursorOn ? 1 : 0,
        }} />
      </div>
    </div>
  </div>
);

export const TerminalTypewriter: React.FC = () => {
  const frame = useCurrentFrame();

  // 帧确定打字:2f/字符
  const chars = Math.min(CMD.length, Math.max(0, Math.floor((frame - T.typeStart) / 2)));

  // 方块光标 12f 周期方波闪(全程)
  const cursorOn = frame % 12 < 6;

  // 回车急推:整场景 scale 1→3.2,6f Easing.in(cubic),向命令行推入
  const pushScale = interpolate(frame, [T.enter, T.pushEnd], [1, 3.2], {
    extrapolateLeft: 'clamp', extrapolateRight: 'clamp',
    easing: Easing.in(Easing.cubic),
  });
  // 末 2f 运动模糊(f62–f64),硬切后摘罩=条件挂载
  const pushBlur = interpolate(frame, [T.pushEnd - 2, T.pushEnd], [0, 10], {
    extrapolateLeft: 'clamp', extrapolateRight: 'clamp',
  });

  // 硬切:f64 起终端场景整体卸载,dashboard 挂载
  const cut = frame >= T.pushEnd;

  // dashboard 落定:1.06→1 回稳 4f
  const dashScale = interpolate(frame, [T.pushEnd, T.settleEnd], [1.06, 1], {
    extrapolateLeft: 'clamp', extrapolateRight: 'clamp',
    easing: Easing.out(Easing.cubic),
  });

  return (
    <div style={{ width: 1920, height: 1080, background: G.bg, position: 'relative', overflow: 'hidden' }}>
      {!cut ? (
        <div style={{
          width: 1920, height: 1080,
          transform: `scale(${pushScale})`,
          transformOrigin: `${FOCUS_X}px ${FOCUS_Y}px`,
          ...(pushBlur > 0 ? { filter: `blur(${pushBlur}px)` } : {}),
        }}>
          <div style={{ position: 'absolute', left: TL, top: TT }}>
            <TerminalWindow chars={chars} cursorOn={cursorOn} />
          </div>
        </div>
      ) : (
        <div style={{
          width: 1920, height: 1080,
          transform: `scale(${dashScale})`,
          transformOrigin: '960px 540px',
        }}>
          <FakeDashboard variant="A" />
        </div>
      )}
    </div>
  );
};


// ===== demos/typography/typewriter-moves/TypewriterErrorRetype.tsx =====
// typewriter-error-retype|打字机误删重打
// 浅底居中大字:f2 起 2f/字符打出 "just a dashboard"(16 字符) → f32–48 停顿
// 16f(光标闪两下=犹豫) → f48 起 1.5f/字符逐字退格删掉 "a dashboard"(11 字符,
// 字符直接消失) → f68 起 1.5f/字符果断打出 "your command center"(19 字符,
// f95 打完) → 光标闪两个周期(10f/周期)后 f110 永久熄灭。节奏三档:打 2f/删
// 1.5f/重打 1.5f 且无停顿。f110 后全静止,160f 总长 → 收尾真静止 50f。
// 等宽感:逐字符固定宽 span,无 letter-spacing 动画。全部帧确定。
import React from 'react';
import { useCurrentFrame } from 'remotion';
import { G } from '../../_fixtures/Fixtures';

const TEXT1 = 'just a dashboard'; // 16 chars
const KEEP = 5; // "just " 保留
const DEL = TEXT1.length - KEEP; // 11 chars 待删
const TEXT2 = 'your command center'; // 19 chars

const T1 = 2; // 第一遍打字起点,2f/字符
const PAUSE_START = T1 + (TEXT1.length - 1) * 2; // f32 打完
const DS = PAUSE_START + 16; // f48 开删,1.5f/字符
const RS = 68; // 重打起点,1.5f/字符(删完 f64.5 后小顿 3.5f)
const TYPE2_END = RS + (TEXT2.length - 1) * 1.5; // f95 打完
const CURSOR_OFF = 115; // 两个 10f 闪烁周期后熄灭(最后一次视觉变化 f110)

const CHAR_W = 58; // fontSize 96 的等宽感字符宽

// 光标可见性:打字/删除时常亮;停顿段 8f 周期闪两下;打完后 10f 周期闪两下;之后永灭
const cursorOn = (f: number): boolean => {
  if (f >= CURSOR_OFF) return false;
  if (f >= TYPE2_END) {
    // f95 起:on[95,100) off[100,105) on[105,110) off[110,115)
    return Math.floor((f - TYPE2_END) / 5) % 2 === 0;
  }
  if (f >= DS) return true; // 删除 + 重打:常亮(果断)
  if (f >= PAUSE_START) {
    // 犹豫段 f32–48:on[32,36) off[36,40) on[40,44) off[44,48)
    return Math.floor((f - PAUSE_START) / 4) % 2 === 0;
  }
  return true; // 第一遍打字:常亮
};

export const TypewriterErrorRetype: React.FC = () => {
  const f = useCurrentFrame();

  // 第一遍已打出字符数
  const n1 = f < T1 ? 0 : Math.min(TEXT1.length, Math.floor((f - T1) / 2) + 1);
  // 已删除字符数(从尾部删)
  const removed = f < DS ? 0 : Math.min(DEL, Math.floor((f - DS) / 1.5) + 1);
  // 第二遍已打出字符数
  const n2 = f < RS ? 0 : Math.min(TEXT2.length, Math.floor((f - RS) / 1.5) + 1);

  const shown =
    TEXT1.slice(0, Math.max(KEEP, n1 - removed)).slice(0, n1) +
    TEXT2.slice(0, n2);

  const chars = shown.split('');

  return (
    <div
      style={{
        width: 1920,
        height: 1080,
        background: G.bg,
        position: 'relative',
      }}
    >
      {/* 左缘锚定:最终句 24 字符×58=1392px,左起 264 恰好整体居中;
          打字过程不横移(真打字机感) */}
      <div
        style={{
          position: 'absolute',
          left: 264,
          top: 490,
          display: 'flex',
          alignItems: 'center',
        }}
      >
        {chars.map((c, i) => (
          <span
            key={i}
            style={{
              display: 'inline-block',
              width: CHAR_W,
              textAlign: 'center',
              fontFamily: '"Courier New", Courier, monospace',
              fontSize: 96,
              fontWeight: 700,
              color: G.ink,
              lineHeight: 1.1,
            }}
          >
            {c === ' ' ? ' ' : c}
          </span>
        ))}
        {/* 光标:竖线,条件挂载而非 opacity 0 */}
        {cursorOn(f) && (
          <span
            style={{
              display: 'inline-block',
              width: 7,
              height: 100,
              marginLeft: 4,
              background: G.ink,
            }}
          />
        )}
      </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/typewriter-moves/TerminalTypewriter.tsx, demos/typography/typewriter-moves/TypewriterErrorRetype.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