Motion · UI entrance
Page waterfall wall
Real page screenshots split into 3–4 columns scroll endlessly at different speeds and directions on a tilted-back 3D wall.
Source: video-shotcraft (Vincent Wei) ↗ · Apache-2.0 · Remotion composition (React/TSX) — preview video, no in-browser player
When to use
A sense-of-scale section for "many pages/features/templates"; montage mid-section build-up or a product-breadth shot after the intro
Intention
A flowing overview: let the viewer "see a lot of pages stream by" in a few seconds, reading as scale rather than detail. Division of labor with outro-group-photo-launch: that's a convergent overview (elements fly in to surround the wordmark and freeze for a group photo, semantics "they belong to the same product"), this is a flowing overview (content keeps streaming without ever freezing, semantics "there's still more we haven't shown you"). Division of labor with odometer-digit-roll: that's a mechanical reel with a final value (settling digit by digit), this loops infinitely with no final value. Division of labor with wall-reveal-moves: that's the wall's entrance move, this is the wall's ongoing state — wall-reveal-moves can hand off into this card to keep flowing after it enters.
Duration & energy
- Duration: 4–6s (an infinite loop body, trimmed to the section's needed length)
- Energy: Medium (a flowing-display type)
Shot recipe (original Chinese — timing, easing, parameter table, known pitfalls)
意图
流动式一览:让观众在几秒内"看到很多页面流过",读感是体量而非细节。 与 outro-group-photo-launch 分工:那是聚拢式一览(元素飞来围住 字标定格合影,语义"属于同一产品"),本卡是流动式一览(内容持续 流过不定格,语义"还有很多没给你看完")。与 odometer-digit-roll 分工: 那是有终值的机械滚轮(逐位停稳读数字),本卡无终值无限循环。 与 wall-reveal-moves 分工:那是整墙入场动作,本卡是墙的持续状态, 可用那卡入场后交棒本卡续流。
动效核心
- lib 组件
assets/lib/VerticalTicker.tsx:父层 perspective(1000px)
rotateX(20°) scale(1.2) 后仰成墙;每列 [...items,...items] 翻倍 + progress 取模 translateY 0→-50% 无缝循环;上下渐变遮罩收边
- 差速反向是视差命门:3 列 loop 时长错开(如 12/9/14s),中列反向——
等速同向读作一张大图在滚,差速反向才读出"独立的列"
- 镜头缓推寄生在外层(scale 1→1.06 线性铺满全镜),墙自身循环、
镜头单向,两个运动源方向不同不打架
- 素材:真实页面截图切片(Q1 脱敏后),卡片级切块比整页长图好——
整页切片滚动时观众来不及读,卡片块有完整轮廓可辨识
参数表
| 参数 | 典型值 | 调节手感 | |------|--------|----------| | 列数 | 3(1080p)/ 4(超宽) | 2 列读不出"墙",5 列单列过窄纹理糊 | | loop 时长 | 相邻列差 ≥25%(12/9/14s) | 差速太小视差读不出;最快列 <7s 开始读作"刷屏" | | 方向 | 中列反向(-1,1,-1 或交替) | 全同向=一张大图;反向列是"独立列"的最强证据 | | 倾角/透视 | rotateX 20° / perspective 1000px | >25° 顶部行文字透视压糊(Q2);<12° 读不出墙面后仰 | | scale | 1.2 基础 + 缓推至 ~1.26 | 1.2 是补透视收缩的底值,低于它四角露底 | | 遮罩高度 | 200px | 太矮行硬进硬出;太高可视区剩不下 2 行 | | 循环周期 | -50% 位移必须恰等于单副本周期(内容高+n·gap) | lib 已用 marginBottom 保证;自写实现用 flex gap 会差 gap/2 跳帧 |
已知坑
- **参数借鉴自外部实现(remotion-3d-ticker),非实战定稿;
首次实战须以真实素材回验**
- 每列 items 总高必须 ≥ 视口有效高(否则同屏见同一切片两次,穿帮);
列间切片不要复用同一张图,同图同时出现在两列读作素材穷
- 本卡是氛围/铺陈镜头不是信息镜头:切片上文字只求"看得出是真页面"
不求读完;想让观众读某一块,交棒 spotlight-hero-card 单主角处理
- P4 去重:与 outro 合影同片共存没问题(一流动一聚拢),但全片
瀑布墙只出现一次,二次出现读作凑时长
参考实现
assets/lib/VerticalTicker.tsx(核心组件); demo 见 demos/ui-entrance/page-waterfall-wall/(PageWaterfallWall.tsx / VerticalTicker.tsx)。
Reference implementation (Remotion)
- demos/ui-entrance/page-waterfall-wall/PageWaterfallWall.tsx — https://raw.githubusercontent.com/Vincentwei1021/video-shotcraft/main/demos/ui-entrance/page-waterfall-wall/PageWaterfallWall.tsx
- demos/ui-entrance/page-waterfall-wall/VerticalTicker.tsx — https://raw.githubusercontent.com/Vincentwei1021/video-shotcraft/main/demos/ui-entrance/page-waterfall-wall/VerticalTicker.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/page-waterfall-wall.jsongit clone https://github.com/Vincentwei1021/video-shotcraft
# component: demos/ui-entrance/page-waterfall-wall/PageWaterfallWall.tsx, demos/ui-entrance/page-waterfall-wall/VerticalTicker.tsx
npx remotion render <CompositionId> out.mp4claude mcp add --transport http gimgs https://gimgs.net/mcp
# then: search_motion_templates / get_motion_template("page-waterfall-wall")Prompt
Use the video-shotcraft shot recipe "Page waterfall wall" (Remotion composition, Apache-2.0) published on gimgs.net. Recipe JSON (prompt = timing/easing/parameter table/pitfalls, files = TSX source): https://gimgs.net/motion/page-waterfall-wall.json Raw TSX: https://gimgs.net/motion/page-waterfall-wall/template.html Repository: https://github.com/Vincentwei1021/video-shotcraft (component: demos/ui-entrance/page-waterfall-wall/PageWaterfallWall.tsx, demos/ui-entrance/page-waterfall-wall/VerticalTicker.tsx; imports demos/_fixtures and assets/lib) Spec: 1920x1080, 30 fps, 5.06s 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/page-waterfall-wall/PageWaterfallWall.tsx =====
import React from 'react';
import {
AbsoluteFill,
Img,
interpolate,
staticFile,
useCurrentFrame,
useVideoConfig,
} from 'remotion';
import { VerticalTicker, TickerColumn } from './VerticalTicker';
const BG = '#101014';
const shot = (file: string) => (
<div
style={{
borderRadius: 16,
overflow: 'hidden',
boxShadow: '0 12px 40px rgba(0,0,0,0.45)',
background: '#fff',
}}
>
<Img src={staticFile(`textures/${file}`)} style={{ width: '100%', display: 'block' }} />
</div>
);
// 3 列差速反向(配方卡参数:loop 12/9/14s,中列反向)
export const buildColumns = (loops: [number, number, number]): TickerColumn[] => [
{
items: ['card1.png', 'card2.png', 'card3.png', 'card10.png'].map(shot),
durationInSeconds: loops[0],
direction: -1,
},
{
items: ['card4.png', 'card5.png', 'card6.png', 'projects-empty.png'].map(shot),
durationInSeconds: loops[1],
direction: 1,
},
{
items: ['card7.png', 'card8.png', 'card9.png', 'float-search.png'].map(shot),
durationInSeconds: loops[2],
direction: -1,
},
];
export const PageWaterfallWall: React.FC = () => {
const frame = useCurrentFrame();
const { durationInFrames } = useVideoConfig();
// 镜头缓推寄生在外层,墙自身循环、镜头单向
const push = interpolate(frame, [0, durationInFrames], [1, 1.06]);
return (
<AbsoluteFill style={{ backgroundColor: BG }}>
<AbsoluteFill style={{ transform: `scale(${push})` }}>
<VerticalTicker
columns={buildColumns([12, 9, 14])}
backgroundColor={BG}
columnWidth={560}
gap={30}
/>
</AbsoluteFill>
</AbsoluteFill>
);
};
// 接缝自检用:短 loop(3s=90f)、无镜头推,f0 与 f90 应逐像素一致
export const SeamTest: React.FC = () => (
<AbsoluteFill style={{ backgroundColor: BG }}>
<VerticalTicker
columns={buildColumns([3, 3, 3])}
backgroundColor={BG}
columnWidth={560}
gap={30}
/>
</AbsoluteFill>
);
// ===== demos/ui-entrance/page-waterfall-wall/VerticalTicker.tsx =====
// origin: vibe-motion/skills remotion-3d-ticker assets/VerticalTicker.tsx
// (2026-07-13 扫描,2026-07-15 吸收;改动:items 泛化为 ReactNode、
// 遮罩高度/透视角/scale/列宽/间距提为 props)
import React from 'react';
import { AbsoluteFill, useCurrentFrame, useVideoConfig } from 'remotion';
export interface TickerColumn {
/** Column content — any ReactNode (screenshot-slice divs, <Img>, text cards…) */
items: React.ReactNode[];
/** Seconds for one full loop (one copy of items scrolling its own height) */
durationInSeconds: number;
/** -1 scrolls up, 1 scrolls down */
direction: -1 | 1;
}
export interface VerticalTickerProps {
columns: TickerColumn[];
/** Background color, also used by the top/bottom fade masks */
backgroundColor?: string;
/** Height of each fade mask in px */
maskHeight?: number;
/** rotateX tilt of the whole wall, degrees */
tiltDeg?: number;
/** CSS perspective distance in px */
perspective?: number;
/** Overall scale of the tilted wall (compensates perspective shrink) */
scale?: number;
/** Column width in px */
columnWidth?: number;
/** Gap between columns and between items within a column, px */
gap?: number;
}
/**
* 3D 透视多列无限纵向滚动墙。每列 [...items, ...items] 翻倍 +
* progress 取模 translateY 0→-50%:前半段恰好滚出视口时归零,
* 后半段与前半段像素级一致,循环点不可见。
* 接缝无痕的前提:-50% 必须恰等于单副本周期(内容高 + n 个 gap)。
* 源实现用 flex gap(翻倍后只有 2n-1 个 gap)会差 gap/2 px 跳一下,
* 这里改为每项 marginBottom = gap,容器高恰为周期两倍。
*/
export const VerticalTicker: React.FC<VerticalTickerProps> = ({
columns,
backgroundColor = '#000',
maskHeight = 200,
tiltDeg = 20,
perspective = 1000,
scale = 1.2,
columnWidth = 400,
gap = 30,
}) => {
return (
<AbsoluteFill style={{ backgroundColor, overflow: 'hidden' }}>
<div
style={{
width: '100%',
height: '100%',
display: 'flex',
justifyContent: 'center',
gap,
transform: `perspective(${perspective}px) rotateX(${tiltDeg}deg) scale(${scale})`,
transformOrigin: 'center center',
}}
>
{columns.map((col, idx) => (
<Column key={idx} {...col} width={columnWidth} gap={gap} />
))}
</div>
{/* Fade masks sit above the wall so rows enter/exit softly */}
<div
style={{
position: 'absolute',
top: 0,
left: 0,
right: 0,
height: maskHeight,
background: `linear-gradient(to bottom, ${backgroundColor} 0%, transparent 100%)`,
zIndex: 10,
}}
/>
<div
style={{
position: 'absolute',
bottom: 0,
left: 0,
right: 0,
height: maskHeight,
background: `linear-gradient(to top, ${backgroundColor} 0%, transparent 100%)`,
zIndex: 10,
}}
/>
</AbsoluteFill>
);
};
const Column: React.FC<TickerColumn & { width: number; gap: number }> = ({
items,
durationInSeconds,
direction,
width,
gap,
}) => {
const frame = useCurrentFrame();
const { fps } = useVideoConfig();
const loopFrames = durationInSeconds * fps;
const progress = (frame % loopFrames) / loopFrames;
// up: 0 → -50%; down: -50% → 0(起点即偏移半程,首帧无空白)
const translateY = direction === -1 ? progress * -50 : -50 + progress * 50;
return (
<div style={{ width, height: '100%', display: 'flex', flexDirection: 'column' }}>
<div
style={{
display: 'flex',
flexDirection: 'column',
transform: `translateY(${translateY}%)`,
willChange: 'transform',
}}
>
{[...items, ...items].map((node, i) => (
<div key={i} style={{ marginBottom: gap }}>
{node}
</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/page-waterfall-wall/PageWaterfallWall.tsx, demos/ui-entrance/page-waterfall-wall/VerticalTicker.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.