Motion · UI entrance
Paper craft moves
Two paper-craft techniques: masking-tape-slap pins a card down with tape slaps, and popup-book-rise stands cards up like a pop-up book.
Source: video-shotcraft (Vincent Wei) ↗ · Apache-2.0 · Remotion composition (React/TSX) — preview video, no in-browser player
Variants
Sway = amplitude envelope × sine (rot ±1.5°/bob ±5px); the tape swoops in over 6f: scale 1.45→1 plus rotate undershoots 16°→overshoots 7°→settles, with a single-frame scaleY 0.72 squash on landing; the torn edge uses a 14-point clipPath jag.
Settling entrance for a single card/badge; aligned with a copy emphasis beat
Two-layer 3D: the scene sits at rotateX 75° overhead (perspective 2600); each card springs from rotateX 0→-90° (damping 11, overshooting to -95°) with its transform-origin on the base edge, preserve-3d carried through; back rows go first, front rows follow, staggered by 7f.
Full-dashboard opening; "the system is being built"
When to use
Physical material language for paper-and-ink hero visuals: use A for a single card's settling entrance, B for a full dashboard opening build; naturally paired with paper/ink + accent-color hero visuals (paper/ink/amber template films)
Intention
A UI translation of Wes Anderson notebook art and pop-up book paper craft. A: a card floats lightly into place and hangs there swaying slightly (unsecured paper), two torn strips of translucent tape slap down one after another on opposite corners, and on the second slap the card simultaneously stops swaying, its shadow thins, and it settles down — the instant of being "pinned down" is the star, and the two slap sounds are a natural audio cue. B: the whole page lies flat like an open book seen from above, and cards like paper pieces glued to the page stand up one after another along their base edges in a staggered wave to form a wall, overshooting to 95° and springing back to 90° (paper's springiness), with the shadow at the root narrowing as it stands up.
Duration & energy
- Duration: A 3–4s / B 4–5.5s
- Energy: A Medium (a two-beat hit) / B Medium-high (standing up the wall has depth impact)
Shot recipe (original Chinese — timing, easing, parameter table, known pitfalls)
意图
Wes Anderson 手账美术与立体书纸艺的 UI 翻译。A:卡片轻飘入位后 悬着微晃(未固定的纸),两条撕边半透明胶带"啪、啪"先后拍在对角, 第二条拍下同帧卡片停晃、投影变薄、整卡下沉——"按死"的定妆 一瞬是主角,两声"啪"是天然音效点。B:整页平躺如摊开书页(俯视), 卡片像贴在页上的纸片沿各自底边错峰立起成墙,立到 95° 回弹 90° (纸的韧性),根部投影随立起收窄。
两式选型
| 式 | 做法 | 适用 | |----|------|------| | A masking-tape-slap | 晃动=幅度包络×正弦(rot ±1.5°/bob ±5px);胶带扑入 6f:scale 1.45→1 + rotate 欠 16°→过冲 7°→回正 + 落帧 scaleY 0.72 一帧压扁;撕边 14 点 clipPath 锯齿 | 单卡/徽章的定妆入场;文案重音对齐 | | B popup-book-rise | 双层 3D:场景 rotateX 75° 俯视(persp 2600),每卡 rotateX 0→-90° spring(damping 11 过冲 -95°),origin 底边,preserve-3d 贯通;远排先近排后错峰 7f | 整版 dashboard 开场;"系统被搭建起来" |
参数表
| 参数 | 典型值 | 调节手感 | |------|--------|----------| | A 按死三件套 | 同帧:晃动 2f 归零 + 投影 34px→8px + 全卡下沉 2px | 三件拆帧就没有"按死"感;第一条胶带只把包络衰到 0.45(半死) | | A 胶带质感 | 半透明 + 锯齿撕边 + 落点故意歪 2–3° | 歪斜是设计的手工感;直挺挺读作 UI 元素 | | B 立起方向 | rotateX 0→-90°(向观众立起) | demo 判例:方向写反卡片往画面里倒被底板裁切 | | B 根部投影 | blur 矩形不随卡立起,高 104→14px、透明度 0.26→0.1 随角度插值 | 无投影纸片浮空;投影跟着立起来穿帮 | | B 收尾 | 全部站定后场景 75°→68° 轻回正(in-out cubic) | 直接定住少一口呼吸 |
已知坑
- demo 在灰阶/占位素材上调校通过——参数是调校起点非实战定稿,
首次实战须以真实素材回验
- A 的晃动是"未固定"的叙事铺垫不是手持抖动(Q3 边界):
振幅包络必须最终归零,全片只在被按死前存在
- B 与 tilt-reveal 分工:tilt 是机位抬头看静止页面,
本卡机位基本不动、构件自己站起来
- B 立墙后卡片呈 90° 侧立态,文字不可读——立墙是构图动作,
信息交给立起后的正视段落(Q6 同源)
- 声音:A 两声"啪"(纸拍击拟音);B 每排立起一声轻纸响,
错峰对齐(S2/S4 同源)
参考实现
demos/ui-entrance/paper-craft-moves/ (MaskingTapeSlap.tsx / PopupBookRise.tsx)
Reference implementation (Remotion)
- demos/ui-entrance/paper-craft-moves/MaskingTapeSlap.tsx — https://raw.githubusercontent.com/Vincentwei1021/video-shotcraft/main/demos/ui-entrance/paper-craft-moves/MaskingTapeSlap.tsx
- demos/ui-entrance/paper-craft-moves/PopupBookRise.tsx — https://raw.githubusercontent.com/Vincentwei1021/video-shotcraft/main/demos/ui-entrance/paper-craft-moves/PopupBookRise.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/paper-craft-moves.jsongit clone https://github.com/Vincentwei1021/video-shotcraft
# component: demos/ui-entrance/paper-craft-moves/MaskingTapeSlap.tsx, demos/ui-entrance/paper-craft-moves/PopupBookRise.tsx
npx remotion render <CompositionId> out.mp4claude mcp add --transport http gimgs https://gimgs.net/mcp
# then: search_motion_templates / get_motion_template("paper-craft-moves")Prompt
Use the video-shotcraft shot recipe "Paper craft moves" (Remotion composition, Apache-2.0) published on gimgs.net. Recipe JSON (prompt = timing/easing/parameter table/pitfalls, files = TSX source): https://gimgs.net/motion/paper-craft-moves.json Raw TSX: https://gimgs.net/motion/paper-craft-moves/template.html Repository: https://github.com/Vincentwei1021/video-shotcraft (component: demos/ui-entrance/paper-craft-moves/MaskingTapeSlap.tsx, demos/ui-entrance/paper-craft-moves/PopupBookRise.tsx; imports demos/_fixtures and assets/lib) Spec: 1920x1080, 30 fps, 4.72s 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/paper-craft-moves/MaskingTapeSlap.tsx =====
// masking-tape-slap —— 纸胶带拍定
// 一张 Card 轻飘入位后悬着微晃(±1.5° 正弦 + 5px 上下浮),两条半透明纸胶带
// 先后从画外拍在对角(scale 1.45→1 + rotate 过冲 + 一帧压扁)。第一条拍下晃动减半,
// 第二条拍下同帧卡片停晃、投影瞬间变薄、整卡 2px 下沉——"按死"的一瞬是主角。
// 帧确定性:全部由 frame 派生,无随机。收尾 f86 后真静止 54f。
import React from 'react';
import { useCurrentFrame, interpolate, Easing } from 'remotion';
import { G, Card, TitleBlock } from '../../_fixtures/Fixtures';
const CARD_W = 560;
const CARD_H = 350;
const CX = (1920 - CARD_W) / 2;
const CY = (1080 - CARD_H) / 2 + 40;
const FLOAT_START = 12; // 开头 12f 空场静置
const FLOAT_END = 38;
const SLAP1 = 58;
const SLAP2 = 82;
const APPROACH = 6; // 胶带从画外扑向卡面的帧数
const FREEZE = 2; // 拍死后晃动归零帧数
// 悬浮晃动幅度包络:入位后升起 → 第一条胶带拍下后减半 → 第二条拍下冻结(由外层处理)
const amp = (f: number): number => {
const rise = interpolate(f, [FLOAT_END, FLOAT_END + 8], [0, 1], {
extrapolateLeft: 'clamp',
extrapolateRight: 'clamp',
});
const damp = interpolate(f, [SLAP1, SLAP1 + 4], [1, 0.45], {
extrapolateLeft: 'clamp',
extrapolateRight: 'clamp',
});
return rise * damp;
};
const rawRot = (f: number): number => amp(f) * 1.5 * Math.sin((f - FLOAT_END) * 0.16);
const rawBob = (f: number): number => amp(f) * 5 * Math.sin((f - FLOAT_END) * 0.11);
// 第二条拍下(SLAP2)同帧起 2f 内把晃动按死到 0
const frozen = (f: number, raw: (x: number) => number): number =>
f <= SLAP2
? raw(f)
: interpolate(f, [SLAP2, SLAP2 + FREEZE], [raw(SLAP2), 0], {
extrapolateRight: 'clamp',
});
const Tape: React.FC<{
frame: number;
land: number;
cx: number; // 胶带中心点(世界坐标)
cy: number;
rot: number; // 落定角度
fromX: number; // 画外来向偏移
fromY: number;
}> = ({ frame, land, cx, cy, rot, fromX, fromY }) => {
if (frame < land - APPROACH) return null; // 条件卸载:拍上前真不存在
const t = interpolate(frame, [land - APPROACH, land], [0, 1], {
easing: Easing.out(Easing.cubic),
extrapolateLeft: 'clamp',
extrapolateRight: 'clamp',
});
const scale = interpolate(t, [0, 1], [1.45, 1]);
const dx = fromX * (1 - t);
const dy = fromY * (1 - t);
const opacity = interpolate(frame, [land - APPROACH, land - APPROACH + 2], [0, 0.85], {
extrapolateRight: 'clamp',
});
// rotate 过冲:来时欠 16° → 落帧过 7° → 4f 内回正
const r = interpolate(frame, [land - APPROACH, land, land + 4], [rot - 16, rot + 7, rot], {
easing: Easing.out(Easing.quad),
extrapolateLeft: 'clamp',
extrapolateRight: 'clamp',
});
// 一帧压扁:落帧 scaleY 0.72,次帧 0.9,随后复原
const sy = frame === land ? 0.72 : frame === land + 1 ? 0.9 : 1;
return (
<div
style={{
position: 'absolute',
left: cx - 160,
top: cy - 34,
width: 320,
height: 68,
transform: `translate(${dx}px, ${dy}px) rotate(${r}deg) scale(${scale}) scaleY(${sy})`,
transformOrigin: '50% 50%',
opacity,
background:
'linear-gradient(90deg, rgba(214,212,206,0.95) 0%, rgba(226,224,218,0.95) 30%, rgba(212,210,204,0.95) 60%, rgba(222,220,214,0.95) 100%)',
// 撕边:两端锯齿
clipPath:
'polygon(0% 8%, 2.5% 0%, 97% 3%, 100% 12%, 98.2% 30%, 100% 52%, 98% 74%, 100% 90%, 96.5% 100%, 3% 97%, 0% 88%, 1.8% 64%, 0% 42%, 2% 22%)',
boxShadow: '0 1px 3px rgba(0,0,0,0.15)',
}}
/>
);
};
export const MaskingTapeSlap: React.FC = () => {
const frame = useCurrentFrame();
// 卡片飘入:从上方 -120px 缓落
const floatY = interpolate(frame, [FLOAT_START, FLOAT_END], [-120, 0], {
easing: Easing.out(Easing.cubic),
extrapolateLeft: 'clamp',
extrapolateRight: 'clamp',
});
const floatOp = interpolate(frame, [FLOAT_START, FLOAT_START + 10], [0, 1], {
extrapolateLeft: 'clamp',
extrapolateRight: 'clamp',
});
const rot = frozen(frame, rawRot);
const bob = frozen(frame, rawBob);
// 按死:2px 下沉 + 投影瞬间变薄
const sink = interpolate(frame, [SLAP2, SLAP2 + FREEZE], [0, 2], {
extrapolateLeft: 'clamp',
extrapolateRight: 'clamp',
});
const shOff = interpolate(frame, [SLAP2, SLAP2 + FREEZE], [16, 3], {
extrapolateLeft: 'clamp',
extrapolateRight: 'clamp',
});
const shBlur = interpolate(frame, [SLAP2, SLAP2 + FREEZE], [34, 8], {
extrapolateLeft: 'clamp',
extrapolateRight: 'clamp',
});
const shAlpha = interpolate(frame, [SLAP2, SLAP2 + FREEZE], [0.22, 0.1], {
extrapolateLeft: 'clamp',
extrapolateRight: 'clamp',
});
return (
<div style={{ width: 1920, height: 1080, background: G.bg, position: 'relative', overflow: 'hidden' }}>
<div style={{ position: 'absolute', top: 110, width: '100%', textAlign: 'center' }}>
<TitleBlock text="MASKING TAPE SLAP" size={72} />
</div>
<div
style={{
position: 'absolute',
left: CX,
top: CY,
transform: `translateY(${floatY + bob + sink}px) rotate(${rot}deg)`,
transformOrigin: '50% 50%',
opacity: floatOp,
}}
>
<Card
w={CARD_W}
h={CARD_H}
seed={3}
style={{ boxShadow: `0 ${shOff}px ${shBlur}px rgba(0,0,0,${shAlpha})` }}
/>
</div>
{/* 两条胶带钉在卡片对角(世界坐标,卡片在其下滑动微晃) */}
<Tape frame={frame} land={SLAP1} cx={CX + 55} cy={CY + 40} rot={-45} fromX={-170} fromY={-130} />
<Tape frame={frame} land={SLAP2} cx={CX + CARD_W - 55} cy={CY + CARD_H - 40} rot={-45} fromX={170} fromY={130} />
</div>
);
};
// ===== demos/ui-entrance/paper-craft-moves/PopupBookRise.tsx =====
// popup-book-rise —— 立体书立起
// FakeDashboard(A) 打平躺下(场景 rotateX 75° 透视俯视),6 张卡片是贴在页上的
// 纸片,沿各自底边从平躺错峰立起(rotateX 90°→-5° 过冲→0° 回弹,即立到 95° 再回 90°),
// 根部投影随立起角度收窄变淡。全部立起后整个场景轻微回正(75°→68°)收尾。
// 卡片用独立网格复刻 dashboard A 区布局(Fixtures 内嵌卡无法单独驱动)。
// 收尾 f108 后真静止 ≥52f。帧确定性:全由 frame 派生。
import React from 'react';
import { useCurrentFrame, interpolate, spring, Easing } from 'remotion';
import { G, Card } from '../../_fixtures/Fixtures';
const FPS = 30;
const HOLD = 14; // 开头静置
const STAGGER = 7;
const RISE_DUR = 34; // spring 视觉收敛帧数
const LAST_START = HOLD + 5 * STAGGER; // 49
const SETTLE = LAST_START + RISE_DUR; // 83:全部立起
const REST = SETTLE + 25; // 108:场景回正完成
// dashboard A 区几何(照抄 FakeDashboard:侧栏 220 + 顶栏 72 + padding 36 + gap 28)
const AREA_X = 220 + 36;
const AREA_Y = 72 + 36;
const AREA_W = 1920 - 220 - 72;
const AREA_H = 1080 - 72 - 72;
const GAP = 28;
const CELL_W = (AREA_W - 2 * GAP) / 3;
const CELL_H = (AREA_H - GAP) / 2;
const PageCard: React.FC<{ i: number; frame: number }> = ({ i, frame }) => {
const col = i % 3;
const row = Math.floor(i / 3);
// 远排(row 0)先立,近排后立;同排从左到右
const order = row === 0 ? col : 3 + col;
const start = HOLD + order * STAGGER;
const s = spring({
frame: frame - start,
fps: FPS,
config: { damping: 11, stiffness: 130, mass: 0.9 },
durationInFrames: RISE_DUR,
durationRestThreshold: 0.0001,
});
// 平躺(贴页面 = local 0°)→ 立起(垂直页面 = local -90°,顶边朝观众翻起),
// spring 过冲自然冲过 -90° 到约 -95°(纸的韧性)再回弹。
const rx = interpolate(s, [0, 1], [0, -90]);
// 根部投影:躺平时长影(卡片盖在页面上),立起后收成窄条
const lie = 1 - Math.min(Math.abs(rx) / 90, 1); // 1 = 躺平, 0 = 立直
const shH = 14 + 90 * Math.max(lie, 0);
const shAlpha = 0.1 + 0.16 * Math.max(lie, 0);
return (
<div
style={{
position: 'absolute',
left: AREA_X + col * (CELL_W + GAP),
top: AREA_Y + row * (CELL_H + GAP),
width: CELL_W,
height: CELL_H,
transformStyle: 'preserve-3d',
}}
>
{/* 根部投影贴在页面上,不随卡片立起 */}
<div
style={{
position: 'absolute',
left: 6,
right: 6,
bottom: -4,
height: shH,
background: `rgba(0,0,0,${shAlpha})`,
borderRadius: 12,
filter: 'blur(10px)',
}}
/>
<div
style={{
position: 'absolute',
inset: 0,
transform: `rotateX(${rx}deg)`,
transformOrigin: '50% 100%',
backfaceVisibility: 'hidden',
}}
>
<Card w={0} h={0} seed={i + 1} style={{ width: '100%', height: '100%' }} />
</div>
</div>
);
};
export const PopupBookRise: React.FC = () => {
const frame = useCurrentFrame();
// 场景(书页)俯视角:全程 75°,全部立起后轻微回正到 68°
const sceneRx = interpolate(frame, [SETTLE, REST], [75, 68], {
easing: Easing.inOut(Easing.cubic),
extrapolateLeft: 'clamp',
extrapolateRight: 'clamp',
});
return (
<div style={{ width: 1920, height: 1080, background: '#dddddb', position: 'relative', overflow: 'hidden' }}>
<div style={{ position: 'absolute', inset: 0, perspective: 2600, perspectiveOrigin: '50% 30%' }}>
<div
style={{
position: 'absolute',
inset: 0,
transform: `translateY(-40px) rotateX(${sceneRx}deg)`,
transformOrigin: '50% 62%',
transformStyle: 'preserve-3d',
}}
>
{/* 书页底板:dashboard 的壳(侧栏+顶栏+空白页面) */}
<div style={{ position: 'absolute', inset: 0, background: G.bg, boxShadow: '0 40px 80px rgba(0,0,0,0.25)' }}>
<div style={{ position: 'absolute', left: 0, top: 0, bottom: 0, width: 220, background: G.side, padding: '28px 22px', boxSizing: 'border-box', display: 'flex', flexDirection: 'column', gap: 18 }}>
<div style={{ width: 40, height: 40, borderRadius: 10, background: '#777775' }} />
{Array.from({ length: 7 }).map((_, i) => (
<div key={i} style={{ height: 12, width: `${60 + ((i * 29) % 35)}%`, background: G.sideBar, borderRadius: 6 }} />
))}
</div>
<div style={{ position: 'absolute', left: 220, right: 0, top: 0, height: 72, background: G.panel, borderBottom: `2px solid ${G.line}`, display: 'flex', alignItems: 'center', padding: '0 32px', gap: 20, boxSizing: 'border-box' }}>
<div style={{ height: 18, width: 180, background: G.bar, borderRadius: 9 }} />
<div style={{ marginLeft: 'auto', height: 36, width: 320, background: '#fff', border: `2px solid ${G.line}`, borderRadius: 18, boxSizing: 'border-box' }} />
<div style={{ width: 36, height: 36, borderRadius: 18, background: G.mid }} />
</div>
</div>
{/* 6 张纸片卡沿底边立起 */}
{Array.from({ length: 6 }).map((_, i) => (
<PageCard key={i} i={i} frame={frame} />
))}
</div>
</div>
</div>
);
};
- 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/paper-craft-moves/MaskingTapeSlap.tsx, demos/ui-entrance/paper-craft-moves/PopupBookRise.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.