// ===== 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) => (
); // 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 ( ); }; // 接缝自检用:短 loop(3s=90f)、无镜头推,f0 与 f90 应逐像素一致 export const SeamTest: React.FC = () => ( ); // ===== 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, , 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 = ({ columns, backgroundColor = '#000', maskHeight = 200, tiltDeg = 20, perspective = 1000, scale = 1.2, columnWidth = 400, gap = 30, }) => { return (
{columns.map((col, idx) => ( ))}
{/* Fade masks sit above the wall so rows enter/exit softly */}
); }; const Column: React.FC = ({ 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 (
{[...items, ...items].map((node, i) => (
{node}
))}
); };