Motion · Tương tác
Chọn một chip nền đen
Năm chip lựa chọn xếp 3+2 giữa khung lần lượt mờ dần hiện ra; khung được chọn chèn một khung ép màu xám, ngay sau đó vài khung nền chuyển đen tuyền chữ trắng kèm nảy nhẹ 1→1.04→1, các chip khác mờ còn 18% nhưng vị trí khóa chặt; rồi các chip còn lại về 0, chip đen dịch lên thu hẹp, phía dưới hiện dòng công thức tí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
Trình diễn tương tác chọn một/gói/mức giá; cảnh nhân quả "chọn nó rồi thì sao"; luồng chốt giá/tham số.
Ý đồ
Một khung lóe xám + ba khung đảo đen là cách tách hai trạng thái :active → :selected của UI thật ra diễn riêng. Người xem thấy "ngón tay nhấn xuống" rồi "hệ thống xác nhận", chứ không phải một lần chuyển sắc mượt. Các chip khác giảm còn 18% nhưng vị trí tuyệt đối không di chuyển: một khi sắp xếp lại, người xem sẽ tưởng trang vừa tải lại, ngữ nghĩa "chọn một" sẽ mất.
Thời lượng & năng lượng
- Thời lượng: Khoảng 5.0s (150f@30fps).
- Năng lượng: Trung bình thấp (điểm bùng nổ duy nhất là khung lóe xám đó, còn lại đều đang thu gọn).
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)
意图
一帧灰闪 + 三帧反黑,是把真实 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)
Mã nguồn tham chiếu (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
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/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 "Chọn một chip nền đen" (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 |
|---|---|---|---|
| — | |||
Cùng danh mục

Sóng âm giọng nói trực tiếp
Sóng âm trực tiếp trên viên nang ghi âm — 64 cột dọc mảnh nhấp nhô theo "lời nói", giữa lúc nói vươn cao, ngừng thì co lại thành dòng chấm, sóng cuộn từ phải sang trái; trọn màn nói→ngừng→nói→gửi.

Gõ, lọc, đi vào chi tiết
Gõ tìm kiếm trên UI thật, lưới tự thu về một thẻ duy nhất, rồi cú click xuyên vào trang chi tiết.

Đổi theme: quét dọc và gợn sóng
Hai kiểu đổi theme — theme-sweep quét chéo (biên quét qua đâu đổi da đến đó) và palette-ripple kiểu kết hợp (panel ⌘K co lại thành điểm, gợn sóng lan ra từ điểm đó đổi da).
Thumb điều khiển phân đoạn làm nhân vật chính
Cú dịch chuyển thumb trong control phân đoạn thành nhân vật chính cận cảnh — con trỏ bấm vào, thumb trắng trượt qua, icon mới bật lên.