Motion · Typography
Pill slot cycle
A fixed sentence stem stays locked while a pill badge at the end slot-machine-rolls a new word roughly every 0.7s.
Source: video-shotcraft (Vincent Wei) ↗ · Apache-2.0 · Remotion composition (React/TSX) — preview video, no in-browser player
When to use
The most elegant solution for "feature listing" copy (faster than a bullet list, more semantic than scramble decoding); sections with a one-line value prop plus multiple verb phrases
Intention
Listing six features would normally need six lines, or six beats of one-by-one fade-ins that fill the whole screen. Word-slot cycling packs them into a single slot in a single sentence: the stem "One AI tool to ___" is the promise, each pill rolling through the slot is a piece of evidence, and when the final pill flies off and "do it all." lands, the evidence is complete and the conclusion is sealed. The structure carries a built-in three-act shape: promise → enumeration → close. The critical rule is that the stem never moves an inch — the viewer's eyes stay locked on the slot the whole time, and any wobble in the stem breaks the sense of enumeration.
Duration & energy
- Duration: Entrance 12f + 21f per beat × word count + close 14f + hold; 6 words ~5.8s (demo 175f)
- Energy: Medium (a steady metronome, no peak)
Shot recipe (original Chinese — timing, easing, parameter table, known pitfalls)
意图
罗列六个功能,列表要六行、逐条淡入要六拍还占满屏。词槽轮换把它们 塞进一句话的一个槽位里:句干 "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
Reference implementation (Remotion)
- demos/typography/pill-slot-cycle/PillSlotCycle.tsx — https://raw.githubusercontent.com/Vincentwei1021/video-shotcraft/main/demos/typography/pill-slot-cycle/PillSlotCycle.tsx
The component imports shared fixtures from demos/_fixtures and helpers from assets/lib in the repo; SKILL.md there documents the render workflow.
Everything on this page is also machine-readable. Point your agent at the JSON, or add the gimgs MCP server and ask it to search motion templates.
- Fetch the JSON — it holds the shot recipe (timing, easing, parameter table, pitfalls) and the full TSX source.
- Clone the video-shotcraft repo; the component imports shared fixtures from demos/_fixtures and helpers from assets/lib.
- Register the component as a <Composition> in a Remotion project and render with `npx remotion render`, or re-implement the recipe in your own engine.
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 "Pill slot cycle" (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 |
|---|---|---|---|
| — | |||
More in this category

Word relay geometry
Three benefit words each carry their own dedicated geometry relay — a dashed circle spins and shrinks → three solid circles trim-grow in sequence → a metallic sheen sweeps then collapses to pure white; each new word outlines then fills in.

Word relay filmstrip
A left column of black-and-white page cards steps forward while a serif big word on the right relays in place (a fixed noun + a cycling verb) — the column steps only when the word changes, aligned to the current page card's midpoint.

Vertical word-roll cycle
The sentence's last word cycles on a vertical roller, 3 swaps at 0.55s each, fast then very slow with a slight overshoot; neighboring rows carry vertical blur and grayscale by distance, the centered word snapping from gray to accent color as it lands.

Two code reveals, side by side
The same syntax-highlighted code reveals two ways side by side — left: line-level stagger fading up 8px, right: character-by-character typing keeping each token's original color, with a #3a4468 block cursor on the current character.