// ===== demos/camera/graze-face-tour/GrazeFaceTour.tsx ===== // graze-face-tour v2 —— 源片 clickup-30.mp4 约 28.5–33s: // 相机大倾角贴着 UI 表面游走特写,三段接力:侧栏树 → 顶部 tab 条 → 列表行。 // v2(用户意见):页面文字初始悬浮在界面上空(3D 抬高),空中时在 UI 面上 // 投模糊同形软影;随镜头推进先后落贴回界面,影子随高度收敛消失。 import React from 'react'; import { AbsoluteFill, interpolate, useCurrentFrame, Easing } from 'remotion'; const FONT = 'Helvetica, Arial, sans-serif'; const INK = '#3a3a40'; const MID = '#8b8b92'; const FAINT = '#c8c8ce'; const easeIO = Easing.bezier(0.45, 0, 0.25, 1); const easeFall = Easing.bezier(0.5, 0.05, 0.6, 1); // 加速下落、末端软着陆 /* ---------- 悬浮 + 同形软影 ---------- * h=悬浮高度(px)。文字整体向上(屏幕上-左)抬起, * 原位置留一份模糊压暗的同形投影;h→0 时两者重合、影子消失。 */ const FloatWrap: React.FC<{ h: number; children: React.ReactNode }> = ({ h, children }) => (
{h > 1.5 && (
{children}
)}
{children}
); /* 每行的悬浮高度:land = 该行贴回完成的段内时刻(0..1),之前从 H 高度加速落下 */ const liftOf = (t: number, land: number, H = 120) => { const FALL = 0.34; const p = Math.min(1, Math.max(0, (t - (land - FALL)) / FALL)); return (1 - easeFall(p)) * H; }; /* ---------- 灰阶自绘 UI 素材 ---------- */ const Chip: React.FC<{ letter: string; tone: string }> = ({ letter, tone }) => (
{letter}
); const Tri: React.FC<{ open?: boolean }> = ({ open }) => (
); const DocIcon: React.FC = () => (
); const FolderIcon: React.FC = () => (
); const TreeRow: React.FC<{ depth: number; label: string; icon?: 'tri' | 'triOpen' | 'doc' | 'folder' | 'dash' | 'chip'; chip?: string; count?: string; size?: number; dim?: boolean; }> = ({ depth, label, icon, chip, count, size = 58, dim }) => (
{icon === 'tri' && } {icon === 'triOpen' && } {icon === 'doc' && } {icon === 'folder' && } {icon === 'dash' &&
} {chip && }
{label}
{count &&
{count}
}
); const RecentCard: React.FC<{ title: string; sub: string; w?: number }> = ({ title, sub, w = 880 }) => (
{title}
{sub}
); /* 场景 A:侧栏 SPACES 树 + 右侧 Recent 卡(对标截图 1/2/4) * v2:树行/卡片按镜头行进方向(自上而下)先后从空中落贴回界面 */ const SceneTree: React.FC<{ t?: number }> = ({ t = 1 }) => { // 树行落地时刻:镜头由树顶滑向树底,行 index 越大越晚落 const L = (i: number, n = 14) => liftOf(t, 0.22 + (i / n) * 0.62, 130); const rows: [number, string, 'tri' | 'triOpen' | 'doc' | 'folder' | 'dash' | undefined, string | undefined, string | undefined][] = [ [0, 'People & Teams', 'doc', undefined, undefined], [0, 'Goals', 'doc', undefined, undefined], [0, 'Docs', 'doc', undefined, undefined], [0, 'More', 'dash', undefined, undefined], [0, 'EPD', 'tri', 'E', undefined], [0, 'Product roadmap', 'tri', 'P', undefined], [0, 'Design', 'triOpen', 'D', undefined], [1, 'Designer handbook', 'doc', undefined, undefined], [1, '3.0', 'folder', undefined, undefined], [1, 'Design system', 'folder', undefined, undefined], [2, 'Design system', 'doc', undefined, undefined], [2, 'Components', 'dash', undefined, '56'], [2, 'Patterns', 'dash', undefined, '8'], [2, 'Tokens', 'dash', undefined, '256'], ]; return (
{rows.slice(0, 4).map((r, i) => ( ))}
SPACES
{rows.slice(4).map((r, i) => ( ))}
Recent
Favorites
Todo
Comments
Done
≡ List
▦ Gallery
); }; /* 场景 B:顶部 tab 条 + 左上侧栏导航(对标截图 6/7) * v2:tab 条、logo、Home 行、侧栏项先后从空中贴落 */ const SceneTopNav: React.FC<{ t?: number }> = ({ t = 1 }) => (
{['Product analytics', 'ClickUp 3.0', 'Widget brainstorm', 'Design system'].map((tb, i) => (
1 ? 0.75 : 1 }}>
{tb}
))}
ClickUp
Home
3
{['Inbox', 'Company', 'People & Teams', 'Goals', 'Docs'].map((tb, i) => (
{tb}
))}
‹
›
Home
Home
Search by app, filetype…
); /* 场景 C:列表行(对标截图 4 右下 TODAY/TASK NAME 区) * v2:Todo 头/TODAY 徽章/任务行自上而下先后贴落 */ const SceneListRows: React.FC<{ t?: number }> = ({ t = 1 }) => (
Todo
Comments
Done
Delegated
≡ List
▦ Gallery
Filter
Group
Sort
TODAY
TASK NAME
{['New Bugs Per Week', 'Designer handbook', 'Mobile screens', 'Product roadmap'].map((tb, i) => (
{tb}
…
))}
); /* ---------- 3D 摄影 ---------- */ type Cam = { rx: number; ry: number; rz: number; scale: number; x: [number, number]; y: [number, number]; }; const Plane: React.FC<{ cam: Cam; t: number; edge?: 'left' | 'top'; children: React.ReactNode; }> = ({ cam, t, edge = 'left', children }) => { const x = interpolate(t, [0, 1], cam.x, { easing: easeIO }); const y = interpolate(t, [0, 1], cam.y, { easing: easeIO }); return (
{/* 屏幕边缘霓虹晕染 */} {edge === 'left' ? ( <>
) : ( <>
)} {children} {/* 远端压暗:贴面透视里远处渐暗 */}
); }; /* 背景霓虹描边矩形(暗场道具) */ const NeonRects: React.FC<{ drift: number }> = ({ drift }) => (
); // 平移目标以内容坐标 (cx,cy) 给出:translate = (W/2-cx, H/2-cy) const SEGS: { cam: Cam; edge: 'left' | 'top'; render: (t: number) => React.ReactNode }[] = [ { // 侧栏树:从树顶(SPACES 附近)贴面滑到树底(Components/Patterns/Tokens) cam: { rx: 12, ry: 30, rz: -6, scale: 0.95, x: [1450 - 950, 1450 - 880], y: [1200 - 1050, 1200 - 1850] }, edge: 'left', render: (t) => , }, { // 顶栏 tab 条 → 右区 Home 大标题 cam: { rx: 20, ry: -20, rz: 6, scale: 0.95, x: [1500 - 800, 1500 - 2000], y: [1050 - 350, 1050 - 800] }, edge: 'top', render: (t) => , }, { // 列表行:沿 TASK NAME 行右扫 cam: { rx: 14, ry: 28, rz: -5, scale: 1.05, x: [1450 - 900, 1450 - 680], y: [1100 - 620, 1100 - 1240] }, edge: 'left', render: (t) => , }, ]; const SEG_LEN = 50; // 每段 50 帧,总 150 const FADE = 7; const Stage: React.FC = () => { const frame = useCurrentFrame(); return ( {SEGS.map((s, i) => { const start = i * SEG_LEN; const local = frame - start; if (local < -FADE || local > SEG_LEN + FADE) return null; const t = Math.min(1, Math.max(0, local / SEG_LEN)); // 相邻段交叉淡化:后段在边界前 [-FADE,0] 淡入、前段在 [SEG_LEN-FADE, SEG_LEN] 淡出,边界帧无纯黑 const opacity = interpolate(local, [-FADE, 0, SEG_LEN - FADE, SEG_LEN], [i === 0 ? 1 : 0, 1, 1, i === SEGS.length - 1 ? 1 : 0]); return ( {s.render(t)} ); })} ); }; export const GrazeFaceTour: React.FC = () => { const frame = useCurrentFrame(); // 呼吸感的焦点带(浅景深):焦点椭圆随段落略移 const seg = Math.min(2, Math.floor(frame / SEG_LEN)); const focusX = [44, 40, 46][seg]; const focusY = [46, 40, 50][seg]; return ( {/* 屏幕空间浅景深:焦点带外整体模糊 */} {/* 暗角 + 冷调压暗 */} ); };