Motion · Chữ & title card
Xoay vòng nhãn trong câu
Thân câu cố định đứng yên, nhãn pill cuối câu cuộn slot đổi từ khoảng ~0.7s một lần cho đến khi câu hoàn chỉnh.
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
Giải pháp thanh lịch nhất cho nội dung kiểu "liệt kê tính năng" (nhanh hơn danh sách từng dòng, có ngữ nghĩa hơn giải mã ký tự lộn xộn); đoạn có một câu bán hàng + nhiều cụm động từ
Ý đồ
Liệt kê sáu tính năng thường cần sáu dòng, mờ vào từng dòng cũng tốn sáu nhịp và chiếm hết màn hình. Xoay vòng từ trong ô nhét tất cả vào một ô của một câu: thân câu "One AI tool to ___" là lời hứa, mỗi nhãn pill cuộn qua ô là một bằng chứng, cuối cùng nhãn bay đi và "do it all." an vị — liệt kê xong, kết luận đóng dấu. Cấu trúc tự mang ba hồi: hứa hẹn → liệt kê → chốt lại. Mấu chốt là thân câu đứng yên tuyệt đối: mắt người xem suốt cả đoạn luôn khóa vào ô đó, thân câu chỉ cần lung lay là cảm giác liệt kê tan biến.
Thời lượng & năng lượng
- Thời lượng: Vào cảnh 12f + mỗi nhịp 21f × số từ + kết 14f + giữ; 6 từ khoảng 5.8s (demo 175f)
- Năng lượng: Trung bình (bộ đếm nhịp ổn định, không có đỉnh)
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)
意图
罗列六个功能,列表要六行、逐条淡入要六拍还占满屏。词槽轮换把它们 塞进一句话的一个槽位里:句干 "One AI tool to ___" 是承诺,槽里 滚过的每个 pill 是证据,最后 pill 飞走、"do it all." 落位——证据 列举完毕,结论盖章。结构自带三幕:承诺→列举→收束。命门是句干 纹丝不动:观众的眼睛全程钉在槽位上,句干一晃列举感就散。
动效核心
- 句干绝对定位左端锚死——不能用 flex 居中,pill 宽度变化会带着
句干重排(首轮踩过的坑,已是判例)
- 每拍 21f(~0.7s),前 8f 完成换位、后 13f 静置——静置段是给观众
读词的,读不完的拍子等于没换
- 入场:translateY 120→0 + blur 14→4→0,Easing.out(cubic);
离场:translateY 0→-130 + blur 0→10,Easing.in(cubic) 加速飞出 ——新来的减速落定、旧的加速逃走,方向感是"向上翻页"
- 槽内用隐形占位 pill(visibility hidden)撑宽度,新旧两枚绝对定位
叠放——槽宽随当前词自然变化但不抖
- 收束拍:末 pill 上飞(7f ease-in),"do it all." 从下 90px 带
Easing.back(1.4) 过冲落位——全片唯一一次过冲,留给结论
参数表
| 参数 | 典型值 | 调节手感 | |------|--------|----------| | 拍长 | 21f/词,5–7 词 | <16f 读不完短语;>28f 节拍器感散掉;8+ 词观众开始数数 | | 换位窗 | 拍内前 8f | 拉长到 12f+ 新旧同屏太久,读作两个 pill 打架 | | 飞行距离 | 入 +120 / 出 -130px | <80px 读作原地闪换;入出不对称(出略远)让离场更决绝 | | 运动模糊 | 入 14→0 / 出 0→10px blur | 去掉 blur 换位读作硬弹;>20px 中间帧糊成色块 | | 收束过冲 | Easing.back(1.4),14f | >2 结论落位像卡通;无过冲结论与列举没有档次差 | | 收尾 hold | 完整句静置 ≥45f | 这句话是全段目的,不 hold 等于白列举 |
已知坑
- demo 在灰阶/占位素材上调校通过——参数是调校起点非实战定稿,
首次实战须以真实素材回验
- 与 odometer-digit-roll / split-flap-title 分工:那两卡是字符/数位级
机械滚动(无语义),本卡是整词级 pill 轮换且嵌在活句子里——文案是 "句干+动词短语"结构才用本卡,纯数字/纯标题词别硬套
- 与 typewriter-moves 分工:打字机是"一个字一个字造句",本卡是
"句子造好了换零件"——同片可共存但别在同一句文案上连用
- 与 type-rhythm-sync 分工:那是既有文字随音轨变属性(不换内容),
本卡换内容——绑音轨时本卡拍点对鼓点即可,别再叠字重脉冲
- pill 短语长度要接近(demo 最长 "Draft an agenda")——长短差 >2 倍
槽宽跳变太猛,句子重心左右甩
- 收束句与句干字号字重必须完全一致(demo 都是 96px/800)——
"do it all." 若比句干大,读作新标题而非句子补完
参考实现
demos/typography/pill-slot-cycle/ (PillSlotCycle.tsx) 原片出处:notion-ai 4.5–8.5s
Mã nguồn tham chiếu (Remotion)
- demos/typography/pill-slot-cycle/PillSlotCycle.tsx — https://raw.githubusercontent.com/Vincentwei1021/video-shotcraft/main/demos/typography/pill-slot-cycle/PillSlotCycle.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/pill-slot-cycle.jsongit clone https://github.com/Vincentwei1021/video-shotcraft
# component: demos/typography/pill-slot-cycle/PillSlotCycle.tsx
npx remotion render <CompositionId> out.mp4claude mcp add --transport http gimgs https://gimgs.net/mcp
# then: search_motion_templates / get_motion_template("pill-slot-cycle")Prompt
Use the video-shotcraft shot recipe "Xoay vòng nhãn trong câu" (Remotion composition, Apache-2.0) published on gimgs.net. Recipe JSON (prompt = timing/easing/parameter table/pitfalls, files = TSX source): https://gimgs.net/motion/pill-slot-cycle.json Raw TSX: https://gimgs.net/motion/pill-slot-cycle/template.html Repository: https://github.com/Vincentwei1021/video-shotcraft (component: demos/typography/pill-slot-cycle/PillSlotCycle.tsx; imports demos/_fixtures and assets/lib) Spec: 1920x1080, 30 fps, 5.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).
// ===== demos/typography/pill-slot-cycle/PillSlotCycle.tsx =====
// pill-slot-cycle —— 句中词槽轮换:固定句干 + 句尾 pill 老虎机式滚一格
// 源:notion-ai 4.5–8.5s。旧 pill 上飞淡出、新 pill 从下带运动模糊滑入,
// 连换 6 次后 pill 消失、句子落成 "One AI tool to do it all." 收束。
import React from 'react';
import { AbsoluteFill, useCurrentFrame, interpolate, Easing } from 'remotion';
import { G } from '../../_fixtures/Fixtures';
const FONT = 'Helvetica, Arial, sans-serif';
const PILLS = [
{ label: 'Ask a question', icon: '?' },
{ label: 'Find in Drive', icon: '▲' },
{ label: 'Find in Slack', icon: '#' },
{ label: 'Summarize', icon: '≡' },
{ label: 'Improve writing', icon: '✎' },
{ label: 'Draft an agenda', icon: '☰' },
];
const BEAT = 21; // ~0.7s @30fps
const INTRO = 12; // 句干入场
const CYCLES = PILLS.length;
const Pill: React.FC<{ label: string; icon: string; style?: React.CSSProperties }> = ({
label,
icon,
style,
}) => (
<div
style={{
display: 'inline-flex',
alignItems: 'center',
gap: 16,
padding: '14px 36px 14px 24px',
borderRadius: 999,
background: '#fff',
border: `3px solid ${G.border}`,
boxShadow: '0 6px 18px rgba(0,0,0,0.10)',
fontFamily: FONT,
fontWeight: 700,
fontSize: 64,
color: G.ink,
whiteSpace: 'nowrap',
...style,
}}
>
<span
style={{
width: 58,
height: 58,
borderRadius: 14,
background: G.bg,
display: 'inline-flex',
alignItems: 'center',
justifyContent: 'center',
fontSize: 38,
color: G.mid,
flexShrink: 0,
}}
>
{icon}
</span>
{label}
</div>
);
export const PillSlotCycle: React.FC = () => {
const frame = useCurrentFrame();
// 句干入场:ease-out 上浮淡入
const stemT = interpolate(frame, [0, INTRO], [0, 1], {
extrapolateRight: 'clamp',
easing: Easing.out(Easing.cubic),
});
const cycleStart = INTRO;
const cycleEnd = cycleStart + CYCLES * BEAT;
// 当前处于第几个 pill 拍
const rel = frame - cycleStart;
const idx = Math.min(Math.floor(rel / BEAT), CYCLES - 1);
const beatFrame = rel - idx * BEAT;
const SWAP = 8; // 每拍前 8f 完成换位
// 收束段:pill 上飞消失,"do it all." 从下滑入落位
const isFinale = frame >= cycleEnd;
const finT = interpolate(frame, [cycleEnd, cycleEnd + 14], [0, 1], {
extrapolateLeft: 'clamp',
extrapolateRight: 'clamp',
easing: Easing.out(Easing.back(1.4)),
});
// 槽内容渲染
let slot: React.ReactNode = null;
if (!isFinale && rel >= 0) {
const incoming = PILLS[idx];
const outgoing = idx > 0 ? PILLS[idx - 1] : null;
// 新 pill:从下 +120px 带 blur 滑入,ease-out
const inT = interpolate(beatFrame, [0, SWAP], [0, 1], {
extrapolateRight: 'clamp',
easing: Easing.out(Easing.cubic),
});
const inY = interpolate(inT, [0, 1], [120, 0]);
const inBlur = interpolate(inT, [0, 0.7, 1], [14, 4, 0]);
// 旧 pill:向上 -120px 加速飞出淡掉
const outT = interpolate(beatFrame, [0, SWAP - 1], [0, 1], {
extrapolateRight: 'clamp',
easing: Easing.in(Easing.cubic),
});
const outY = interpolate(outT, [0, 1], [0, -130]);
slot = (
<div style={{ position: 'relative', display: 'inline-block' }}>
{/* 撑起槽宽度的隐形占位(当前 pill) */}
<Pill label={incoming.label} icon={incoming.icon} style={{ visibility: 'hidden' }} />
{outgoing && outT < 1 && (
<div
style={{
position: 'absolute',
left: 0,
top: 0,
transform: `translateY(${outY}px)`,
opacity: 1 - outT,
filter: `blur(${outT * 10}px)`,
}}
>
<Pill label={outgoing.label} icon={outgoing.icon} />
</div>
)}
<div
style={{
position: 'absolute',
left: 0,
top: 0,
transform: `translateY(${inY}px)`,
opacity: idx === 0 ? inT : Math.min(1, inT * 1.6),
filter: `blur(${inBlur}px)`,
}}
>
<Pill label={incoming.label} icon={incoming.icon} />
</div>
</div>
);
} else if (isFinale) {
// 最后一个 pill 上飞离场(前 8f),然后 "do it all." 落位
const lastOutT = interpolate(frame, [cycleEnd, cycleEnd + 7], [0, 1], {
extrapolateLeft: 'clamp',
extrapolateRight: 'clamp',
easing: Easing.in(Easing.cubic),
});
slot = (
<div style={{ position: 'relative', display: 'inline-block' }}>
<span
style={{
fontFamily: FONT,
fontWeight: 800,
fontSize: 96,
color: G.ink,
letterSpacing: -1,
display: 'inline-block',
opacity: finT,
transform: `translateY(${(1 - finT) * 90}px)`,
filter: `blur(${(1 - finT) * 8}px)`,
whiteSpace: 'nowrap',
}}
>
do it all.
</span>
{lastOutT < 1 && (
<div
style={{
position: 'absolute',
left: 0,
top: -8,
transform: `translateY(${-130 * lastOutT}px)`,
opacity: 1 - lastOutT,
filter: `blur(${lastOutT * 10}px)`,
}}
>
<Pill label={PILLS[CYCLES - 1].label} icon={PILLS[CYCLES - 1].icon} />
</div>
)}
</div>
);
}
return (
<AbsoluteFill style={{ background: G.bg }}>
{/* 句干固定不动:整行左端锚死,不随 pill 宽度居中重排 */}
<div
style={{
position: 'absolute',
left: 300,
top: 540,
transform: `translateY(calc(-50% + ${(1 - stemT) * 50}px))`,
display: 'flex',
alignItems: 'center',
gap: 36,
opacity: stemT,
}}
>
<span
style={{
fontFamily: FONT,
fontWeight: 800,
fontSize: 96,
color: G.ink,
letterSpacing: -1,
whiteSpace: 'nowrap',
}}
>
One AI tool to
</span>
{slot}
</div>
</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/pill-slot-cycle/PillSlotCycle.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.