{
  "slug": "page-waterfall-wall",
  "title": "Page waterfall wall",
  "tagline": "Real page screenshots split into 3–4 columns scroll endlessly at different speeds and directions on a tilted-back 3D wall.",
  "description": "Real page screenshots split into 3–4 columns scroll endlessly at different speeds and directions on a tilted-back 3D wall.",
  "category": "ui-entrance",
  "tags": [
    "shotcraft",
    "remotion",
    "ui-entrance"
  ],
  "tier": "free",
  "license": "Apache-2.0",
  "spec": {
    "width": 1920,
    "height": 1080,
    "fps": 30,
    "duration": 5.06,
    "aspect": "16:9"
  },
  "kind": "remotion",
  "params_schema": {
    "type": "object",
    "properties": {}
  },
  "default_params": {},
  "preview": {
    "poster": "https://gimgs.net/motion/media/page-waterfall-wall/poster-9b0c76199d07.jpg",
    "video": "https://gimgs.net/motion/media/page-waterfall-wall/video-ad54f718918e.mp4"
  },
  "variants": [
    {
      "key": "page-waterfall-wall",
      "label": "page-waterfall-wall",
      "description": "Page waterfall wall — real page screenshots cut into 3–4 columns scroll endlessly at differential speeds in opposite directions on a 3D tilted-back wall; parallax plus a slow camera push create an overview of \"too much content to ever finish streaming.\"",
      "use": "",
      "video": "https://gimgs.net/motion/media/page-waterfall-wall/video-ad54f718918e.mp4",
      "poster": "https://gimgs.net/motion/media/page-waterfall-wall/poster-9b0c76199d07.jpg"
    }
  ],
  "credits": {
    "author": "video-shotcraft (Vincent Wei)",
    "url": "https://github.com/Vincentwei1021/video-shotcraft"
  },
  "page_url": "https://gimgs.net/motion/page-waterfall-wall.html",
  "json_url": "https://gimgs.net/motion/page-waterfall-wall.json",
  "template_url": "https://gimgs.net/motion/page-waterfall-wall/template.html",
  "play_url": null,
  "markdown_url": "https://gimgs.net/motion/page-waterfall-wall.md",
  "updated_at": "2026-09-24T07:40:11.752Z",
  "prompt": "## When to use\n\nA sense-of-scale section for \"many pages/features/templates\"; montage mid-section build-up or a product-breadth shot after the intro\n\n## Intention\n\nA 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.\n\n## Duration & energy\n\n- Duration: 4–6s (an infinite loop body, trimmed to the section's needed length)\n- Energy: Medium (a flowing-display type)\n\n## Shot recipe (original Chinese — timing, easing, parameter table, known pitfalls)\n\n## 意图\n流动式一览：让观众在几秒内\"看到很多页面流过\"，读感是体量而非细节。\n与 outro-group-photo-launch 分工：那是**聚拢式**一览（元素飞来围住\n字标定格合影，语义\"属于同一产品\"），本卡是**流动式**一览（内容持续\n流过不定格，语义\"还有很多没给你看完\"）。与 odometer-digit-roll 分工：\n那是有终值的机械滚轮（逐位停稳读数字），本卡无终值无限循环。\n与 wall-reveal-moves 分工：那是整墙**入场动作**，本卡是墙的**持续状态**，\n可用那卡入场后交棒本卡续流。\n\n## 动效核心\n- lib 组件 `assets/lib/VerticalTicker.tsx`：父层 perspective(1000px)\n  rotateX(20°) scale(1.2) 后仰成墙；每列 [...items,...items] 翻倍 +\n  progress 取模 translateY 0→-50% 无缝循环；上下渐变遮罩收边\n- 差速反向是视差命门：3 列 loop 时长错开（如 12/9/14s），中列反向——\n  等速同向读作一张大图在滚，差速反向才读出\"独立的列\"\n- 镜头缓推寄生在外层（scale 1→1.06 线性铺满全镜），墙自身循环、\n  镜头单向，两个运动源方向不同不打架\n- 素材：真实页面截图切片（Q1 脱敏后），卡片级切块比整页长图好——\n  整页切片滚动时观众来不及读，卡片块有完整轮廓可辨识\n\n## 参数表\n| 参数 | 典型值 | 调节手感 |\n|------|--------|----------|\n| 列数 | 3（1080p）/ 4（超宽） | 2 列读不出\"墙\"，5 列单列过窄纹理糊 |\n| loop 时长 | 相邻列差 ≥25%（12/9/14s） | 差速太小视差读不出；最快列 <7s 开始读作\"刷屏\" |\n| 方向 | 中列反向（-1,1,-1 或交替） | 全同向=一张大图；反向列是\"独立列\"的最强证据 |\n| 倾角/透视 | rotateX 20° / perspective 1000px | >25° 顶部行文字透视压糊（Q2）；<12° 读不出墙面后仰 |\n| scale | 1.2 基础 + 缓推至 ~1.26 | 1.2 是补透视收缩的底值，低于它四角露底 |\n| 遮罩高度 | 200px | 太矮行硬进硬出；太高可视区剩不下 2 行 |\n| 循环周期 | -50% 位移必须恰等于单副本周期（内容高+n·gap） | lib 已用 marginBottom 保证；自写实现用 flex gap 会差 gap/2 跳帧 |\n\n## 已知坑\n- **参数借鉴自外部实现（remotion-3d-ticker），非实战定稿；\n  首次实战须以真实素材回验**\n- 每列 items 总高必须 ≥ 视口有效高（否则同屏见同一切片两次，穿帮）；\n  列间切片不要复用同一张图，同图同时出现在两列读作素材穷\n- 本卡是氛围/铺陈镜头不是信息镜头：切片上文字只求\"看得出是真页面\"\n  不求读完；想让观众读某一块，交棒 spotlight-hero-card 单主角处理\n- P4 去重：与 outro 合影同片共存没问题（一流动一聚拢），但全片\n  瀑布墙只出现一次，二次出现读作凑时长\n\n## 参考实现\nassets/lib/VerticalTicker.tsx（核心组件）；\ndemo 见 demos/ui-entrance/page-waterfall-wall/（PageWaterfallWall.tsx / VerticalTicker.tsx）。\n\n## Reference implementation (Remotion)\n\n- demos/ui-entrance/page-waterfall-wall/PageWaterfallWall.tsx — https://raw.githubusercontent.com/Vincentwei1021/video-shotcraft/main/demos/ui-entrance/page-waterfall-wall/PageWaterfallWall.tsx\n- demos/ui-entrance/page-waterfall-wall/VerticalTicker.tsx — https://raw.githubusercontent.com/Vincentwei1021/video-shotcraft/main/demos/ui-entrance/page-waterfall-wall/VerticalTicker.tsx\n\nThe component imports shared fixtures from `demos/_fixtures` and helpers from `assets/lib` in the repo; SKILL.md there documents the render workflow.\n",
  "flow": [
    {
      "step": "Read the recipe",
      "note": "Prompt = when to use, intention, parameter table with typical values and how each one feels, known pitfalls. Treat values as calibrated starting points."
    },
    {
      "step": "Get the code",
      "tool": "git",
      "cmd": "git clone https://github.com/Vincentwei1021/video-shotcraft && cd video-shotcraft && npm install"
    },
    {
      "step": "Register + render",
      "tool": "remotion",
      "cmd": "npx remotion render <CompositionId> out.mp4   # component: demos/ui-entrance/page-waterfall-wall/PageWaterfallWall.tsx, demos/ui-entrance/page-waterfall-wall/VerticalTicker.tsx"
    },
    {
      "step": "Or re-implement",
      "note": "Port the timing/easing from the recipe to HTML + WAAPI (gimgs motion contract) or any engine; keep the same segment windows and easing."
    }
  ],
  "files": [
    {
      "path": "demos/ui-entrance/page-waterfall-wall/PageWaterfallWall.tsx",
      "url": "https://raw.githubusercontent.com/Vincentwei1021/video-shotcraft/main/demos/ui-entrance/page-waterfall-wall/PageWaterfallWall.tsx"
    },
    {
      "path": "demos/ui-entrance/page-waterfall-wall/VerticalTicker.tsx",
      "url": "https://raw.githubusercontent.com/Vincentwei1021/video-shotcraft/main/demos/ui-entrance/page-waterfall-wall/VerticalTicker.tsx"
    }
  ],
  "contract": "React/Remotion composition (TSX) from the open-source video-shotcraft library (Apache-2.0). It is not a browser-playable template: clone the repo, register the component in a Remotion project and render with `npx remotion render`. The recipe (prompt) describes timing, easing and parameters so you can also re-implement the shot in any engine.",
  "render": {
    "engine": "remotion",
    "frames": 152,
    "repo": "https://github.com/Vincentwei1021/video-shotcraft",
    "files": [
      "https://raw.githubusercontent.com/Vincentwei1021/video-shotcraft/main/demos/ui-entrance/page-waterfall-wall/PageWaterfallWall.tsx",
      "https://raw.githubusercontent.com/Vincentwei1021/video-shotcraft/main/demos/ui-entrance/page-waterfall-wall/VerticalTicker.tsx"
    ],
    "note": "Clone https://github.com/Vincentwei1021/video-shotcraft (npm install). The component lives under demos/{category}/{name}/ and imports shared fixtures from demos/_fixtures and helpers from assets/lib. Copy it (with those imports) into a Remotion project, register a <Composition> with the template's width/height/fps/duration in Root.tsx, then `npx remotion render <CompositionId> out.mp4`. SKILL.md in the repo documents the full Claude Code / Codex workflow. The prompt below is the shot recipe (timing, easing, parameter table, pitfalls) — enough to re-implement the shot in HTML/WAAPI or another engine if you prefer."
  },
  "source": "// ===== demos/ui-entrance/page-waterfall-wall/PageWaterfallWall.tsx =====\nimport React from 'react';\nimport {\n  AbsoluteFill,\n  Img,\n  interpolate,\n  staticFile,\n  useCurrentFrame,\n  useVideoConfig,\n} from 'remotion';\nimport { VerticalTicker, TickerColumn } from './VerticalTicker';\n\nconst BG = '#101014';\n\nconst shot = (file: string) => (\n  <div\n    style={{\n      borderRadius: 16,\n      overflow: 'hidden',\n      boxShadow: '0 12px 40px rgba(0,0,0,0.45)',\n      background: '#fff',\n    }}\n  >\n    <Img src={staticFile(`textures/${file}`)} style={{ width: '100%', display: 'block' }} />\n  </div>\n);\n\n// 3 列差速反向（配方卡参数：loop 12/9/14s，中列反向）\nexport const buildColumns = (loops: [number, number, number]): TickerColumn[] => [\n  {\n    items: ['card1.png', 'card2.png', 'card3.png', 'card10.png'].map(shot),\n    durationInSeconds: loops[0],\n    direction: -1,\n  },\n  {\n    items: ['card4.png', 'card5.png', 'card6.png', 'projects-empty.png'].map(shot),\n    durationInSeconds: loops[1],\n    direction: 1,\n  },\n  {\n    items: ['card7.png', 'card8.png', 'card9.png', 'float-search.png'].map(shot),\n    durationInSeconds: loops[2],\n    direction: -1,\n  },\n];\n\nexport const PageWaterfallWall: React.FC = () => {\n  const frame = useCurrentFrame();\n  const { durationInFrames } = useVideoConfig();\n  // 镜头缓推寄生在外层，墙自身循环、镜头单向\n  const push = interpolate(frame, [0, durationInFrames], [1, 1.06]);\n\n  return (\n    <AbsoluteFill style={{ backgroundColor: BG }}>\n      <AbsoluteFill style={{ transform: `scale(${push})` }}>\n        <VerticalTicker\n          columns={buildColumns([12, 9, 14])}\n          backgroundColor={BG}\n          columnWidth={560}\n          gap={30}\n        />\n      </AbsoluteFill>\n    </AbsoluteFill>\n  );\n};\n\n// 接缝自检用：短 loop（3s=90f）、无镜头推，f0 与 f90 应逐像素一致\nexport const SeamTest: React.FC = () => (\n  <AbsoluteFill style={{ backgroundColor: BG }}>\n    <VerticalTicker\n      columns={buildColumns([3, 3, 3])}\n      backgroundColor={BG}\n      columnWidth={560}\n      gap={30}\n    />\n  </AbsoluteFill>\n);\n\n\n// ===== demos/ui-entrance/page-waterfall-wall/VerticalTicker.tsx =====\n// origin: vibe-motion/skills remotion-3d-ticker assets/VerticalTicker.tsx\n//         (2026-07-13 扫描，2026-07-15 吸收；改动：items 泛化为 ReactNode、\n//          遮罩高度/透视角/scale/列宽/间距提为 props)\nimport React from 'react';\nimport { AbsoluteFill, useCurrentFrame, useVideoConfig } from 'remotion';\n\nexport interface TickerColumn {\n  /** Column content — any ReactNode (screenshot-slice divs, <Img>, text cards…) */\n  items: React.ReactNode[];\n  /** Seconds for one full loop (one copy of items scrolling its own height) */\n  durationInSeconds: number;\n  /** -1 scrolls up, 1 scrolls down */\n  direction: -1 | 1;\n}\n\nexport interface VerticalTickerProps {\n  columns: TickerColumn[];\n  /** Background color, also used by the top/bottom fade masks */\n  backgroundColor?: string;\n  /** Height of each fade mask in px */\n  maskHeight?: number;\n  /** rotateX tilt of the whole wall, degrees */\n  tiltDeg?: number;\n  /** CSS perspective distance in px */\n  perspective?: number;\n  /** Overall scale of the tilted wall (compensates perspective shrink) */\n  scale?: number;\n  /** Column width in px */\n  columnWidth?: number;\n  /** Gap between columns and between items within a column, px */\n  gap?: number;\n}\n\n/**\n * 3D 透视多列无限纵向滚动墙。每列 [...items, ...items] 翻倍 +\n * progress 取模 translateY 0→-50%：前半段恰好滚出视口时归零，\n * 后半段与前半段像素级一致，循环点不可见。\n * 接缝无痕的前提：-50% 必须恰等于单副本周期（内容高 + n 个 gap）。\n * 源实现用 flex gap（翻倍后只有 2n-1 个 gap）会差 gap/2 px 跳一下，\n * 这里改为每项 marginBottom = gap，容器高恰为周期两倍。\n */\nexport const VerticalTicker: React.FC<VerticalTickerProps> = ({\n  columns,\n  backgroundColor = '#000',\n  maskHeight = 200,\n  tiltDeg = 20,\n  perspective = 1000,\n  scale = 1.2,\n  columnWidth = 400,\n  gap = 30,\n}) => {\n  return (\n    <AbsoluteFill style={{ backgroundColor, overflow: 'hidden' }}>\n      <div\n        style={{\n          width: '100%',\n          height: '100%',\n          display: 'flex',\n          justifyContent: 'center',\n          gap,\n          transform: `perspective(${perspective}px) rotateX(${tiltDeg}deg) scale(${scale})`,\n          transformOrigin: 'center center',\n        }}\n      >\n        {columns.map((col, idx) => (\n          <Column key={idx} {...col} width={columnWidth} gap={gap} />\n        ))}\n      </div>\n      {/* Fade masks sit above the wall so rows enter/exit softly */}\n      <div\n        style={{\n          position: 'absolute',\n          top: 0,\n          left: 0,\n          right: 0,\n          height: maskHeight,\n          background: `linear-gradient(to bottom, ${backgroundColor} 0%, transparent 100%)`,\n          zIndex: 10,\n        }}\n      />\n      <div\n        style={{\n          position: 'absolute',\n          bottom: 0,\n          left: 0,\n          right: 0,\n          height: maskHeight,\n          background: `linear-gradient(to top, ${backgroundColor} 0%, transparent 100%)`,\n          zIndex: 10,\n        }}\n      />\n    </AbsoluteFill>\n  );\n};\n\nconst Column: React.FC<TickerColumn & { width: number; gap: number }> = ({\n  items,\n  durationInSeconds,\n  direction,\n  width,\n  gap,\n}) => {\n  const frame = useCurrentFrame();\n  const { fps } = useVideoConfig();\n\n  const loopFrames = durationInSeconds * fps;\n  const progress = (frame % loopFrames) / loopFrames;\n  // up: 0 → -50%; down: -50% → 0（起点即偏移半程，首帧无空白）\n  const translateY = direction === -1 ? progress * -50 : -50 + progress * 50;\n\n  return (\n    <div style={{ width, height: '100%', display: 'flex', flexDirection: 'column' }}>\n      <div\n        style={{\n          display: 'flex',\n          flexDirection: 'column',\n          transform: `translateY(${translateY}%)`,\n          willChange: 'transform',\n        }}\n      >\n        {[...items, ...items].map((node, i) => (\n          <div key={i} style={{ marginBottom: gap }}>\n            {node}\n          </div>\n        ))}\n      </div>\n    </div>\n  );\n};\n\n\n"
}