Motion · UI xuất hiện
Thẻ sản phẩm tự lắp dần
Thẻ chi tiết tự lắp ráp từng trường như bị bắt lấy — ảnh, tiêu đề, breadcrumb pill bật ra, giá cũ gạch ngang, giá mới bật lên.
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 năng lực cho trang chi tiết sản phẩm/mục; cảnh chính cho các câu chuyện "trích xuất có cấu trúc", "tự động điền", "dữ liệu tự mọc ra"
Ý đồ
Một thẻ chi tiết chỉ mờ vào thẳng khiến người xem chỉ thấy "có một cái thẻ". Lắp từng trường biến thẻ thành một biểu mẫu đang được điền trực tiếp: mỗi trường tự vào vị trí của nó, sự chú ý của người xem được dẫn dắt đi qua cấu trúc thông tin của thẻ — ảnh ở đâu, tiêu đề ở đâu, giá ở đâu, nội dung trọng tâm ở đâu. Việc giá bị gạch ngang hạ cấp là sự kiện ngữ nghĩa duy nhất của cả cảnh (không phải "xuất hiện" mà là "thay đổi"), nên nó được đặt ở vị trí nổi bật nhất, giữa đoạn.
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 (nhiều sự kiện nhỏ liên tục dày đặc, không có bùng nổ đơn lẻ; đẩy máy chậm liên tục giữ cảm giác tiến triể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)
意图
一张详情卡直接淡入,观众只看到"有一张卡"。逐字段落位则把卡变成一份正在被填写的 表单:每个字段各自到位,观众的注意力被牵着走一遍卡的信息结构——图在哪、标题在哪、 价格在哪、正文重点在哪。价格划线降级是全片唯一的语义事件(不是"出现"而是"改变"), 所以它被安排在中段最显眼的位置。
动效核心
- 字段时间表用"配方帧"书写:
F = f => f/60,把 60 分母的帧号折成 t。图 f=0、标题 f=4、
三枚 pill f=8/10/12、价格行 f=16、三行正文 f=30/32/34、三块色卡 f=40/42/44 —— 相邻字段间隔只有 2 帧,是"抓取"的密度而非"逐条展示"的密度
- 每个字段共享同一条落位曲线:
seg(t, t0, t0+0.1, E.outCubic),两种落位模式——
rise 是 translateY(6→0)(图/标题/价格行/正文行),pop 是 scale(lerp(E.outBack(k), 0.4, 1))(pill 与色卡);小件用 pop 大件用 rise
- 价格降级发生在 f=26(配方帧),是硬切:
oldP同帧换色(#17181c→#9a9da6)、
加 line-through、字号 21px→15px 三件事一起发生;紧接 seg(t, F(26), +0.12) 让 强调色新价用 E.spring(nk, 0.35) 从 1.15 收到 1
- 马克笔高亮:两个高亮片段各有一层绝对定位的
ACCENT_SOFT底块,`transform-origin:
left center + scaleX(0→1),窗口是 F(30+li*2+4)` 起、行程 0.085(≈5f)——比字段 落位还快,读作"笔一划过去"
- 整卡呼吸:
seg(t, 0, 0.75, E.outQuad)驱动scale(1→1.06),缓起缓收摊在前 75%——
这层运动与所有字段无关,作用是让密集的小事件之间画面仍在推进
参数表
| 参数 | 典型值 | 调节手感 | |------|--------|----------| | 字段时间表 | 配方帧 0/4/8/10/12/16/30/32/34/40/42/44(F(f)=f/60) | 同类字段间隔 2 帧、跨类间隔 4–14 帧——间隔的疏密就是信息分组;全部改成等间隔会读作机械点名 | | 单字段行程 | 0.1(≈15f) | 行程远大于间隔 → 同时有五六个字段在动,画面是"涌"而非"点";压到 0.04 以下会变成逐个硬闪 | | rise 位移 | 6px | 只有 6px,字段是"沉下去一点点";>20px 会与整卡前推的方向感打架 | | pop 起始缩放 | 0.4 + E.outBack | 0.4 是明确的"从无到有";pill 与色卡尺寸小,过冲不会撞到邻居,大件用 pop 就会显得廉价 | | 价格降级点 | 配方帧 26(t≈0.433) | 安排在价格行落位(f=16)之后 10 帧——必须留出"先看清原价"的时间,紧贴落位会读作从来就是打折价 | | 新价 spring | E.spring(nk, 0.35),1.15→1,窗 0.12 | 全片唯一的过冲量级事件;阻尼调小会抖两下,与旁边的划线动作抢注意力 | | 高亮刷过 | 窗 0.085(≈5f),scaleX 从左 | 5f 是马克笔速度;>0.15 读作背景色渐变而不是划线 | | 整卡前推 | scale 1→1.06,窗 0→0.75,outQuad | 6% 摊在 3.75s 上是纯呼吸;>12% 卡会撞到画框,且 4.5% 的 padding 内的文字会被推出边界 |
已知坑
- 时间表用的是 60 分母的"配方帧",而实际时长 5000ms = 150f@30fps。这两套帧号不通约:
想按 30fps 的帧数排事件,必须先把配方帧换算成 t 再对照(f=44 → t≈0.733 → 110f@30fps)
- 最后一个字段在 t≈0.733 落位,之后 1.3s 只有整卡前推(且前推在 t=0.75 停止)——
末段有约 1.2s 的完全静止定格。这是给观众通读整卡的时间,别当成 bug 砍掉
- 三行正文用
white-space: nowrap写死不换行,卡宽 78%。换更长文案或中文时会溢出
被 overflow 裁掉,落地必须重新配字号与行长
- 所有内容都是占位("Sample Product Title"、
$249/$189、"Placeholder copy for…"、
三个纯色色卡),字段结构本身才是可复用的部分。换成真实商品数据后字段数量往往变化, 时间表要按新的字段分组重排间隔
- 高亮块的
t0 = F(30+li*2+4)比所在行的落位(F(30+li*2))晚 4 配方帧,两者是耦合
的偏移量;改行落位时间必须同步改高亮,否则出现"高亮先到、文字后来"
ACCENT/ACCENT_SOFT是全卡唯一色相来源(新价文字 + 两块高亮),换品牌色改这
两个变量即可;其余全是中性灰阶
参考实现
demos/ui-entrance/product-card-progressive-assemble/ (ProductCardProgressiveAssemble.tsx)
Mã nguồn tham chiếu (Remotion)
- demos/ui-entrance/product-card-progressive-assemble/ProductCardProgressiveAssemble.tsx — https://raw.githubusercontent.com/Vincentwei1021/video-shotcraft/main/demos/ui-entrance/product-card-progressive-assemble/ProductCardProgressiveAssemble.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/product-card-progressive-assemble.jsongit clone https://github.com/Vincentwei1021/video-shotcraft
# component: demos/ui-entrance/product-card-progressive-assemble/ProductCardProgressiveAssemble.tsx
npx remotion render <CompositionId> out.mp4claude mcp add --transport http gimgs https://gimgs.net/mcp
# then: search_motion_templates / get_motion_template("product-card-progressive-assemble")Prompt
Use the video-shotcraft shot recipe "Thẻ sản phẩm tự lắp dần" (Remotion composition, Apache-2.0) published on gimgs.net. Recipe JSON (prompt = timing/easing/parameter table/pitfalls, files = TSX source): https://gimgs.net/motion/product-card-progressive-assemble.json Raw TSX: https://gimgs.net/motion/product-card-progressive-assemble/template.html Repository: https://github.com/Vincentwei1021/video-shotcraft (component: demos/ui-entrance/product-card-progressive-assemble/ProductCardProgressiveAssemble.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/ui-entrance/product-card-progressive-assemble/ProductCardProgressiveAssemble.tsx =====
// product-card-progressive-assemble — Progressive Assemble 字段逐个落位(motion-lab 定稿转原生 Remotion)
// 详情卡像被逐字段抓取般自建:图→标题→breadcrumb pill 依次 pop→价格出现后被划线降级、
// 强调色新价 spring 跳出→正文逐行揭示+强调色高亮块由左向右刷过→色卡点亮。
// 整卡极慢 scale 前推保持呼吸。设计坐标 480×270(DesignStage 等比放大)。
import React from 'react';
import { DesignStage, E, lerp, seg, useT } from '../../_fixtures/Motion';
export const PRODUCT_CARD_PROGRESSIVE_ASSEMBLE_DURATION = 150; // 5000ms @30fps
const ACCENT = '#ff6a1f';
const ACCENT_SOFT = 'rgba(255,122,26,.42)';
const F = (f: number) => f / 60; // recipe 帧 → t
// 字段落位样式:rise = 上移 6px 淡入;pop = outBack 从 0.4 弹到 1
const fieldStyle = (t: number, f0: number, mode: 'rise' | 'pop' = 'rise'): React.CSSProperties => {
const k = seg(t, F(f0), F(f0) + 0.1, E.outCubic);
return {
opacity: Math.min(1, k * 2),
transform:
mode === 'pop'
? `scale(${lerp(E.outBack(Math.min(1, k)), 0.4, 1)})`
: `translateY(${lerp(k, 6, 0)}px)`,
};
};
// 描述行:[文本, 是否马克笔高亮]
const LINES: [string, boolean?][][] = [
[['Placeholder copy for '], ['a key highlight', true], [' in the']],
[['product body. '], ['Second highlight', true], [' sits here on']],
[['the third line of neutral sample text.']],
];
export const ProductCardProgressiveAssemble: React.FC = () => {
const t = useT();
// 整卡呼吸前推
const push = seg(t, 0, 0.75, E.outQuad);
// 价格降级(f=26):旧价划线缩小变灰,新价 spring 跳出
const cut = t >= F(26);
const nk = seg(t, F(26), F(26) + 0.12);
return (
<DesignStage bg="#0d0e13" raster="zoom">
<div
style={{
position: 'absolute',
left: '50%',
top: '50%',
width: '78%',
height: '74%',
transform: `translate(-50%,-50%) scale(${lerp(push, 1, 1.06)})`,
background: '#f6f5f2',
borderRadius: 12,
boxShadow: '0 18px 50px rgba(0,0,0,.5)',
fontFamily: '-apple-system,Helvetica,sans-serif',
display: 'flex',
padding: '4.5%',
boxSizing: 'border-box',
gap: '5%',
}}
>
{/* 左:商品图占位(f=0 rise 落位) */}
<div
style={{
flex: '0 0 38%',
background: '#e2e0da',
borderRadius: 8,
position: 'relative',
overflow: 'hidden',
...fieldStyle(t, 0),
}}
>
<div
style={{
position: 'absolute',
left: '18%',
top: '14%',
width: '64%',
height: '72%',
background: 'linear-gradient(160deg,#2b3040,#171a24)',
borderRadius: '40% 40% 14% 14%/26% 26% 10% 10%',
}}
>
{/* 拉链 */}
<div style={{ position: 'absolute', left: '49%', top: '20%', width: '2%', height: '58%', background: '#525a70' }} />
</div>
</div>
{/* 右列 */}
<div style={{ flex: 1, display: 'flex', flexDirection: 'column', minWidth: 0 }}>
{/* 标题(f=4) */}
<div style={{ fontSize: 19, fontWeight: 800, color: '#17181c', letterSpacing: '-.3px', ...fieldStyle(t, 4) }}>
Sample Product Title
</div>
{/* breadcrumb pills(f=8 起每 2 帧一个 pop) */}
<div style={{ display: 'flex', gap: 6, margin: '8px 0 10px' }}>
{['Category', 'Subgroup', 'Detail'].map((txt, i) => (
<div
key={txt}
style={{
fontSize: 9,
fontWeight: 600,
color: '#5a5e6b',
background: '#e9e7e1',
padding: '3px 9px',
borderRadius: 99,
...fieldStyle(t, 8 + i * 2, 'pop'),
}}
>
{txt}
</div>
))}
</div>
{/* 价格行(f=16):f=26 起旧价降级、新价 spring 跳出 */}
<div style={{ display: 'flex', alignItems: 'baseline', gap: 9, marginBottom: 11, ...fieldStyle(t, 16) }}>
<span
style={{
fontSize: cut ? 15 : 21,
fontWeight: 800,
color: cut ? '#9a9da6' : '#17181c',
textDecoration: cut ? 'line-through' : 'none',
}}
>
$249
</span>
<span
style={{
fontSize: 21,
fontWeight: 800,
color: ACCENT,
opacity: Math.min(1, nk * 3),
transform: `scale(${lerp(E.spring(nk, 0.35), 1.15, 1)})`,
}}
>
$189
</span>
</div>
{/* 描述行(f=30 起每 2 帧一行)+ 马克笔高亮(行 f0+4 起由左向右刷过) */}
<div style={{ fontSize: 10.5, lineHeight: 1.75, color: '#494d58' }}>
{LINES.map((segs, li) => (
<div key={li} style={{ whiteSpace: 'nowrap', ...fieldStyle(t, 30 + li * 2) }}>
{segs.map(([txt, isMark], si) =>
!isMark ? (
<span key={si}>{txt}</span>
) : (
<span key={si} style={{ position: 'relative', display: 'inline-block' }}>
<span
style={{
position: 'absolute',
left: -2,
right: -2,
top: '8%',
bottom: '4%',
background: ACCENT_SOFT,
borderRadius: 2,
transform: `scaleX(${seg(t, F(30 + li * 2 + 4), F(30 + li * 2 + 4) + 0.085, E.outCubic)})`,
transformOrigin: 'left center',
}}
/>
<span style={{ position: 'relative', fontWeight: 700, color: '#26282f' }}>{txt}</span>
</span>
),
)}
</div>
))}
</div>
{/* 色卡(f=40 起每 2 帧一个 pop) */}
<div style={{ display: 'flex', gap: 7, marginTop: 'auto' }}>
{['#191b20', '#8b8f99', '#3a5b8c'].map((cclr, i) => (
<div
key={cclr}
style={{
width: 16,
height: 16,
borderRadius: 4,
background: cclr,
outline: '1.5px solid rgba(0,0,0,.12)',
outlineOffset: 1.5,
...fieldStyle(t, 40 + i * 2, 'pop'),
}}
/>
))}
</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/ui-entrance/product-card-progressive-assemble/ProductCardProgressiveAssemble.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

Hiện hình cả bức tường: ba kiểu
Ba kiểu vào hình hàng loạt cả bức tường — bento thắp sáng từng ô, lưới lật sóng, và bản vẽ blueprint hiện nét; tất cả hiện hình tại chỗ không dịch chuyển, bổ trợ cho deck-deal-flyin kiểu bay vào dịch chuyển.

Cột nhập theo bậc thang giá trị
16 cột vào hình với delay là bậc thang thời gian, đồng thời chiều cao/sắc màu/dịch chuyển/độ mờ mỗi thuộc tính tự trải thành bậc thang giá trị từ đầu đến cuối; nhịp hai đổi gốc lệch nhịp về tâm, biên độ xung lấy tâm làm đỉnh trải lại.

Biến hình SVG lặp vòng
Một viền khép kín 140 điểm biến hình mượt sang viền khác rồi biến lại, hai hình trước hết được lấy mẫu lại cùng số điểm trong tọa độ cực, nội suy bán kính từng điểm + inOutCubic, giữa chừng chèn hơi thở scale nhẹ, tự xoay chậm và sắc màu trôi 185°→305°.

Hiện hình từ khung xương
Hiện hình ba tầng từ phác thảo đến khung xương đến nội dung — nét vẽ tay được thay bằng thanh xám khung xương rồi rõ nét thành UI thật.