{
  "slug": "typewriter-moves",
  "title": "Typewriter: two moods",
  "tagline": "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.\"",
  "description": "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 min",
  "category": "typography",
  "tags": [
    "shotcraft",
    "remotion",
    "typography",
    "opening"
  ],
  "tier": "free",
  "license": "Apache-2.0",
  "spec": {
    "width": 1920,
    "height": 1080,
    "fps": 30,
    "duration": 4.89,
    "aspect": "16:9"
  },
  "kind": "remotion",
  "params_schema": {
    "type": "object",
    "properties": {}
  },
  "default_params": {},
  "preview": {
    "poster": "https://gimgs.net/motion/media/typewriter-moves/poster-cc1cc40352f9.jpg",
    "video": "https://gimgs.net/motion/media/typewriter-moves/video-2324d56f005b.mp4"
  },
  "variants": [
    {
      "key": "terminal-typewriter",
      "label": "terminal-typewriter",
      "description": "2f/character typing out the command, the cursor blinking on a square wave (f%12<6); on the Enter frame the whole scene pushes in sharply over 6f, scale 1→3.2 (origin locked to the command line's center) with 10px blur in the last 2f, hard-cutting to the dashboard settling from 1.06 to 1",
      "use": "Developer-tool openers; CLI-forward products",
      "video": "https://gimgs.net/motion/media/typewriter-moves/video-2324d56f005b.mp4",
      "poster": "https://gimgs.net/motion/media/typewriter-moves/poster-cc1cc40352f9.jpg"
    },
    {
      "key": "error-retype",
      "label": "error-retype",
      "description": "Type at 2f/character → pause 16f with the cursor blinking twice → backspace at 1.5f/character → retype at 1.5f/character with no hesitation; the cursor stays solid while typing/deleting and only blinks during the pause",
      "use": "A \"change of mind\" beat on a slogan card; negation-style copy",
      "video": "https://gimgs.net/motion/media/typewriter-moves/video-f7d1b4984c0c.mp4",
      "poster": "https://gimgs.net/motion/media/typewriter-moves/poster-error-retype-80ee2f74c5fa.jpg"
    }
  ],
  "credits": {
    "author": "video-shotcraft (Vincent Wei)",
    "url": "https://github.com/Vincentwei1021/video-shotcraft"
  },
  "page_url": "https://gimgs.net/motion/typewriter-moves.html",
  "json_url": "https://gimgs.net/motion/typewriter-moves.json",
  "template_url": "https://gimgs.net/motion/typewriter-moves/template.html",
  "play_url": null,
  "markdown_url": "https://gimgs.net/motion/typewriter-moves.md",
  "updated_at": "2026-09-24T07:40:32.059Z",
  "prompt": "## When to use\n\nDeveloper-product openers (A), slogan/selling-point text cards (B); a text entrance with its own built-in sense of time\n\n## Intention\n\nTyping 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.\n\n## Duration & energy\n\n- Duration: A: ~5s / B: ~5.5s\n- Energy: A medium-high / B medium-low\n\n## Shot recipe (original Chinese — timing, easing, parameter table, known pitfalls)\n\n## 意图\n打字是唯一自带时间性的文字入场——字符一个个到来本身就是节奏，\n不需要额外动效。A 把打字当**引信**：暗色终端窗逐字敲出命令、\n方块光标方波闪，回车帧镜头急推进命令行硬切产品界面，命令行是\n场景的导火索，开发者产品标配；B 把打字当**独白**：逐字打出平庸词\n（\"just a dashboard\"）→ 停顿犹豫 → 退格删掉 → 更快打出卖点词\n（\"your command center\"），犹豫-否定-宣言的三幕剧，戏全在三档\n速度差里。与 scramble-decode 的区别：乱码解码是机器在猜，\n打字是人在说。\n\n## 两式选型\n| 式 | 做法 | 适用 |\n|----|------|------|\n| A terminal-typewriter | 2f/字符敲出命令，光标 f%12<6 方波闪；回车帧整场景 6f 急推 scale 1→3.2（origin 锁命令行中心）+ 末 2f blur 10px，硬切 dashboard 1.06→1 回稳 | 开发者工具开场；CLI 卖点产品 |\n| B error-retype | 打 2f/字符 → 停顿 16f 光标闪两下 → 退 1.5f/字符 → 重打 1.5f/字符无犹豫；光标打删常亮、停顿才闪 | slogan 字卡的\"改口\"戏；否定式文案 |\n\n## 参数表\n| 参数 | 典型值 | 调节手感 |\n|------|--------|----------|\n| 字符节奏 | substring(0, floor((f-起点)/步长))，帧确定无插值 | 任何缓动打字都读作加载动画 |\n| A 光标 | 方块，f%12<6 方波 | 淡入淡出的光标不是终端是网页 |\n| A 急推 | f58–64 scale 1→3.2 Easing.in(cubic)，origin 钉命令行文字中心 | 幅度小于 3x 读作抖了一下，不是\"冲进去\" |\n| A 硬切 | f64 条件挂载切换（终端整棵卸载，blur 随之摘罩），落定回稳 4f | 交叉溶解毁掉引信-爆炸因果 |\n| B 三档速度 | 打 2f/字符、删 1.5f、重打 1.5f 且零犹豫 | 三档差必须可感——等速改口读作 bug 不是戏，命门 |\n| B 光标三态 | 打删常亮＝果断；停顿 4f 半周期闪两下＝犹豫；完稿闪两下后条件摘除 | 光标就是演员的脸，全程等速闪就没有\"犹豫\" |\n| B 排版 | 逐字符固定宽 span（58px 等宽），左缘锚定 | 变宽字体/整串测宽会逐帧重排抖动 |\n| 收尾 | A 静止 ≥77f / B 静止 ≥50f | 光标残留 0.05 opacity 都毁真静止 |\n\n## 已知坑\n- demo 在灰阶/占位素材上调校通过——参数是调校起点非实战定稿，\n  首次实战须以真实素材回验\n- 声音强依赖：键击声逐字符对齐、B 退格音调更低（sound-design\n  §4.5）——无声打字机只剩字幕机\n- A 回车急推是与 crash-zoom 的交棒衔接：急推末帧动势最猛处硬切，\n  切后新场景不得再推（一棒只交一次）\n- B 全片 ≤1 次——改口是一次性的戏剧转折，改两次口是口吃\n- 等宽字体或逐字符 span 二选一必须占一个，否则字符到来时\n  整行重排，打字变成抽搐\n\n## 参考实现\ndemos/typography/typewriter-moves/\n（TerminalTypewriter.tsx / TypewriterErrorRetype.tsx）\n\n## Reference implementation (Remotion)\n\n- demos/typography/typewriter-moves/TerminalTypewriter.tsx — https://raw.githubusercontent.com/Vincentwei1021/video-shotcraft/main/demos/typography/typewriter-moves/TerminalTypewriter.tsx\n- demos/typography/typewriter-moves/TypewriterErrorRetype.tsx — https://raw.githubusercontent.com/Vincentwei1021/video-shotcraft/main/demos/typography/typewriter-moves/TypewriterErrorRetype.tsx\n\nThe component imports shared fixtures from `demos/_fixtures` and helpers from `assets/lib` in the repo; SKILL.md there documents the render workflow.\n",
  "flow": [
    {
      "step": "Read the recipe",
      "note": "Prompt = when to use, intention, parameter table with typical values and how each one feels, known pitfalls. Treat values as calibrated starting points."
    },
    {
      "step": "Get the code",
      "tool": "git",
      "cmd": "git clone https://github.com/Vincentwei1021/video-shotcraft && cd video-shotcraft && npm install"
    },
    {
      "step": "Register + render",
      "tool": "remotion",
      "cmd": "npx remotion render <CompositionId> out.mp4   # component: demos/typography/typewriter-moves/TerminalTypewriter.tsx, demos/typography/typewriter-moves/TypewriterErrorRetype.tsx"
    },
    {
      "step": "Or re-implement",
      "note": "Port the timing/easing from the recipe to HTML + WAAPI (gimgs motion contract) or any engine; keep the same segment windows and easing."
    }
  ],
  "files": [
    {
      "path": "demos/typography/typewriter-moves/TerminalTypewriter.tsx",
      "url": "https://raw.githubusercontent.com/Vincentwei1021/video-shotcraft/main/demos/typography/typewriter-moves/TerminalTypewriter.tsx"
    },
    {
      "path": "demos/typography/typewriter-moves/TypewriterErrorRetype.tsx",
      "url": "https://raw.githubusercontent.com/Vincentwei1021/video-shotcraft/main/demos/typography/typewriter-moves/TypewriterErrorRetype.tsx"
    }
  ],
  "contract": "React/Remotion composition (TSX) from the open-source video-shotcraft library (Apache-2.0). It is not a browser-playable template: clone the repo, register the component in a Remotion project and render with `npx remotion render`. The recipe (prompt) describes timing, easing and parameters so you can also re-implement the shot in any engine.",
  "render": {
    "engine": "remotion",
    "frames": 147,
    "repo": "https://github.com/Vincentwei1021/video-shotcraft",
    "files": [
      "https://raw.githubusercontent.com/Vincentwei1021/video-shotcraft/main/demos/typography/typewriter-moves/TerminalTypewriter.tsx",
      "https://raw.githubusercontent.com/Vincentwei1021/video-shotcraft/main/demos/typography/typewriter-moves/TypewriterErrorRetype.tsx"
    ],
    "note": "Clone https://github.com/Vincentwei1021/video-shotcraft (npm install). The component lives under demos/{category}/{name}/ and imports shared fixtures from demos/_fixtures and helpers from assets/lib. Copy it (with those imports) into a Remotion project, register a <Composition> with the template's width/height/fps/duration in Root.tsx, then `npx remotion render <CompositionId> out.mp4`. SKILL.md in the repo documents the full Claude Code / Codex workflow. The prompt below is the shot recipe (timing, easing, parameter table, pitfalls) — enough to re-implement the shot in HTML/WAAPI or another engine if you prefer."
  },
  "source": "// ===== demos/typography/typewriter-moves/TerminalTypewriter.tsx =====\n// terminal-typewriter —— 终端打字机触发\n// 深色终端窗居中，\"$ acme deploy --prod\" 逐字符敲出（2f/字符，帧确定\n// substring），方块光标 12f 方波闪 → 敲完停 12f → 回车帧：整场景 6f\n// Easing.in(cubic) 急推 scale 1→3.2 向命令行推入（末 2f 加 blur）硬切到\n// FakeDashboard A 全屏，1.06→1 回稳 4f 落定。收尾真静止 ≥40f。\nimport React from 'react';\nimport { useCurrentFrame, interpolate, Easing } from 'remotion';\nimport { G, FakeDashboard } from '../../_fixtures/Fixtures';\n\nconst CMD = 'acme deploy --prod';\n\n// 时间轴（30fps，共 145f）\nconst T = {\n  typeStart: 10,                       // 开始敲字\n  typeEnd: 10 + CMD.length * 2,        // 46：18 字符 × 2f\n  enter: 58,                           // 敲完停 12f 后回车\n  pushEnd: 64,                         // 6f 急推结束，硬切帧\n  settleEnd: 68,                       // dashboard 1.06→1 回稳 4f\n  total: 145,                          // f68 起真静止 77f\n};\n\n// 终端窗几何\nconst TW = 1100;\nconst TH = 620;\nconst TL = (1920 - TW) / 2; // 410\nconst TT = (1080 - TH) / 2; // 230\nconst TITLEBAR = 52;\nconst PAD = 34;\n// 命令行基线（推入焦点）：标题栏下第二行文字中心\nconst FOCUS_X = 960;\nconst FOCUS_Y = TT + TITLEBAR + PAD + 92; // ≈ 408\n\nconst TerminalWindow: React.FC<{ chars: number; cursorOn: boolean }> = ({ chars, cursorOn }) => (\n  <div style={{\n    width: TW, height: TH, background: '#1e1e1c', borderRadius: 14,\n    boxShadow: '0 24px 64px rgba(0,0,0,0.35)',\n    overflow: 'hidden', boxSizing: 'border-box',\n  }}>\n    {/* 标题栏：三圆点窗控（灰阶） */}\n    <div style={{\n      height: TITLEBAR, background: '#2a2a28', borderBottom: '1px solid #3a3a38',\n      display: 'flex', alignItems: 'center', gap: 12, padding: '0 22px',\n      boxSizing: 'border-box',\n    }}>\n      {['#6a6a68', '#8f8f8d', '#b5b5b3'].map((c, i) => (\n        <div key={i} style={{ width: 16, height: 16, borderRadius: 8, background: c }} />\n      ))}\n      <div style={{ margin: '0 auto', height: 10, width: 200, background: '#4a4a48', borderRadius: 5 }} />\n      <div style={{ width: 72 }} />\n    </div>\n    {/* 内容区 */}\n    <div style={{\n      padding: PAD, fontFamily: 'Menlo, Consolas, monospace', fontSize: 40,\n      color: '#d8d8d6', lineHeight: 1.5,\n    }}>\n      {/* 一行历史输出做上下文 */}\n      <div style={{ color: '#7a7a78', fontSize: 32, marginBottom: 18 }}>~/acme-app (main)</div>\n      <div style={{ display: 'flex', alignItems: 'center', whiteSpace: 'pre' }}>\n        <span style={{ color: '#9f9f9d' }}>{'$ '}</span>\n        <span>{CMD.substring(0, chars)}</span>\n        <span style={{\n          display: 'inline-block', width: 24, height: 48, marginLeft: 4,\n          background: '#d8d8d6', opacity: cursorOn ? 1 : 0,\n        }} />\n      </div>\n    </div>\n  </div>\n);\n\nexport const TerminalTypewriter: React.FC = () => {\n  const frame = useCurrentFrame();\n\n  // 帧确定打字：2f/字符\n  const chars = Math.min(CMD.length, Math.max(0, Math.floor((frame - T.typeStart) / 2)));\n\n  // 方块光标 12f 周期方波闪（全程）\n  const cursorOn = frame % 12 < 6;\n\n  // 回车急推：整场景 scale 1→3.2，6f Easing.in(cubic)，向命令行推入\n  const pushScale = interpolate(frame, [T.enter, T.pushEnd], [1, 3.2], {\n    extrapolateLeft: 'clamp', extrapolateRight: 'clamp',\n    easing: Easing.in(Easing.cubic),\n  });\n  // 末 2f 运动模糊（f62–f64），硬切后摘罩=条件挂载\n  const pushBlur = interpolate(frame, [T.pushEnd - 2, T.pushEnd], [0, 10], {\n    extrapolateLeft: 'clamp', extrapolateRight: 'clamp',\n  });\n\n  // 硬切：f64 起终端场景整体卸载，dashboard 挂载\n  const cut = frame >= T.pushEnd;\n\n  // dashboard 落定：1.06→1 回稳 4f\n  const dashScale = interpolate(frame, [T.pushEnd, T.settleEnd], [1.06, 1], {\n    extrapolateLeft: 'clamp', extrapolateRight: 'clamp',\n    easing: Easing.out(Easing.cubic),\n  });\n\n  return (\n    <div style={{ width: 1920, height: 1080, background: G.bg, position: 'relative', overflow: 'hidden' }}>\n      {!cut ? (\n        <div style={{\n          width: 1920, height: 1080,\n          transform: `scale(${pushScale})`,\n          transformOrigin: `${FOCUS_X}px ${FOCUS_Y}px`,\n          ...(pushBlur > 0 ? { filter: `blur(${pushBlur}px)` } : {}),\n        }}>\n          <div style={{ position: 'absolute', left: TL, top: TT }}>\n            <TerminalWindow chars={chars} cursorOn={cursorOn} />\n          </div>\n        </div>\n      ) : (\n        <div style={{\n          width: 1920, height: 1080,\n          transform: `scale(${dashScale})`,\n          transformOrigin: '960px 540px',\n        }}>\n          <FakeDashboard variant=\"A\" />\n        </div>\n      )}\n    </div>\n  );\n};\n\n\n// ===== demos/typography/typewriter-moves/TypewriterErrorRetype.tsx =====\n// typewriter-error-retype｜打字机误删重打\n// 浅底居中大字：f2 起 2f/字符打出 \"just a dashboard\"(16 字符) → f32–48 停顿\n// 16f(光标闪两下=犹豫) → f48 起 1.5f/字符逐字退格删掉 \"a dashboard\"(11 字符，\n// 字符直接消失) → f68 起 1.5f/字符果断打出 \"your command center\"(19 字符，\n// f95 打完) → 光标闪两个周期(10f/周期)后 f110 永久熄灭。节奏三档：打 2f/删\n// 1.5f/重打 1.5f 且无停顿。f110 后全静止，160f 总长 → 收尾真静止 50f。\n// 等宽感：逐字符固定宽 span，无 letter-spacing 动画。全部帧确定。\nimport React from 'react';\nimport { useCurrentFrame } from 'remotion';\nimport { G } from '../../_fixtures/Fixtures';\n\nconst TEXT1 = 'just a dashboard'; // 16 chars\nconst KEEP = 5; // \"just \" 保留\nconst DEL = TEXT1.length - KEEP; // 11 chars 待删\nconst TEXT2 = 'your command center'; // 19 chars\n\nconst T1 = 2; // 第一遍打字起点，2f/字符\nconst PAUSE_START = T1 + (TEXT1.length - 1) * 2; // f32 打完\nconst DS = PAUSE_START + 16; // f48 开删，1.5f/字符\nconst RS = 68; // 重打起点，1.5f/字符（删完 f64.5 后小顿 3.5f）\nconst TYPE2_END = RS + (TEXT2.length - 1) * 1.5; // f95 打完\nconst CURSOR_OFF = 115; // 两个 10f 闪烁周期后熄灭（最后一次视觉变化 f110）\n\nconst CHAR_W = 58; // fontSize 96 的等宽感字符宽\n\n// 光标可见性：打字/删除时常亮；停顿段 8f 周期闪两下；打完后 10f 周期闪两下；之后永灭\nconst cursorOn = (f: number): boolean => {\n  if (f >= CURSOR_OFF) return false;\n  if (f >= TYPE2_END) {\n    // f95 起：on[95,100) off[100,105) on[105,110) off[110,115)\n    return Math.floor((f - TYPE2_END) / 5) % 2 === 0;\n  }\n  if (f >= DS) return true; // 删除 + 重打：常亮（果断）\n  if (f >= PAUSE_START) {\n    // 犹豫段 f32–48：on[32,36) off[36,40) on[40,44) off[44,48)\n    return Math.floor((f - PAUSE_START) / 4) % 2 === 0;\n  }\n  return true; // 第一遍打字：常亮\n};\n\nexport const TypewriterErrorRetype: React.FC = () => {\n  const f = useCurrentFrame();\n\n  // 第一遍已打出字符数\n  const n1 = f < T1 ? 0 : Math.min(TEXT1.length, Math.floor((f - T1) / 2) + 1);\n  // 已删除字符数（从尾部删）\n  const removed = f < DS ? 0 : Math.min(DEL, Math.floor((f - DS) / 1.5) + 1);\n  // 第二遍已打出字符数\n  const n2 = f < RS ? 0 : Math.min(TEXT2.length, Math.floor((f - RS) / 1.5) + 1);\n\n  const shown =\n    TEXT1.slice(0, Math.max(KEEP, n1 - removed)).slice(0, n1) +\n    TEXT2.slice(0, n2);\n\n  const chars = shown.split('');\n\n  return (\n    <div\n      style={{\n        width: 1920,\n        height: 1080,\n        background: G.bg,\n        position: 'relative',\n      }}\n    >\n      {/* 左缘锚定：最终句 24 字符×58=1392px，左起 264 恰好整体居中；\n          打字过程不横移（真打字机感） */}\n      <div\n        style={{\n          position: 'absolute',\n          left: 264,\n          top: 490,\n          display: 'flex',\n          alignItems: 'center',\n        }}\n      >\n        {chars.map((c, i) => (\n          <span\n            key={i}\n            style={{\n              display: 'inline-block',\n              width: CHAR_W,\n              textAlign: 'center',\n              fontFamily: '\"Courier New\", Courier, monospace',\n              fontSize: 96,\n              fontWeight: 700,\n              color: G.ink,\n              lineHeight: 1.1,\n            }}\n          >\n            {c === ' ' ? ' ' : c}\n          </span>\n        ))}\n        {/* 光标：竖线，条件挂载而非 opacity 0 */}\n        {cursorOn(f) && (\n          <span\n            style={{\n              display: 'inline-block',\n              width: 7,\n              height: 100,\n              marginLeft: 4,\n              background: G.ink,\n            }}\n          />\n        )}\n      </div>\n    </div>\n  );\n};\n\n\n"
}