Motion · UI entrance
Product card progressive assemble
A detail card self-assembles field by field — image, title, breadcrumb pill pop in, the old price strikes through, a new price springs in.
Source: video-shotcraft (Vincent Wei) ↗ · Apache-2.0 · Remotion composition (React/TSX) — preview video, no in-browser player
When to use
A capability showcase for a product/listing detail page; the hero shot for "structured extraction," "auto-fill," or "data growing itself" narratives
Intention
A detail card that just fades in reads to the viewer as "there's a card." Landing it field by field turns the card into a form being filled out live: each field lands on its own, and the viewer's attention is walked through the card's information structure — where's the image, where's the title, where's the price, where's the key line of copy. The price strikethrough downgrade is the film's only semantic event (not an "appearance" but a "change"), so it's placed at the most prominent point, mid-sequence.
Duration & energy
- Duration: About 5.0s (150f@30fps)
- Energy: Medium (dense, continuous small events with no single burst; a slow push sustains the sense of progress)
Shot recipe (original Chinese — timing, easing, parameter table, known pitfalls)
意图
一张详情卡直接淡入,观众只看到"有一张卡"。逐字段落位则把卡变成一份正在被填写的 表单:每个字段各自到位,观众的注意力被牵着走一遍卡的信息结构——图在哪、标题在哪、 价格在哪、正文重点在哪。价格划线降级是全片唯一的语义事件(不是"出现"而是"改变"), 所以它被安排在中段最显眼的位置。
动效核心
- 字段时间表用"配方帧"书写:
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)
Reference implementation (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
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/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 "Product card progressive assemble" (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 |
|---|---|---|---|
| — | |||
More in this category

Wall reveal: three moves
Three whole-wall batch entrances — bento cell-by-cell light-up, grid wave flip, and blueprint draw-on; all reveal in place with no displacement, complementing the fly-in, displacement-type deck-deal-flyin.

Value-stagger gradient bars
16 bars enter with delay as a time stagger, while height/hue/offset/blur each lay out as a value gradient from first to last; the second beat moves the stagger origin to center and re-lays the pulse peaking there.

SVG shape morph loop
A 140-point closed outline morphs smoothly into another and back; both shapes are resampled to equal point counts in polar coordinates with per-point radius interpolation and inOutCubic, plus a light scale breathing, slow rotation, and hue drifting 185°→305° mid-morph.

Skeleton reveal
Three-tier reveal from sketch to structure to content — hand-drawn doodles get swapped for skeleton bars, which resolve into real UI.