Motion · UI xuất hiện
Chồng thẻ danh sách ép nảy
Thẻ danh sách bay lên từ đáy khung hình lần lượt xếp chồng, mỗi lần chạm đất ép và bật cả chồng, đồng thời bộ đếm nhảy số đồng bộ.
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
Dùng cho cảnh feed/radar/hộp thư kiểu "mỗi ngày đều có thứ mới"; nhấn mạnh danh sách tài sản đang tích lũy liên tục.
Ý đồ
"Chồng lên có trọng lượng": mỗi thẻ mới rơi lên khiến cả chồng đã ổn định bị ép xuống rồi bật lại — khán giả đọc được "đây là thứ tích lũy thật" từ phản hồi vật lý đó. Bộ đếm nhảy số đồng bộ chốt chặt cảm giác về số lượng.
Thời lượng & năng lượng
- Thời lượng: Khoảng 3s (18–88f)
- Năng lượng: Trung bình
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)
意图
"堆叠有重量":每张新卡落上来,已落定的整摞被压下再弹回——观众从物理反馈里读出"这是实打实攒下来的东西"。计数器同步跳格把数量感钉死。
动效核心
- 预备拍(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)
Mã nguồn tham chiếu (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
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/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 "Chồng thẻ danh sách ép nảy" (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 |
|---|---|---|---|
| — | |||
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.