{
  "slug": "split-flap-title",
  "title": "Split-flap board title",
  "tagline": "Airport split-flap title — each character's two mechanical flap halves flip through 2 garbled frames and clatter to a stop on the target letter, cascading left to right.",
  "description": "Airport split-flap title — each character's two mechanical flap halves flip through 2 garbled frames and clatter to a stop on the target letter, cascading lef",
  "category": "typography",
  "tags": [
    "shotcraft",
    "remotion",
    "typography",
    "opening"
  ],
  "tier": "free",
  "license": "Apache-2.0",
  "spec": {
    "width": 1920,
    "height": 1080,
    "fps": 30,
    "duration": 4.72,
    "aspect": "16:9"
  },
  "kind": "remotion",
  "params_schema": {
    "type": "object",
    "properties": {}
  },
  "default_params": {},
  "preview": {
    "poster": "https://gimgs.net/motion/media/split-flap-title/poster-c5642f277783.jpg",
    "video": "https://gimgs.net/motion/media/split-flap-title/video-e51f32ee4ed0.mp4"
  },
  "variants": [
    {
      "key": "split-flap-title",
      "label": "split-flap-title",
      "description": "Airport split-flap title — each character's two mechanical flap halves flip through 2 garbled frames and clatter to a stop on the target letter, cascading left to right",
      "use": "",
      "video": "https://gimgs.net/motion/media/split-flap-title/video-e51f32ee4ed0.mp4",
      "poster": "https://gimgs.net/motion/media/split-flap-title/poster-c5642f277783.jpg"
    }
  ],
  "credits": {
    "author": "video-shotcraft (Vincent Wei)",
    "url": "https://github.com/Vincentwei1021/video-shotcraft"
  },
  "page_url": "https://gimgs.net/motion/split-flap-title.html",
  "json_url": "https://gimgs.net/motion/split-flap-title.json",
  "template_url": "https://gimgs.net/motion/split-flap-title/template.html",
  "play_url": null,
  "markdown_url": "https://gimgs.net/motion/split-flap-title.md",
  "updated_at": "2026-09-24T07:40:24.400Z",
  "prompt": "## When to use\n\nOpening/chapter big titles; countdown, release-date, or data-announcement copy; any beat that needs a \"mechanical announcement\" feel\n\n## Intention\n\nThe third of the three text moves: embossing (paper-title-card) is ink pressed into paper, a \"whisper\"; the typewriter (document-typewriter-reveal) is a document being written by hand, a \"narration\"; the split-flap is a station departure board, a \"broadcast\" with a mechanical-announcement tone. Choose split-flap for countdown/departure feels (version releases, deadlines), data-announcement feels (metric reveals), or when the title needs a layer of retro mechanical texture. It naturally reads as a monospaced dark-panel grid, which in the library's paper-and-ink aesthetic comes across as \"a mechanical display panel set on the page\" — the off-color block is itself the point of interest, so the background product footage dims and desaturates to give it room.\n\n## Duration & energy\n\n- Duration: ~4.7s (140f: ≥20f garbled hold to establish + cascading flip + ≥15f settled hold)\n- Energy: Medium (sustained mechanical motion, not an instant impact)\n\n## Shot recipe (original Chinese — timing, easing, parameter table, known pitfalls)\n\n## 意图\n文字三式的第三式：压印（paper-title-card）是纸上落墨的\"低语\"，打字机\n（document-typewriter-reveal）是文档被人写出来的\"叙述\"，翻牌是车站大屏的\n\"播报\"——带机械宣告腔。选翻牌的语境：倒计时/发车感（版本发布、截止日）、\n数据播报感（指标揭晓）、或想给标题一层复古机械质感。它天然是等宽深底\n格子，在库内纸墨审美里读作\"纸面上摆了一块机械显示屏\"——异色块本身\n就是看点，背景产品画面压暗降饱和给它让位。\n\n## 动效核心\n- 每字符一个深底翻牌格（上下两半 overflow hidden + 中缝铰链线），字符\n  序列固定为 2 个乱码 → 1 个乱码 → 目标字（首态乱码，建立段可见）\n- 单次翻牌 5f 两段式：前半旧字上半叶 rotateX 0→-90 掉落（Easing.in\n  重力感 + brightness 变暗到 0.55），后半新字下半叶 90→0 拍下（回亮）\n- 每格翻 3 次；字符间 stagger 4f，左→右级联成波\n- 停定咔哒：整格 6px 下沉回弹（下沉→过冲 -1.5px→归零）\n- 节拍：开头 ≥20f 整排乱码静止建立，结尾全部停定后 ≥15f 静止收束\n- 乱码用 seed 正弦哈希（sin(i·127.3)·43758 取小数），禁 Math.random\n\n## 参数表\n| 参数 | 典型值 | 调节手感 |\n|------|--------|----------|\n| 单次翻牌 | 5f，p 走 Easing.in(quad)；<0.5 旧字上半叶 0→-90 掉落，≥0.5 新字下半叶 90→0 拍下 | Easing.in 是重力感来源——越掉越快才像机械叶片；匀速读作电子屏假翻 |\n| 翻牌次数 | 每格 3 次（2 乱码中间态 + 1 落定） | 少于 2 次读不出\"翻找\"过程；多于 4 次拖节奏且乱码抢戏 |\n| 级联 | stagger 4f 左→右；10 字符约 36f 波峰扫完 | 4f 是波浪感甜点——太密整排齐翻没有波，太疏尾部字符等太久 |\n| 停定咔哒 | 落定后 local 15→22f：translateY 0→6→-1.5→0，Easing.out(quad) | 6px 是可感下限（4px 无感判例）；这一下就是\"咔哒\"的画面体，删了只剩滑动 |\n| 明暗 | 掉落叶 brightness 1→0.55，拍下叶 0.55→1 | 明暗差是叶片\"转过铰链\"的立体感来源，比 rotateX 本身更被肉眼读到 |\n| 翻牌格 | 118×156，perspective 420，圆角 10，中缝铰链线 4px #141412 | 铰链线压住翻转轴穿帮；perspective 太大叶片透视变形夸张 |\n| 配色字体 | 格底 #262624 / 字 #f4f4f2，Helvetica 800 100px；空格占 52px、格间 gap 12 | 深底浅字是翻牌屏的身份证；等宽格子布局，别用比例字体挤格 |\n| 节拍 | 建立 0–21f 静止 → 22f 起级联 → 全停后静止 ≥15f | 建立段让观众先认出\"这是块翻牌屏\"，直接开翻读作故障闪烁 |\n\n## 已知坑\n- demo 在灰阶/占位素材上调校通过——参数是调校起点非实战定稿，\n  首次实战须以真实素材回验\n- 声音强依赖：每格停定的机械拍打声是效果的一半，实战必须配——级联\n  停定天然是一串节奏点，`assets/audio/sfx/ui/switch-click-quick.mp3` 或\n  `sfx/mech/gear-lock-metallic.mp3` 起步（更贴机械翻牌），也可用\n  `sfx/text/typewriter-hit-single.mp3`；配法纪律见 sound-design.md\n  （同类元素同音，拟音与动作等长，连发交替双样本防机枪感）\n- 下沉回弹 6px 是可感下限：肉眼看不出就照可感性判例加大幅度，别调\n  曲线不调幅度\n- 一支片翻牌字 ≤1 次：机械宣告腔太抢，重复即贬值（P4 手法去重精神）；\n  与压印/打字机同片混用时留给最重的那句标题\n\n## 参考实现\ndemos/typography/split-flap-title/\n（SplitFlapFlip.tsx）\n\n## Reference implementation (Remotion)\n\n- demos/typography/split-flap-title/SplitFlapFlip.tsx — https://raw.githubusercontent.com/Vincentwei1021/video-shotcraft/main/demos/typography/split-flap-title/SplitFlapFlip.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/split-flap-title/SplitFlapFlip.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/split-flap-title/SplitFlapFlip.tsx",
      "url": "https://raw.githubusercontent.com/Vincentwei1021/video-shotcraft/main/demos/typography/split-flap-title/SplitFlapFlip.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": 142,
    "repo": "https://github.com/Vincentwei1021/video-shotcraft",
    "files": [
      "https://raw.githubusercontent.com/Vincentwei1021/video-shotcraft/main/demos/typography/split-flap-title/SplitFlapFlip.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/split-flap-title/SplitFlapFlip.tsx =====\nimport React from 'react';\nimport { AbsoluteFill, Easing, interpolate, useCurrentFrame } from 'remotion';\nimport { FakeDashboard, G } from '../../_fixtures/Fixtures';\n\n// split-flap-flip：机场翻牌字。每字符一个深底翻牌格（上下两半），\n// 逐格翻过 3 个乱码中间态后咔哒停在目标字，左→右 4f 级联成波。\n// 节拍：0–21 建立（整排乱码静止）→ 22 起级联翻牌 → 78 全部停定 → 静止到 140。\n\nconst TEXT = 'SHIP FASTER';\nconst CHARSET = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789#$%&';\nconst START = 22; // 级联起始帧\nconst STAGGER = 4; // 字符间级联延迟\nconst FLIP = 5; // 单次翻牌时长\nconst NFLIP = 3; // 每字符翻 3 次（2 个乱码中间态 + 1 次落到目标字）\nconst CELL_W = 118;\nconst CELL_H = 156;\n\n// seed 正弦哈希（禁 Math.random）\nconst rnd = (a: number) => {\n  const x = Math.sin(a * 127.3) * 43758.5453;\n  return x - Math.floor(x);\n};\nconst garble = (i: number, k: number) =>\n  CHARSET[Math.floor(rnd(i * 7.13 + k * 3.71 + 1) * CHARSET.length)];\n\nconst FLAP_BG = '#262624';\nconst FLAP_INK = '#f4f4f2';\n\n// 半格：上/下半各自 overflow hidden，内部整字定位错半格露出对应一半\nconst Half: React.FC<{ ch: string; part: 'top' | 'bottom' }> = ({ ch, part }) => (\n  <div\n    style={{\n      position: 'absolute',\n      left: 0,\n      top: part === 'top' ? 0 : CELL_H / 2,\n      width: CELL_W,\n      height: CELL_H / 2,\n      overflow: 'hidden',\n      background: FLAP_BG,\n      borderRadius: part === 'top' ? '10px 10px 0 0' : '0 0 10px 10px',\n    }}\n  >\n    <div\n      style={{\n        position: 'absolute',\n        left: 0,\n        top: part === 'top' ? 0 : -CELL_H / 2,\n        width: CELL_W,\n        height: CELL_H,\n        display: 'flex',\n        alignItems: 'center',\n        justifyContent: 'center',\n        fontFamily: 'Helvetica, Arial, sans-serif',\n        fontWeight: 800,\n        fontSize: 100,\n        color: FLAP_INK,\n      }}\n    >\n      {ch}\n    </div>\n  </div>\n);\n\nconst FlapCell: React.FC<{ target: string; i: number; frame: number }> = ({\n  target,\n  i,\n  frame,\n}) => {\n  // 该格的字符序列：2 个乱码 → 1 个乱码 → 目标字（首态也是乱码，建立段可见）\n  const seq = [garble(i, 0), garble(i, 1), garble(i, 2), target];\n  const local = frame - (START + i * STAGGER);\n  const done = local >= NFLIP * FLIP;\n\n  // 停定咔哒：整格下沉回弹（1px 肉眼无感，放大到 6px 才有\"咔哒\"）\n  const clickY = done\n    ? interpolate(local, [15, 17, 19, 22], [0, 6, -1.5, 0], {\n        extrapolateLeft: 'clamp',\n        extrapolateRight: 'clamp',\n        easing: Easing.out(Easing.quad),\n      })\n    : 0;\n\n  let topCh = seq[0];\n  let bottomCh = seq[0];\n  let flap: React.ReactNode = null;\n\n  if (done) {\n    topCh = target;\n    bottomCh = target;\n  } else if (local > 0) {\n    const k = Math.min(NFLIP - 1, Math.floor(local / FLIP));\n    const from = seq[k];\n    const to = seq[k + 1];\n    const p = Easing.in(Easing.quad)((local - k * FLIP) / FLIP); // 重力感：越掉越快\n    topCh = to; // 上半静态：翻开后露出下一字符的上半\n    bottomCh = from; // 下半静态：保持旧字符直到活动叶盖下来\n    if (p < 0.5) {\n      // 前半程：旧字符上半叶 0→-90 掉下\n      const deg = p * 2 * 90;\n      flap = (\n        <div\n          style={{\n            position: 'absolute',\n            inset: 0,\n            transform: `rotateX(${-deg}deg)`,\n            transformOrigin: `center ${CELL_H / 2}px`,\n            backfaceVisibility: 'hidden',\n            filter: `brightness(${1 - p * 2 * 0.45})`,\n            zIndex: 2,\n          }}\n        >\n          <Half ch={from} part=\"top\" />\n        </div>\n      );\n    } else {\n      // 后半程：新字符下半叶 90→0 拍下盖住旧下半\n      const deg = 90 - (p - 0.5) * 2 * 90;\n      flap = (\n        <div\n          style={{\n            position: 'absolute',\n            inset: 0,\n            transform: `rotateX(${deg}deg)`,\n            transformOrigin: `center ${CELL_H / 2}px`,\n            backfaceVisibility: 'hidden',\n            filter: `brightness(${0.55 + (p - 0.5) * 2 * 0.45})`,\n            zIndex: 2,\n          }}\n        >\n          <Half ch={to} part=\"bottom\" />\n        </div>\n      );\n    }\n  }\n\n  return (\n    <div\n      style={{\n        position: 'relative',\n        width: CELL_W,\n        height: CELL_H,\n        transform: `translateY(${clickY}px)`,\n        perspective: 420,\n        borderRadius: 10,\n        boxShadow: '0 6px 18px rgba(0,0,0,0.3)',\n      }}\n    >\n      <Half ch={topCh} part=\"top\" />\n      <Half ch={bottomCh} part=\"bottom\" />\n      {flap}\n      {/* 中缝铰链线 */}\n      <div\n        style={{\n          position: 'absolute',\n          left: 0,\n          top: CELL_H / 2 - 2,\n          width: CELL_W,\n          height: 4,\n          background: '#141412',\n          zIndex: 3,\n        }}\n      />\n    </div>\n  );\n};\n\nexport const SplitFlapFlip: React.FC = () => {\n  const frame = useCurrentFrame();\n  let letterIdx = 0;\n  return (\n    <AbsoluteFill style={{ background: G.bg, overflow: 'hidden' }}>\n      {/* 背景假页面压暗，突出翻牌板 */}\n      <div style={{ position: 'absolute', inset: 0, opacity: 0.3, filter: 'saturate(0.8)' }}>\n        <FakeDashboard variant=\"A\" />\n      </div>\n      <AbsoluteFill style={{ alignItems: 'center', justifyContent: 'center' }}>\n        <div style={{ display: 'flex', gap: 12, alignItems: 'center' }}>\n          {TEXT.split('').map((ch, idx) => {\n            if (ch === ' ') {\n              return <div key={idx} style={{ width: 52 }} />;\n            }\n            const i = letterIdx++;\n            return <FlapCell key={idx} target={ch} i={i} frame={frame} />;\n          })}\n        </div>\n      </AbsoluteFill>\n    </AbsoluteFill>\n  );\n};\n\n\n"
}