Motion · Interação
Chip Grid Blackout Select
Five option chips fade in one by one in a centered 3+2 layout; the selected one flashes a gray press frame, then within a few frames turns solid black with white text and a subtle 1→1.04→1 bounce, while other chips fade to 18% without moving; remaining chips then clear, and the black chip rises and narrows as a calculation row appears below.
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
Interactive demos of single-select/plan/tier picking; the "what happens after you pick it" causal shot; pricing/parameter-checkout flows.
Intention
One gray flash frame plus a three-frame reverse-to-black split apart real UI's :active → :selected two-stage state into separate beats. Viewers see a "finger press" and then a "system confirms," rather than one gradient. Other chips drop to 18% opacity but their positions never move — if they reflowed, viewers would think the page reloaded, and the semantics of "single select" would be lost.
Duration & energy
- Duration: About 5.0s (150f@30fps).
- Energy: Medium-low (the only burst is that one gray flash frame; the rest is settling).
Shot recipe (original Chinese — timing, easing, parameter table, known pitfalls)
意图
一帧灰闪 + 三帧反黑,是把真实 UI 的 :active → :selected 两级状态 拆开演。观众看到的是"手指按下"再"系统确认",而不是一次渐变。其余 chip 降到 18% 而位置绝不移动:一旦重排,观众会以为页面刷新了, "单选"的语义就丢了。
动效核心
- 5 个 chip 分 3+2 两行居中(top 96/136,gap 10,高 30、圆角 15),
按 d = 0.05 + i*0.028 逐个 outQuad 淡入(各 0.04)
- 1 帧按压灰闪:
seg(FS, FS+0.006) * (1 − seg(FS+0.006, FS+0.014)),
rgba(120,120,120,.5) 覆盖层(FS=0.44,0.006×5000ms ≈ 1 帧)
- 反黑:
seg(FS+0.008 → FS+0.04, linear)(≈5 帧)驱动
背景 #fff→INK、描边 LINE→INK、文字 TXT→#fff
- 按压回弹:
sc = 1 + sin(pr·π)*0.04,pr走 FS+0.008→FS+0.075
——正弦保证起止都精确回到 1,不需要额外关键帧
- 余项降级:
fade = seg(FS+0.008 → FS+0.075, outQuad)把 opacity
压到 lerp(fade, 1, 0.18) = 18%,transform 恒为 none
- 收束:
lift = seg(FS+0.30 → FS+0.42, inOutCubic)(选中后≈1.5s)
余项 opacity 归零,黑 chip translate(cx·lift, −46·lift) + scale→0.82; cx = 220 − (offsetLeft + width/2) 只测一次,让它上移时同时回到中线
- 算式行
FS+0.36→FS+0.42浮现,随后逐词加深到 t≈0.98
参数表
| 参数 | 典型值 | 调节手感 | |------|--------|----------| | 入场 | 间隔 0.028、各 0.04、outQuad | 5 个 chip 在前 0.2 铺完,把时间留给选中段 | | 选中时刻 | FS = 0.44(约全片中点) | 前半读题、后半看结果;FS 早于 0.3 观众还没读完选项 | | 灰闪 | 0.006 亮 + 0.008 落,rgba(120,120,120,.5) | 就一帧:给 2 帧以上就读作 hover 高亮,不是按下 | | 反黑 | FS+0.008→FS+0.04 linear(≈5f) | linear 是故意的:加 ease 会让"变黑"这件事有过程感,太软 | | 按压回弹 | 1 + sin(pr·π)·0.04,跨 0.067 | 0.04 是极轻;到 0.1 就成了弹跳按钮,抢戏 | | 余项降级 | 降到 18%,位置 transform:none | 位置锁死是命门:降到 18% 仍在原位,才读作"没被选中"而非"消失了" | | 收束时刻 | FS+0.30(选中后≈1.5s)起,0.12 inOutCubic | 1.5s 是让观众看清选中态的最短驻留;缩到 0.5s 就赶 | | 上移收窄 | translateY −46px、scale→0.82,横向补 cx 回中线 | 不补 cx 的话上移后会偏在左边,读作"被拖走" |
已知坑
cx用measured标志只测一次(首帧offsetWidth可能为 0 所以要
判空)——每帧重测会在 chip 自身缩放后越算越偏
- 其余 chip 的
transform必须显式写'none',靠 flex 自动重排会让
它们在余项归零时横向滑动
- 选中项要在同一个 flex 行里做 translate,不能改
position,
否则触发行内重排、其余 chip 位置跟着变
- 占位内容:5 条 "Option … plan" 选项名 + 算式行
"18% off · 42.00 → 34.44",落地全部替换;选项名过长会撑破单行 (第三条已是长名压力测试)
- 与
chip-lift-to-user-pill分工:那张选中后横向生长成新对象,
本卡选中后上移收窄让位给结果;同一片子里两者不要连用
参考实现
demos/interaction/chip-grid-single-select-blackout/ (ChipGridSingleSelectBlackout.tsx)
Reference implementation (Remotion)
- demos/interaction/chip-grid-single-select-blackout/ChipGridSingleSelectBlackout.tsx — https://raw.githubusercontent.com/Vincentwei1021/video-shotcraft/main/demos/interaction/chip-grid-single-select-blackout/ChipGridSingleSelectBlackout.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.
- Baixe o JSON — contém a receita do plano (timing, easing, tabela de parâmetros, armadilhas) e o código TSX completo.
- Clone o repositório video-shotcraft; o componente importa fixtures de demos/_fixtures e helpers de assets/lib.
- 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/chip-grid-single-select-blackout.jsongit clone https://github.com/Vincentwei1021/video-shotcraft
# component: demos/interaction/chip-grid-single-select-blackout/ChipGridSingleSelectBlackout.tsx
npx remotion render <CompositionId> out.mp4claude mcp add --transport http gimgs https://gimgs.net/mcp
# then: search_motion_templates / get_motion_template("chip-grid-single-select-blackout")Prompt
Use the video-shotcraft shot recipe "Chip Grid Blackout Select" (Remotion composition, Apache-2.0) published on gimgs.net. Recipe JSON (prompt = timing/easing/parameter table/pitfalls, files = TSX source): https://gimgs.net/motion/chip-grid-single-select-blackout.json Raw TSX: https://gimgs.net/motion/chip-grid-single-select-blackout/template.html Repository: https://github.com/Vincentwei1021/video-shotcraft (component: demos/interaction/chip-grid-single-select-blackout/ChipGridSingleSelectBlackout.tsx; imports demos/_fixtures and assets/lib) Spec: 1920x1080, 30 fps, 5.5s 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/interaction/chip-grid-single-select-blackout/ChipGridSingleSelectBlackout.tsx =====
// chip-grid-single-select-blackout — Single Select 1 帧灰闪单选反黑(motion-lab 定稿转原生 Remotion)
// 五个选项 chip 以 3+2 居中排布逐个淡入;选中帧先插一帧灰色按压块,紧接 3 帧内底色变纯黑、
// 文字变白并做 1→1.04→1 的极轻回弹,同窗口其余 chip 淡到 18% 但位置锁死;1s 后余项归零,
// 黑 chip 上移缩小,下方浮现算式行。
// 设计坐标 480×270(DesignStage 等比放大),440×240 定尺画布居中排版。
import React, { useLayoutEffect, useRef, useState } from 'react';
import { DesignStage, E, lerp, seg, useT } from '../../_fixtures/Motion';
export const CHIP_GRID_SINGLE_SELECT_BLACKOUT_DURATION = 150; // 5000ms @30fps
// ---- 本卡共享量(浅灰瑞士极简系配色 / 字体;BG/DIM/clamp01/h2r 取自 motion-lab/fx/b09.js 同批定义) ----
const SANS = '-apple-system,BlinkMacSystemFont,"Segoe UI",Helvetica,Arial,sans-serif';
const BG = '#F1F1F3'; // 页面浅灰
const INK = '#0B0B0C'; // 纯黑
const TXT = '#111111'; // 正文黑
const DIM = '#C9C9CE'; // 浅灰占位字
const LINE = '#E6E6EA'; // 描边
const clamp01 = (v: number) => (v < 0 ? 0 : v > 1 ? 1 : v);
const h2r = (h: string) => [parseInt(h.slice(1, 3), 16), parseInt(h.slice(3, 5), 16), parseInt(h.slice(5, 7), 16)];
// 颜色插值:mix(p,'#C9C9CE','#111')
const mix = (p: number, a: string, b: string) => {
const A = h2r(a), B = h2r(b), q = clamp01(p);
return `rgb(${Math.round(A[0] + (B[0] - A[0]) * q)},${Math.round(A[1] + (B[1] - A[1]) * q)},${Math.round(A[2] + (B[2] - A[2]) * q)})`;
};
// ---- 参数表(与 effect.js 完全一致) ----
const NAMES = ['Option one plan', 'Option two plan', 'Option three long name', 'Option four', 'Option five variant'];
const TI = 0; // 选中项下标
const FS = 0.44; // 灰闪起点
// 算式行(逐词加深语法)。注意原文含双空格:split(' ') 产生的空串"词"
// 也各占一个 4.5px margin 的空 span,是间距配方的一部分,必须保留。
const CAP_TEXT = '18% off · 42.00 → 34.44';
const CAP_WORDS = CAP_TEXT.split(' ');
const CAP_N = CAP_WORDS.length;
const CAP_ST = 0.78 / CAP_N;
const CAP_WIN = CAP_ST * 1.5;
export const ChipGridSingleSelectBlackout: React.FC = () => {
const t = useT();
// 选中 chip 上移时同步回到水平中线:cx = 220 - 选中 chip 在 440 画布内的中心 x。
// chip 宽度由文本 + padding 决定(flex 布局),挂载后实测一次;149 为兜底估算
// ("Option one plan" 600 12px ≈ 90px 文本 + 30 padding + 2 border,行首起点 ≈ 11)。
const targetRef = useRef<HTMLDivElement>(null);
const [cx, setCx] = useState(149);
useLayoutEffect(() => {
const el = targetRef.current;
if (el && el.offsetWidth) setCx(220 - (el.offsetLeft + el.offsetWidth / 2));
}, []);
const capShow = seg(t, FS + 0.36, FS + 0.42);
const capInn = seg(t, FS + 0.37, 0.98);
return (
<DesignStage bg={BG}>
{/* 页面 + 440×240 定尺画布(居中) */}
<div
style={{
position: 'absolute',
inset: 0,
background: BG,
overflow: 'hidden',
fontFamily: SANS,
WebkitFontSmoothing: 'antialiased',
}}
>
<div style={{ position: 'absolute', left: '50%', top: '50%', width: 440, height: 240, margin: '-120px 0 0 -220px' }}>
{/* 标题 */}
<div
style={{
position: 'absolute',
left: 0,
right: 0,
top: 26,
textAlign: 'center',
font: `600 11px/1 ${SANS}`,
letterSpacing: 2.4,
color: '#9A9AA2',
opacity: seg(t, 0.02, 0.1, E.outQuad),
}}
>
OPTION GROUP
</div>
{/* 两行 chip:3+2 居中排布(flex 布局,位置全程锁死不重排) */}
{[0, 1].map((row) => (
<div
key={row}
style={{
position: 'absolute',
left: 0,
right: 0,
top: row === 0 ? 96 : 136,
display: 'flex',
justifyContent: 'center',
gap: 10,
}}
>
{NAMES.map((n, i) => {
if ((i < 3 ? 0 : 1) !== row) return null;
const d = 0.05 + i * 0.028;
const inP = seg(t, d, d + 0.04, E.outQuad);
let bg = '#fff';
let borderColor = LINE;
let labelColor: string = TXT;
let flashO = '0';
let opacity: string | number;
let transform: string;
if (i === TI) {
// 灰闪 1 帧
flashO = (seg(t, FS, FS + 0.006) * (1 - seg(t, FS + 0.006, FS + 0.014))).toFixed(3);
const bk = seg(t, FS + 0.008, FS + 0.04, E.linear);
bg = mix(bk, '#ffffff', INK);
borderColor = mix(bk, LINE, INK);
labelColor = mix(bk, TXT, '#ffffff');
// 按压回弹 1→1.04→1
const pr = seg(t, FS + 0.008, FS + 0.075, E.linear);
const sc = 1 + Math.sin(pr * Math.PI) * 0.04 * (pr > 0 ? 1 : 0);
const lift = seg(t, FS + 0.30, FS + 0.42, E.inOutCubic);
opacity = inP;
transform =
`translate(${(cx * lift).toFixed(2)}px,${(-46 * lift).toFixed(2)}px) ` +
`scale(${(sc * lerp(lift, 1, 0.82)).toFixed(4)})`;
} else {
const fade = seg(t, FS + 0.008, FS + 0.075, E.outQuad);
const gone = seg(t, FS + 0.30, FS + 0.35, E.outQuad);
opacity = (inP * lerp(fade, 1, 0.18) * (1 - gone)).toFixed(3);
transform = 'none';
}
return (
<div
key={i}
ref={i === TI ? targetRef : undefined}
style={{
position: 'relative',
height: 30,
borderRadius: 15,
background: bg,
border: `1px solid ${borderColor}`,
boxSizing: 'border-box',
display: 'flex',
alignItems: 'center',
padding: '0 15px',
boxShadow: '0 1px 4px rgba(0,0,0,.04)',
overflow: 'hidden',
opacity,
transform,
}}
>
<div style={{ font: `600 12px/1 ${SANS}`, color: labelColor, letterSpacing: '-.01em', whiteSpace: 'nowrap' }}>
{n}
</div>
{/* 按压灰闪块(盖在 label 上) */}
<div style={{ position: 'absolute', inset: 0, background: 'rgba(120,120,120,.5)', opacity: flashO }} />
</div>
);
})}
</div>
))}
{/* 算式行:逐词加深(浅灰占位 → 黑),整行透明度随 show 淡入 */}
<div
style={{
position: 'absolute',
left: '50%',
top: 150,
display: 'flex',
alignItems: 'baseline',
whiteSpace: 'nowrap',
transform: 'translateX(-50%)',
opacity: capShow,
}}
>
{CAP_WORDS.map((w, i) => {
const q = clamp01((capInn - i * CAP_ST) / CAP_WIN);
return (
<span
key={i}
style={{
font: `600 14px/1.25 ${SANS}`,
color: mix(q, DIM, TXT),
letterSpacing: (-0.03 * (1 - q)).toFixed(4) + 'em',
marginRight: i === CAP_N - 1 ? 0 : 4.5,
}}
>
{w}
</span>
);
})}
</div>
</div>
</div>
</DesignStage>
);
};
- 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/interaction/chip-grid-single-select-blackout/ChipGridSingleSelectBlackout.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 |
|---|---|---|---|
| — | |||
Mais nesta categoria

Live voice waveform
A live waveform in a recording pill — 64 thin bars rise and fall with "speech," spiking tall mid-speech and shrinking to a dotted line on pauses, scrolling right to left; a full speak-pause-speak-submit performance.

Type, filter, drill in
Typing a search on a real UI, the grid converges to a single card on its own, then a click drills through into the detail page.

Theme switch: sweep and ripple
Two theme-switch moves — theme-sweep, a diagonal wipe (the skin changes wherever the boundary passes), and palette-ripple, a combo move (a ⌘K panel collapses to a point, a ripple spreads from it to re-skin).
Segmented thumb hero
A segmented control's thumb shift becomes the star of a close-up — a cursor clicks it and a new icon springs in.