Motion · UI entrance
List stack press
List cards fly up from the bottom of the frame one by one to stack up, each landing squashing and rebounding the whole pile while a counter ticks up in sync.
Source: video-shotcraft (Vincent Wei) ↗ · Apache-2.0 · Remotion composition (React/TSX) — preview video, no in-browser player
When to use
For feed/radar/inbox shots that say "something new every day"; emphasizes a continuously accumulating asset list.
Intention
"Stacking has weight": each new card landing presses the settled pile down and lets it rebound — the audience reads "this was genuinely accumulated" from the physical feedback. The counter ticking in sync locks in the sense of quantity.
Duration & energy
- Duration: About 3s (18–88f)
- Energy: Medium
Shot recipe (original Chinese — timing, easing, parameter table, known pitfalls)
意图
"堆叠有重量":每张新卡落上来,已落定的整摞被压下再弹回——观众从物理反馈里读出"这是实打实攒下来的东西"。计数器同步跳格把数量感钉死。
动效核心
- 预备拍(anticipation,借鉴迪士尼法则 2):首张升入前给一拍段落级预备——计数器先于首卡 4–6f 亮起归零/微缩 scale(0.96)→1,把视线引到堆叠区,再让首卡入画。只做段落级一次,不逐卡加
- 卡从画面下方升入,交替微倾斜收平,等距节拍逐张摞起
- 每张到场压下整摞 6px、8f 弹回(stackPress 脉冲——即法则 5 的 follow-through:新卡的动能传给整摞再缓冲耗散,这一笔就是"有重量"的来源)
- 落地后按拖拽层级依次收尾(法则 5):卡体先停 → 整摞弹回 → 高亮条再长出(滞后 2–4f)——三者同帧发生读作贴图切换
- 收尾一道 glaze 扫光掠过整摞
- 屏幕空间 DigitRoll 计数器落一张滚一格;相机跟随堆叠向下
参数表
| 参数 | 典型值 | 调节手感 | |------|--------|----------| | 节拍 | CUES=[18,30,42,54,66](12f 等距),每张飞 22f,bezier(0.45,0.05,0.25,1.12) 末端过冲 | 少量卡(5 张)可以等距;大批量入场才需要递加速(对照 deck-deal 的 R2) | | 入场 | 从下方 600px 升入,交替 ±2° tilt 收平,scale 1.06→1 | 交替倾斜打破对称;同向倾斜读作复制粘贴 | | 堆叠压感 | 后一张到场时整摞压下 6px、8f 弹回(脉冲 [cue,cue+4,cue+8]→[0,6,0]) | 这是"有重量"的关键一笔,砍掉后堆叠变成简单排版 | | 阴影 | 空中 0 32px 64px、落定 0 2px 8px | 阴影随高度收敛,和 deck-deal/spotlight 同一套语言 | | 联动高亮 | 落地后滞后 2–4f 于 72% 高度处长出 40% 宽强调色(模板片为琥珀)底色(7f 长 + 5f 淡);最后一张离切点仅 ~12f 需压缩节拍 | 高亮是次级动作(法则 5/8):在主动作(落地压弹)之后跟进,不与之同帧抢戏;末尾元素的节拍要按剩余帧预算压缩,不能照抄前面的 | | glaze 扫光 | 420px 宽、rotate 14°、mixBlendMode overlay,帧 82→96 从 −700 扫到 2600 | 扫光全镜头只此一次、扫整摞不逐卡(Q4) | | 计数器(digit-roll 手法) | 连续里程计位置 pos:每张卡落地让 pos 沿数字带 +1 格,单格 8f 滚定,bezier(0.25,0.8,0.25,1);滚动量=pos·lineH 沿双份 0-9 带连续下滚 | 计数与落地不同步立刻穿帮;单格滚动必须短于落卡间距(12f)且尾格在镜头收尾前滚定——以 count 为 key 重挂重滚的写法每次都要从 0 滚 22f,12f 节拍下前一次永远滚不完、尾帧停在 4(PR #40 判例);数字带双份拼接+overflow hidden+tabular-nums(不开 tabular 会横向抖) | | 相机 | zoom 1.35→0.9 五键跟随堆叠向下 | 正视机位(见已知坑),相机只做纵向跟随 |
声音
每张落地不逐张配长音;计数落定钉 click-camera(模板片钉在 f648)、段落切入钉 transition-soft(f623)。若逐张配 pop 参照 S2 三招防机枪。
已知坑
- 堆叠/列表信息镜头必须正视(Q6)——全片倾斜化时本镜头曾被单独回滚回正视;风格化机位逐镜头验证,不全局套用
- 逐卡 glint 高亮扫过被否两次(Q4),高亮用静态强调色底色条即可,动感交给压弹
- 与 history-list-stack(document-typewriter-reveal 卡尾段)是同一动作词汇——全片同一手法只当一次主角(P4),两处并用时一处必须降为配角
- anticipation/拖拽层级参数借鉴自迪士尼 12 法则(pixel2motion skill 内置提炼版),非用户判例——是默认建议不是令;与判例冲突时判例优先
- 预备动作幅度必须过肉眼阈值(deck-deal 判例 2026-07-09:小幅度用户完全无感,放大 12 倍才通过)——渲染后自查"不逐帧能否看出蓄力"
参考实现
demos/ui-entrance/list-stack-press/ListStackPress.tsx(原 template/src/aifl/live/ScenePapers.tsx)
Reference implementation (Remotion)
- demos/ui-entrance/list-stack-press/ListStackPress.tsx — https://raw.githubusercontent.com/Vincentwei1021/video-shotcraft/main/demos/ui-entrance/list-stack-press/ListStackPress.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/list-stack-press.jsongit clone https://github.com/Vincentwei1021/video-shotcraft
# component: demos/ui-entrance/list-stack-press/ListStackPress.tsx
npx remotion render <CompositionId> out.mp4claude mcp add --transport http gimgs https://gimgs.net/mcp
# then: search_motion_templates / get_motion_template("list-stack-press")Prompt
Use the video-shotcraft shot recipe "List stack press" (Remotion composition, Apache-2.0) published on gimgs.net. Recipe JSON (prompt = timing/easing/parameter table/pitfalls, files = TSX source): https://gimgs.net/motion/list-stack-press.json Raw TSX: https://gimgs.net/motion/list-stack-press/template.html Repository: https://github.com/Vincentwei1021/video-shotcraft (component: demos/ui-entrance/list-stack-press/ListStackPress.tsx; imports demos/_fixtures and assets/lib) Spec: 960x540, 30 fps, 3.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/list-stack-press/ListStackPress.tsx =====
// list-stack-press —— 列表卡从底部逐张飞上摞起,每张落地压弹整摞、计数器同步跳格
// "堆叠有重量":每张新卡落上来,已落定的整摞被压下再弹回——物理反馈里读出
// "这是实打实攒下来的东西"。参考实现从 template ScenePapers 剥离:
// 预备拍(计数器先于首卡 4–6f 亮起微缩,把视线引到堆叠区);5 卡 12f 等距节拍
// 从底部 600px 升入(交替 ±2° 倾斜收平 + scale 1.06→1);后一张到场时整摞压下
// 6px、8f 弹回(stackPress 脉冲——"有重量"的关键一笔);落地后高亮条滞后 2–4f
// 长出;收尾一道 glaze 扫光掠过整摞;屏幕空间 DigitRoll 计数器落一张滚一格。
// 正视机位(堆叠/列表镜头必须正视,Q6),相机跟随堆叠向下。
import { AbsoluteFill, Img, interpolate, staticFile, useCurrentFrame, Easing } from 'remotion';
import { PageCam2D, CamKey2D } from '../../_fixtures/PageCam2D';
import layout from '../../_textures/live-layout.json';
export const LIST_STACK_PRESS_DURATION = 88; // 18–88f 落在 shot 内 offset 0
const cards = layout.papers.cards;
const PAGE_H = layout.papers.pageH;
const MONO = 'ui-monospace, SFMono-Regular, Menlo, monospace';
const AMBER = 'oklch(52% 0.115 65)';
const FILES = ['paper1.png', 'paper2.png', 'paper3.png', 'paper4.png', 'paper5.png'];
const CUES = [6, 18, 30, 42, 54]; // 首卡前留 6f 给计数器预备拍(anticipation)
const DUR = 22;
const FLY_EASE = Easing.bezier(0.45, 0.05, 0.25, 1.12);
const TILTS = [2, -2, 2, -2, 2];
// 预备拍:计数器先于首卡亮起 + 微缩 (0.96→1),把视线引到堆叠区(段落级一次)
const ANTICIPATE_START = 0;
const ANTICIPATE_END = 6;
const CAM_KEYS: CamKey2D[] = [
{ frame: 0, cx: 960, cy: 270, zoom: 1.35 },
{ frame: 12, cx: 960, cy: 330, zoom: 1.15 },
{ frame: 18, cx: 960, cy: 520, zoom: 1.02 },
{ frame: 56, cx: 960, cy: 820, zoom: 0.95 },
{ frame: 82, cx: 960, cy: 860, zoom: 0.9 },
];
// odometer digit column —— 连续里程计,不重挂:`pos` 是数字带上的连续位置
// (0→5),每张卡落地让 pos 多滚进一格。单格滚动必须短于 12f 落卡间距、
// 且最后一格在镜头结束前落定——key 重挂从 0 重滚 22f 的写法两条都踩
// (前一格永远滚不完、尾帧停在 4)。
const DIGITS = '0123456789';
const ROLL_DUR = 8; // 单格滚动帧数,< 12f 落卡间距
const DigitRoll: React.FC<{ pos: number; lineH: number; color: string }> = ({ pos, lineH, color }) => (
<span style={{ display: 'inline-flex', overflow: 'hidden', height: lineH, verticalAlign: 'bottom' }}>
<span style={{ display: 'inline-block', height: lineH }}>
<span style={{ display: 'block', transform: `translateY(${-pos * lineH}px)` }}>
{(DIGITS + DIGITS).split('').map((d, j) => (
<span key={j} style={{ display: 'block', fontSize: 96, lineHeight: `${lineH}px`, color, fontVariantNumeric: 'tabular-nums' }}>
{d}
</span>
))}
</span>
</span>
</span>
);
export const ListStackPress: React.FC = () => {
const frame = useCurrentFrame();
// 计数器连续位置:每张卡落地(cue+DUR)时向前滚一格,8f 滚定。
// 最后一张 f76 落地、f84 滚定,早于 f88 镜头结束。
let rollPos = 0;
for (const c of CUES) {
rollPos += interpolate(frame, [c + DUR, c + DUR + ROLL_DUR], [0, 1], {
extrapolateLeft: 'clamp', extrapolateRight: 'clamp', easing: Easing.bezier(0.25, 0.8, 0.25, 1),
});
}
// 预备拍:计数器 0→6f 从 scale 0.96 微缩回 1 并亮起,视线先引到堆叠区
const antT = interpolate(frame, [ANTICIPATE_START, ANTICIPATE_END], [0, 1], {
extrapolateLeft: 'clamp', extrapolateRight: 'clamp', easing: Easing.out(Easing.quad),
});
const counterScale = 0.96 + 0.04 * antT;
const counterOpacity = 0.3 + 0.7 * antT;
// when a *later* card enters, the settled stack gets pressed down 6px then
// springs back over ~8 frames
const stackPress = (settledIndex: number) => {
let press = 0;
for (let j = settledIndex + 1; j < CUES.length; j++) {
const cue = CUES[j];
const p = interpolate(frame, [cue, cue + 4, cue + 8], [0, 6, 0], {
extrapolateLeft: 'clamp', extrapolateRight: 'clamp',
});
press = Math.max(press, p);
}
return press;
};
// glaze sweep once everything has landed (~78)
const glazeX = interpolate(frame, [64, 78], [-700, 2600], {
extrapolateLeft: 'clamp', extrapolateRight: 'clamp', easing: Easing.bezier(0.45, 0, 0.35, 1),
});
const glazeVis = interpolate(frame, [63, 68, 74, 77], [0, 1, 1, 0], {
extrapolateLeft: 'clamp', extrapolateRight: 'clamp',
});
return (
<AbsoluteFill style={{ backgroundColor: '#faf7f2' }}>
<PageCam2D src="textures/live/papers-full.png" pageH={PAGE_H} keys={CAM_KEYS}>
{/* cover the printed cards so blank slots stack in */}
{cards.map((c, i) => (
<div
key={`slot-${i}`}
style={{
position: 'absolute', left: c.x - 12, top: c.y - 10,
width: c.w + 24, height: c.h + 20, background: '#faf7f2',
opacity: frame >= CUES[i] + DUR - 2 ? 0 : 1,
}}
/>
))}
{cards.map((c, i) => {
const cue = CUES[i];
const t = interpolate(frame, [cue, cue + DUR], [0, 1], {
extrapolateLeft: 'clamp', extrapolateRight: 'clamp', easing: FLY_EASE,
});
if (t <= 0) return null;
const settled = t >= 0.999;
const dy = 600 * (1 - t) + (settled ? stackPress(i) : 0);
const rot = TILTS[i] * (1 - t);
const scale = 1.06 - 0.06 * t;
const shadow = settled
? '0 2px 8px rgba(60,45,30,.10)'
: `0 32px 64px rgba(60,45,30,${0.22 * (1 - t) + 0.06})`;
// amber highlight over the linked-project name, after landing
const hlStart = cue + DUR;
const hlGrow = interpolate(frame, [hlStart, hlStart + 7], [0, 1], {
extrapolateLeft: 'clamp', extrapolateRight: 'clamp', easing: Easing.bezier(0.3, 0, 0.2, 1),
});
const hlFade = interpolate(frame, [hlStart + 7, hlStart + 12], [1, 0], {
extrapolateLeft: 'clamp', extrapolateRight: 'clamp',
});
return (
<div
key={FILES[i]}
style={{
position: 'absolute', left: c.x, top: c.y, width: c.w, height: c.h,
transform: `translateY(${dy}px) rotate(${rot}deg) scale(${scale})`,
transformOrigin: 'center center', boxShadow: shadow, borderRadius: 12,
}}
>
<Img src={staticFile(`textures/live/${FILES[i]}`)} style={{ position: 'absolute', inset: 0, width: '100%', height: '100%', display: 'block' }} />
{hlGrow > 0 && hlFade > 0 ? (
<div
style={{
position: 'absolute', left: 20, top: c.h * 0.72,
width: `${hlGrow * 40}%`, height: 30,
background: 'oklch(97% 0.028 85)', opacity: 0.6 * hlFade,
borderRight: `2px solid ${AMBER}`, pointerEvents: 'none',
}}
/>
) : null}
</div>
);
})}
{/* page-space glaze sweep at the end */}
<div
style={{
position: 'absolute', top: 40, height: PAGE_H - 80, left: glazeX, width: 420,
transform: 'rotate(14deg)', opacity: glazeVis * 0.5, mixBlendMode: 'overlay',
background: 'linear-gradient(90deg, transparent, rgba(255,240,214,0.9) 45%, rgba(255,240,214,0.9) 55%, transparent)',
pointerEvents: 'none',
}}
/>
</PageCam2D>
{/* screen-space counter, top-right — lands one digit per card */}
<div
style={{
position: 'absolute', top: 70, right: 96, textAlign: 'right',
pointerEvents: 'none', opacity: counterOpacity,
transform: `scale(${counterScale})`, transformOrigin: 'top right',
}}
>
<div style={{ fontFamily: MONO, fontSize: 24, letterSpacing: '0.16em', color: 'oklch(50% 0.006 82)', textTransform: 'uppercase' }}>
Paper Radar
</div>
<div style={{ display: 'flex', justifyContent: 'flex-end', marginTop: 6 }}>
<DigitRoll pos={rollPos} lineH={96 * 1.15} color={AMBER} />
</div>
<div style={{ fontFamily: MONO, fontSize: 20, letterSpacing: '0.12em', color: 'oklch(50% 0.006 82)', marginTop: 4, textTransform: 'uppercase' }}>
Of 31 Fetched Today
</div>
</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/ui-entrance/list-stack-press/ListStackPress.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.