Motion · Chữ & title card
Tiêu đề bảng lật kiểu sân bay
Tiêu đề bảng lật sân bay — mỗi ký tự có hai nửa ô cơ khí lật qua, đảo qua 2 khung mã lỗi rồi lách cách dừng đúng chữ đích, lan từ trái sang phải thành sóng.
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
Khi nào dùng
Tiêu đề lớn mở đầu/mở chương; văn bản đếm ngược, ngày phát hành, kiểu công bố số liệu; nhịp cần "cảm giác thông báo cơ khí"
Ý đồ
Kiểu thứ ba trong ba thủ pháp chữ: dập nổi (paper-title-card) là mực in trên giấy, một lời "thì thầm"; máy đánh chữ (document-typewriter-reveal) là tài liệu được viết ra, một lời "tường thuật"; bảng lật là màn hình lớn ở ga tàu, mang giọng "phát thanh" đầy chất thông báo cơ khí. Chọn bảng lật khi cần cảm giác đếm ngược/khởi hành (phát hành phiên bản, hạn chót), cảm giác công bố số liệu (tiết lộ chỉ số), hoặc muốn thêm lớp chất cơ khí hoài cổ cho tiêu đề. Nó tự nhiên đọc như một lưới nền tối chữ đều hàng, trong thẩm mỹ giấy-mực của thư viện đọc thành "một màn hình cơ khí đặt trên trang giấy" — khối màu khác biệt tự nó là điểm nhấn, nên hình sản phẩm nền phải tối và giảm bão hòa để nhường chỗ.
Thời lượng & năng lượng
- Thời lượng: khoảng 4.7s (140f: ≥20f giữ mã lỗi để thiết lập + lật cascading + ≥15f giữ đứng yên khi dừng)
- Năng lượng: Trung bình (chuyển động cơ khí liên tục, không phải cú va chạm tức thời)
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)
意图
文字三式的第三式:压印(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)
Mã nguồn tham chiếu (Remotion)
- demos/typography/split-flap-title/SplitFlapFlip.tsx — https://raw.githubusercontent.com/Vincentwei1021/video-shotcraft/main/demos/typography/split-flap-title/SplitFlapFlip.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.
- 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.
- Clone repo video-shotcraft; component import fixture chung từ demos/_fixtures và helper từ assets/lib.
- Đă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/split-flap-title.jsongit clone https://github.com/Vincentwei1021/video-shotcraft
# component: demos/typography/split-flap-title/SplitFlapFlip.tsx
npx remotion render <CompositionId> out.mp4claude 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 "Tiêu đề bảng lật kiểu sân bay" (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).
// ===== 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>
);
};
- Read the recipePrompt = when to use, intention, parameter table with typical values and how each one feels, known pitfalls. Treat values as calibrated starting points.
- Get the codetool: git
git clone https://github.com/Vincentwei1021/video-shotcraft && cd video-shotcraft && npm install - Register + rendertool: remotion
npx remotion render <CompositionId> out.mp4 # component: demos/typography/split-flap-title/SplitFlapFlip.tsx - Or re-implementPort the timing/easing from the recipe to HTML + WAAPI (gimgs motion contract) or any engine; keep the same segment windows and easing.
| param | type | default | description |
|---|---|---|---|
| — | |||
Cùng danh mục

Ba từ tiếp sức bằng hình học
Ba từ lợi ích mỗi từ mang riêng một bộ hình học tiếp sức — vòng tròn nét đứt tự xoay thu nhỏ → ba vòng tròn nét liền mọc lên lần lượt → ánh kim sheen quét qua rồi thu về trắng thuần; mỗi từ mới viền nét rồi tô đầy.

Tiếp sức từ với cuộn phim trang
Cột trái các thẻ trang đen trắng cuộn theo bước, bên phải từ lớn chữ serif tiếp sức tại chỗ (danh từ cố định + động từ luân phiên) — chỉ khi đổi từ mới cuộn một bước, khớp chính xác điểm giữa thẻ trang hiện tại.

Cuộn từ dọc theo nhịp
Từ cuối câu đổi trên cuộn dọc, 3 lần đổi từ mỗi lần 0.55s, nhanh trước chậm sau kèm hơi overshoot; các dòng lân cận nhòe dọc và xám dần theo khoảng cách, từ ở giữa vừa đậu liền nhuộm từ xám sang màu nhấn.

Hai cách tiết lộ code song song
Cùng một khối code tô cú pháp được tiết lộ hai cách song song — trái: stagger cấp dòng mờ vào nổi lên 8px, phải: gõ từng ký tự nhưng giữ nguyên màu token gốc, con trỏ vuông #3a4468 đệm dưới ký tự hiện tại.