Motion · Tipografia

Split-flap board title

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.

1920×108016:930 fps4.72s · 142 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

When to use

Opening/chapter big titles; countdown, release-date, or data-announcement copy; any beat that needs a "mechanical announcement" feel

Intention

The 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.

Duration & energy

  • Duration: ~4.7s (140f: ≥20f garbled hold to establish + cascading flip + ≥15f settled hold)
  • Energy: Medium (sustained mechanical motion, not an instant impact)

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

意图

文字三式的第三式:压印(paper-title-card)是纸上落墨的"低语",打字机 (document-typewriter-reveal)是文档被人写出来的"叙述",翻牌是车站大屏的 "播报"——带机械宣告腔。选翻牌的语境:倒计时/发车感(版本发布、截止日)、 数据播报感(指标揭晓)、或想给标题一层复古机械质感。它天然是等宽深底 格子,在库内纸墨审美里读作"纸面上摆了一块机械显示屏"——异色块本身 就是看点,背景产品画面压暗降饱和给它让位。

动效核心

  • 每字符一个深底翻牌格(上下两半 overflow hidden + 中缝铰链线),字符

序列固定为 2 个乱码 → 1 个乱码 → 目标字(首态乱码,建立段可见)

  • 单次翻牌 5f 两段式:前半旧字上半叶 rotateX 0→-90 掉落(Easing.in

重力感 + brightness 变暗到 0.55),后半新字下半叶 90→0 拍下(回亮)

  • 每格翻 3 次;字符间 stagger 4f,左→右级联成波
  • 停定咔哒:整格 6px 下沉回弹(下沉→过冲 -1.5px→归零)
  • 节拍:开头 ≥20f 整排乱码静止建立,结尾全部停定后 ≥15f 静止收束
  • 乱码用 seed 正弦哈希(sin(i·127.3)·43758 取小数),禁 Math.random

参数表

| 参数 | 典型值 | 调节手感 | |------|--------|----------| | 单次翻牌 | 5f,p 走 Easing.in(quad);<0.5 旧字上半叶 0→-90 掉落,≥0.5 新字下半叶 90→0 拍下 | Easing.in 是重力感来源——越掉越快才像机械叶片;匀速读作电子屏假翻 | | 翻牌次数 | 每格 3 次(2 乱码中间态 + 1 落定) | 少于 2 次读不出"翻找"过程;多于 4 次拖节奏且乱码抢戏 | | 级联 | stagger 4f 左→右;10 字符约 36f 波峰扫完 | 4f 是波浪感甜点——太密整排齐翻没有波,太疏尾部字符等太久 | | 停定咔哒 | 落定后 local 15→22f:translateY 0→6→-1.5→0,Easing.out(quad) | 6px 是可感下限(4px 无感判例);这一下就是"咔哒"的画面体,删了只剩滑动 | | 明暗 | 掉落叶 brightness 1→0.55,拍下叶 0.55→1 | 明暗差是叶片"转过铰链"的立体感来源,比 rotateX 本身更被肉眼读到 | | 翻牌格 | 118×156,perspective 420,圆角 10,中缝铰链线 4px #141412 | 铰链线压住翻转轴穿帮;perspective 太大叶片透视变形夸张 | | 配色字体 | 格底 #262624 / 字 #f4f4f2,Helvetica 800 100px;空格占 52px、格间 gap 12 | 深底浅字是翻牌屏的身份证;等宽格子布局,别用比例字体挤格 | | 节拍 | 建立 0–21f 静止 → 22f 起级联 → 全停后静止 ≥15f | 建立段让观众先认出"这是块翻牌屏",直接开翻读作故障闪烁 |

已知坑

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

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

  • 声音强依赖:每格停定的机械拍打声是效果的一半,实战必须配——级联

停定天然是一串节奏点,assets/audio/sfx/ui/switch-click-quick.mp3 或 sfx/mech/gear-lock-metallic.mp3 起步(更贴机械翻牌),也可用 sfx/text/typewriter-hit-single.mp3;配法纪律见 sound-design.md (同类元素同音,拟音与动作等长,连发交替双样本防机枪感)

  • 下沉回弹 6px 是可感下限:肉眼看不出就照可感性判例加大幅度,别调

曲线不调幅度

  • 一支片翻牌字 ≤1 次:机械宣告腔太抢,重复即贬值(P4 手法去重精神);

与压印/打字机同片混用时留给最重的那句标题

参考实现

demos/typography/split-flap-title/ (SplitFlapFlip.tsx)

Reference implementation (Remotion)

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

Prompt

Use the video-shotcraft shot recipe "Split-flap board title" (Remotion composition, Apache-2.0) published on gimgs.net.
Recipe JSON (prompt = timing/easing/parameter table/pitfalls, files = TSX source): https://gimgs.net/motion/split-flap-title.json
Raw TSX: https://gimgs.net/motion/split-flap-title/template.html
Repository: https://github.com/Vincentwei1021/video-shotcraft  (component: demos/typography/split-flap-title/SplitFlapFlip.tsx; imports demos/_fixtures and assets/lib)
Spec: 1920x1080, 30 fps, 4.72s

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

5,278 chars · Apache-2.0Abrir template bruto ↗
// ===== demos/typography/split-flap-title/SplitFlapFlip.tsx =====
import React from 'react';
import { AbsoluteFill, Easing, interpolate, useCurrentFrame } from 'remotion';
import { FakeDashboard, G } from '../../_fixtures/Fixtures';

// split-flap-flip:机场翻牌字。每字符一个深底翻牌格(上下两半),
// 逐格翻过 3 个乱码中间态后咔哒停在目标字,左→右 4f 级联成波。
// 节拍:0–21 建立(整排乱码静止)→ 22 起级联翻牌 → 78 全部停定 → 静止到 140。

const TEXT = 'SHIP FASTER';
const CHARSET = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789#$%&';
const START = 22; // 级联起始帧
const STAGGER = 4; // 字符间级联延迟
const FLIP = 5; // 单次翻牌时长
const NFLIP = 3; // 每字符翻 3 次(2 个乱码中间态 + 1 次落到目标字)
const CELL_W = 118;
const CELL_H = 156;

// seed 正弦哈希(禁 Math.random)
const rnd = (a: number) => {
  const x = Math.sin(a * 127.3) * 43758.5453;
  return x - Math.floor(x);
};
const garble = (i: number, k: number) =>
  CHARSET[Math.floor(rnd(i * 7.13 + k * 3.71 + 1) * CHARSET.length)];

const FLAP_BG = '#262624';
const FLAP_INK = '#f4f4f2';

// 半格:上/下半各自 overflow hidden,内部整字定位错半格露出对应一半
const Half: React.FC<{ ch: string; part: 'top' | 'bottom' }> = ({ ch, part }) => (
  <div
    style={{
      position: 'absolute',
      left: 0,
      top: part === 'top' ? 0 : CELL_H / 2,
      width: CELL_W,
      height: CELL_H / 2,
      overflow: 'hidden',
      background: FLAP_BG,
      borderRadius: part === 'top' ? '10px 10px 0 0' : '0 0 10px 10px',
    }}
  >
    <div
      style={{
        position: 'absolute',
        left: 0,
        top: part === 'top' ? 0 : -CELL_H / 2,
        width: CELL_W,
        height: CELL_H,
        display: 'flex',
        alignItems: 'center',
        justifyContent: 'center',
        fontFamily: 'Helvetica, Arial, sans-serif',
        fontWeight: 800,
        fontSize: 100,
        color: FLAP_INK,
      }}
    >
      {ch}
    </div>
  </div>
);

const FlapCell: React.FC<{ target: string; i: number; frame: number }> = ({
  target,
  i,
  frame,
}) => {
  // 该格的字符序列:2 个乱码 → 1 个乱码 → 目标字(首态也是乱码,建立段可见)
  const seq = [garble(i, 0), garble(i, 1), garble(i, 2), target];
  const local = frame - (START + i * STAGGER);
  const done = local >= NFLIP * FLIP;

  // 停定咔哒:整格下沉回弹(1px 肉眼无感,放大到 6px 才有"咔哒")
  const clickY = done
    ? interpolate(local, [15, 17, 19, 22], [0, 6, -1.5, 0], {
        extrapolateLeft: 'clamp',
        extrapolateRight: 'clamp',
        easing: Easing.out(Easing.quad),
      })
    : 0;

  let topCh = seq[0];
  let bottomCh = seq[0];
  let flap: React.ReactNode = null;

  if (done) {
    topCh = target;
    bottomCh = target;
  } else if (local > 0) {
    const k = Math.min(NFLIP - 1, Math.floor(local / FLIP));
    const from = seq[k];
    const to = seq[k + 1];
    const p = Easing.in(Easing.quad)((local - k * FLIP) / FLIP); // 重力感:越掉越快
    topCh = to; // 上半静态:翻开后露出下一字符的上半
    bottomCh = from; // 下半静态:保持旧字符直到活动叶盖下来
    if (p < 0.5) {
      // 前半程:旧字符上半叶 0→-90 掉下
      const deg = p * 2 * 90;
      flap = (
        <div
          style={{
            position: 'absolute',
            inset: 0,
            transform: `rotateX(${-deg}deg)`,
            transformOrigin: `center ${CELL_H / 2}px`,
            backfaceVisibility: 'hidden',
            filter: `brightness(${1 - p * 2 * 0.45})`,
            zIndex: 2,
          }}
        >
          <Half ch={from} part="top" />
        </div>
      );
    } else {
      // 后半程:新字符下半叶 90→0 拍下盖住旧下半
      const deg = 90 - (p - 0.5) * 2 * 90;
      flap = (
        <div
          style={{
            position: 'absolute',
            inset: 0,
            transform: `rotateX(${deg}deg)`,
            transformOrigin: `center ${CELL_H / 2}px`,
            backfaceVisibility: 'hidden',
            filter: `brightness(${0.55 + (p - 0.5) * 2 * 0.45})`,
            zIndex: 2,
          }}
        >
          <Half ch={to} part="bottom" />
        </div>
      );
    }
  }

  return (
    <div
      style={{
        position: 'relative',
        width: CELL_W,
        height: CELL_H,
        transform: `translateY(${clickY}px)`,
        perspective: 420,
        borderRadius: 10,
        boxShadow: '0 6px 18px rgba(0,0,0,0.3)',
      }}
    >
      <Half ch={topCh} part="top" />
      <Half ch={bottomCh} part="bottom" />
      {flap}
      {/* 中缝铰链线 */}
      <div
        style={{
          position: 'absolute',
          left: 0,
          top: CELL_H / 2 - 2,
          width: CELL_W,
          height: 4,
          background: '#141412',
          zIndex: 3,
        }}
      />
    </div>
  );
};

export const SplitFlapFlip: React.FC = () => {
  const frame = useCurrentFrame();
  let letterIdx = 0;
  return (
    <AbsoluteFill style={{ background: G.bg, overflow: 'hidden' }}>
      {/* 背景假页面压暗,突出翻牌板 */}
      <div style={{ position: 'absolute', inset: 0, opacity: 0.3, filter: 'saturate(0.8)' }}>
        <FakeDashboard variant="A" />
      </div>
      <AbsoluteFill style={{ alignItems: 'center', justifyContent: 'center' }}>
        <div style={{ display: 'flex', gap: 12, alignItems: 'center' }}>
          {TEXT.split('').map((ch, idx) => {
            if (ch === ' ') {
              return <div key={idx} style={{ width: 52 }} />;
            }
            const i = letterIdx++;
            return <FlapCell key={idx} target={ch} i={i} frame={frame} />;
          })}
        </div>
      </AbsoluteFill>
    </AbsoluteFill>
  );
};


  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/split-flap-title/SplitFlapFlip.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