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.

1920×108016:930 fps5.06s · 152 framesremotionLicense: Apache-2.0#shotcraft#remotion#ui-entrance

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.

  1. Fetch the JSON — it holds the shot recipe (timing, easing, parameter table, pitfalls) and the full TSX source.
  2. Clone the video-shotcraft repo; the component imports shared fixtures from demos/_fixtures and helpers from assets/lib.
  3. 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.json
git 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.mp4
claude 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).

JSON · Markdown · llms.txt

5,847 chars · Apache-2.0Open raw template ↗
// ===== 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>
  );
};


  1. Read the recipe
    Prompt = when to use, intention, parameter table with typical values and how each one feels, known pitfalls. Treat values as calibrated starting points.
  2. Get the code
    tool: git
    git clone https://github.com/Vincentwei1021/video-shotcraft && cd video-shotcraft && npm install
  3. Register + render
    tool: remotion
    npx remotion render <CompositionId> out.mp4 # component: demos/ui-entrance/page-waterfall-wall/PageWaterfallWall.tsx, demos/ui-entrance/page-waterfall-wall/VerticalTicker.tsx
  4. Or re-implement
    Port the timing/easing from the recipe to HTML + WAAPI (gimgs motion contract) or any engine; keep the same segment windows and easing.
paramtypedefaultdescription
—

More in this category