Motion · UI xuất hiện
Bức tường thác trang
Ảnh chụp trang thật chia thành 3–4 cột cuộn vô tận tốc độ và chiều khác nhau trên tường 3D ngả về sau.
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
Đoạn tạo cảm giác quy mô "nhiều trang/nhiều tính năng/nhiều mẫu"; phần dựng giữa của montage hoặc cảnh thể hiện độ rộng sản phẩm sau intro
Ý đồ
Một cái nhìn tổng quan kiểu trôi chảy: để người xem "thấy rất nhiều trang trôi qua" trong vài giây, cảm nhận là quy mô chứ không phải chi tiết. Phân công với outro-group-photo-launch: đó là tổng quan kiểu hội tụ (các yếu tố bay vào vây quanh wordmark rồi đứng yên chụp ảnh nhóm, ngữ nghĩa "chúng thuộc về cùng một sản phẩm"), đây là tổng quan kiểu trôi chảy (nội dung liên tục trôi qua không bao giờ đứng yên, ngữ nghĩa "vẫn còn rất nhiều chưa cho bạn xem hết"). Phân công với odometer-digit-roll: đó là cuộn cơ khí có giá trị cuối (dừng khớp từng số), đây lặp vô tận không có giá trị cuối. Phân công với wall-reveal-moves: đó là động tác vào cảnh của cả bức tường, đây là trạng thái duy trì của bức tường — có thể dùng wall-reveal-moves vào cảnh rồi bàn giao cho cảnh này để tiếp tục trôi.
Thời lượng & năng lượng
- Thời lượng: 4–6s (khối lặp vô tận, thời lượng cắt theo nhu cầu đoạn phim)
- Năng lượng: Trung bình (kiểu trưng bày trôi chảy)
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)
意图
流动式一览:让观众在几秒内"看到很多页面流过",读感是体量而非细节。 与 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)。
Mã nguồn tham chiếu (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
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/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 "Bức tường thác trang" (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 |
|---|---|---|---|
| — | |||
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.