Motion · Tipografia

Typewriter: two moods

Two typewriter moves — terminal-typewriter, typing out a command that detonates a scene change, and error-retype, a delete-and-retype three-act "change of mind."

1920×108016:930 fps4.89s · 147 quadrosremotionLicença: Apache-2.0#shotcraft#remotion#typography#opening

Fonte: video-shotcraft (Vincent Wei) ↗ · Apache-2.0 · Composição Remotion (React/TSX) — vídeo de prévia, sem player no navegador

Variantes

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

When to use

Developer-product openers (A), slogan/selling-point text cards (B); a text entrance with its own built-in sense of time

Intention

Typing is the only text entrance with its own built-in sense of time — letters arriving one by one is itself a rhythm, no extra motion needed. A treats typing as a fuse: a dark terminal window types out a command character by character, a block cursor blinking in a square wave, and on the Enter frame the camera sharply pushes in on the command line and hard-cuts to the product UI — the command line is the scene's detonator, a developer-product standard; B treats typing as a monologue: a mediocre phrase ("just a dashboard") is typed → hesitates → backspaces it away → types the selling point faster ("your command center") — a three-act hesitate-negate-declare drama, all the story in the three different speeds. The difference from scramble-decode: garbled decode is a machine guessing, typing is a person speaking.

Duration & energy

  • Duration: A: ~5s / B: ~5.5s
  • Energy: A medium-high / B medium-low

Shot recipe (original Chinese — timing, easing, parameter table, known pitfalls)

意图

打字是唯一自带时间性的文字入场——字符一个个到来本身就是节奏, 不需要额外动效。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)

Reference implementation (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

The component imports shared fixtures from demos/_fixtures and helpers from assets/lib in the repo; SKILL.md there documents the render workflow.

Tudo nesta página também é legível por máquina. Aponte seu agente para o JSON ou adicione o servidor MCP do gimgs e peça para buscar templates de motion.

  1. Baixe o JSON — contém a receita do plano (timing, easing, tabela de parâmetros, armadilhas) e o código TSX completo.
  2. Clone o repositório video-shotcraft; o componente importa fixtures de demos/_fixtures e helpers de assets/lib.
  3. Registre o componente como <Composition> em um projeto Remotion e renderize com `npx remotion render`, ou reimplemente a receita no seu próprio engine.
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 "Typewriter: two moods" (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.0Abrir template bruto ↗
// ===== 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
—

Mais nesta categoria