Motion · Entrada de UI
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.
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
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.
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/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 |
|---|---|---|---|
| — | |||
Mais nesta categoria

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.