{
  "slug": "graze-face-tour",
  "title": "Graze face tour",
  "tagline": "A steep-angle close-up skims low over the UI surface, text initially hovers above the interface with a matching soft shadow, and as the camera moves each piece accelerates down to settle into place.",
  "description": "A steep-angle close-up skims low over the UI surface, text initially hovers above the interface with a matching soft shadow, and as the camera moves each piec",
  "category": "camera",
  "tags": [
    "shotcraft",
    "remotion",
    "camera",
    "ui-entrance"
  ],
  "tier": "free",
  "license": "Apache-2.0",
  "spec": {
    "width": 1920,
    "height": 1080,
    "fps": 30,
    "duration": 5,
    "aspect": "16:9"
  },
  "kind": "remotion",
  "params_schema": {
    "type": "object",
    "properties": {}
  },
  "default_params": {},
  "preview": {
    "poster": "https://gimgs.net/motion/media/graze-face-tour/poster-5d4921d9ba33.jpg",
    "video": "https://gimgs.net/motion/media/graze-face-tour/video-c1c8b50d0af0.mp4"
  },
  "variants": [
    {
      "key": "graze-face-tour",
      "label": "graze-face-tour · Graze face tour",
      "description": "A steep-angle close-up skims low over the UI surface, text initially hovers above the interface with a matching soft shadow, and as the camera moves each piece accelerates down to settle into place.",
      "use": "",
      "video": "https://gimgs.net/motion/media/graze-face-tour/video-c1c8b50d0af0.mp4",
      "poster": "https://gimgs.net/motion/media/graze-face-tour/poster-5d4921d9ba33.jpg"
    }
  ],
  "credits": {
    "author": "video-shotcraft (Vincent Wei)",
    "url": "https://github.com/Vincentwei1021/video-shotcraft"
  },
  "page_url": "https://gimgs.net/motion/graze-face-tour.html",
  "json_url": "https://gimgs.net/motion/graze-face-tour.json",
  "template_url": "https://gimgs.net/motion/graze-face-tour/template.html",
  "play_url": null,
  "markdown_url": "https://gimgs.net/motion/graze-face-tour.md",
  "updated_at": "2026-09-24T07:40:01.238Z",
  "prompt": "## When to use\n\nA tour of feature areas (treating the UI as terrain to skim over); pairs with a dark scene and neon rim light for a \"product's inner world\" segment; interface content entering region by region.\n\n## Intention\n\nShoot the UI as terrain: a steep-angle close-up turns the sidebar tree, navigation, and list rows into passing landscape, with shallow depth of field reinforcing the \"skimming the surface\" feel. The dramatic beat is **text landing from the air**: text/components don't fade in place — they hover above the interface (raised along the 3D z-axis), casting a matching soft shadow onto the interface while airborne, then accelerate down one after another as the camera advances, the shadow converging and vanishing as height decreases — the audience watches the interface \"complete itself\" as the camera passes. The shadow is the perceptible crux: without it, hovering just reads as an ordinary positional entrance; with it, the spatial relationship is instantly established.\n\n## Duration & energy\n\n- Duration: Single segment 4–5s; can chain multiple segments to extend\n- Energy: Medium-high (sustained camera advance plus continuous element landings, high information density)\n\n## Shot recipe (original Chinese — timing, easing, parameter table, known pitfalls)\n\n## 意图\n把 UI 拍成地形：大倾角特写让侧栏树、导航、列表行变成掠过的地貌，\n浅景深强化\"贴面\"。戏眼是**文字空中贴落**：文字/组件不是原地淡入，\n而是悬浮在界面上空（3D z 轴抬高），在空中时往界面投同形软影，\n随镜头行进先后加速贴落、影子随高度收敛消失——观众看见界面在\n镜头经过时\"完成自己\"。影子是可感命门：没有影子，悬浮读作\n普通位移入场；有影子，空间关系瞬间成立。\n\n## 动效核心\n- 机位：CSS perspective + rotateX/rotateY 大倾角（面几乎侧掠），\n  沿界面一条路径持续推进；浅景深＝远处 blur 渐强\n- 悬浮元素：初始悬浮高度 H≈120–180px（沿面法线方向抬起），\n  easeFall 加速下落+软着陆（落地不回弹或极微回弹）\n- 同形软影：元素原位渲染模糊压暗投影，blur/offset/opacity 全部\n  随当前悬浮高度变化（高＝影大而糊而淡，落地＝影与本体重合消失）\n- 错峰贴落：各元素落地时刻按镜头行进方向排布（先经过的先落），\n  界面在镜头前\"依次完成\"\n- 长镜可拆段接力：段间用相邻段交叉淡化（判例：透明度窗口不重叠\n  会产生黑闪帧）\n\n## 参数表\n| 参数 | 典型值 | 调节手感 |\n|------|--------|----------|\n| 悬浮高度 | 120–180px | <80px 影子和位移都读不出；>250px 落地砸得太重 |\n| 影子参数 | blur/offset/opacity 随高度线性收敛 | 影子恒定不随高度变＝贴纸感；落地影必须归零 |\n| 贴落错峰 | 按镜头行进方向 0.2→0.85 归一化排布 | 同帧齐落读作开关；乱序落读作 bug；**错峰=错开起点但下落过程重叠并行**，串行等待（一个落完另一个才开始）被裁（runway 判例） |\n| 下落曲线 | easeFall 加速+软着陆 | ease-out 下落读作气球飘落，重力感靠加速 |\n| 段间接力 | 相邻段交叉淡化 ≥8f | 透明度窗口不搭接会黑闪（返工判例 f100 纯黑帧） |\n| 景深 | 远处 blur 渐强 | 全清晰读作平面滚动，浅景深才有\"贴面\"体感 |\n\n## 已知坑\n- demo 在灰阶/占位素材上调校通过——参数是调校起点非实战定稿，\n  首次实战须以真实素材回验\n- 悬空元素别被环境压黑带吞掉（邻候选 runway 判例）：空中元素\n  需追光/提亮 ~1.3x，否则悬浮状态不可感等于没做\n- 与 space-camera-moves/tension-camera-moves 分工：那两张是相机\n  语言本身；本卡=贴面机位 **+ 元素贴落** 的组合签名，缺一半就\n  改用相机卡\n- **贴落节奏语义判例**（与 neon-frame-orbit-drop 互为反例）：\n  本卡巡礼镜=错峰并行；整体登场镜必须全体同帧齐落（那张卡的\n  错峰版被裁）。选错语法两边都会被裁\n- 原片（clickup-30）是一条连续上行长镜；复刻用三段接力，段落\n  切换藏在交叉淡化里——实战若做真单长镜，路径规划成本高得多\n\n## 参考实现\ndemos/camera/graze-face-tour/\n（GrazeFaceTour.tsx）\n原片出处：clickup-30.mp4\n\n## Reference implementation (Remotion)\n\n- demos/camera/graze-face-tour/GrazeFaceTour.tsx — https://raw.githubusercontent.com/Vincentwei1021/video-shotcraft/main/demos/camera/graze-face-tour/GrazeFaceTour.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/camera/graze-face-tour/GrazeFaceTour.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/camera/graze-face-tour/GrazeFaceTour.tsx",
      "url": "https://raw.githubusercontent.com/Vincentwei1021/video-shotcraft/main/demos/camera/graze-face-tour/GrazeFaceTour.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": 150,
    "repo": "https://github.com/Vincentwei1021/video-shotcraft",
    "files": [
      "https://raw.githubusercontent.com/Vincentwei1021/video-shotcraft/main/demos/camera/graze-face-tour/GrazeFaceTour.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/camera/graze-face-tour/GrazeFaceTour.tsx =====\n// graze-face-tour v2 —— 源片 clickup-30.mp4 约 28.5–33s：\n// 相机大倾角贴着 UI 表面游走特写，三段接力：侧栏树 → 顶部 tab 条 → 列表行。\n// v2（用户意见）：页面文字初始悬浮在界面上空（3D 抬高），空中时在 UI 面上\n// 投模糊同形软影；随镜头推进先后落贴回界面，影子随高度收敛消失。\nimport React from 'react';\nimport { AbsoluteFill, interpolate, useCurrentFrame, Easing } from 'remotion';\n\nconst FONT = 'Helvetica, Arial, sans-serif';\nconst INK = '#3a3a40';\nconst MID = '#8b8b92';\nconst FAINT = '#c8c8ce';\n\nconst easeIO = Easing.bezier(0.45, 0, 0.25, 1);\nconst easeFall = Easing.bezier(0.5, 0.05, 0.6, 1); // 加速下落、末端软着陆\n\n/* ---------- 悬浮 + 同形软影 ----------\n * h=悬浮高度(px)。文字整体向上（屏幕上-左）抬起，\n * 原位置留一份模糊压暗的同形投影；h→0 时两者重合、影子消失。 */\nconst FloatWrap: React.FC<{ h: number; children: React.ReactNode }> = ({ h, children }) => (\n  <div style={{ position: 'relative' }}>\n    {h > 1.5 && (\n      <div style={{\n        position: 'absolute', inset: 0,\n        transform: `translate(${h * 0.22}px, ${h * 0.42}px) scale(${1 + h * 0.0011})`,\n        filter: `blur(${3.5 + h * 0.085}px) brightness(0.32) saturate(0.4)`,\n        opacity: Math.min(0.38, 0.16 + h * 0.004),\n        pointerEvents: 'none',\n      }}>{children}</div>\n    )}\n    <div style={{ transform: `translate(${-h * 0.34}px, ${-h * 0.78}px)` }}>{children}</div>\n  </div>\n);\n\n/* 每行的悬浮高度：land = 该行贴回完成的段内时刻(0..1)，之前从 H 高度加速落下 */\nconst liftOf = (t: number, land: number, H = 120) => {\n  const FALL = 0.34;\n  const p = Math.min(1, Math.max(0, (t - (land - FALL)) / FALL));\n  return (1 - easeFall(p)) * H;\n};\n\n/* ---------- 灰阶自绘 UI 素材 ---------- */\n\nconst Chip: React.FC<{ letter: string; tone: string }> = ({ letter, tone }) => (\n  <div style={{\n    width: 64, height: 64, borderRadius: 16, background: tone,\n    display: 'flex', alignItems: 'center', justifyContent: 'center',\n    fontFamily: FONT, fontWeight: 700, fontSize: 36, color: '#666',\n  }}>{letter}</div>\n);\n\nconst Tri: React.FC<{ open?: boolean }> = ({ open }) => (\n  <div style={{\n    width: 0, height: 0,\n    borderLeft: open ? '16px solid transparent' : '22px solid #9a9aa0',\n    borderRight: open ? '16px solid transparent' : '0 solid transparent',\n    borderTop: open ? '22px solid #9a9aa0' : '14px solid transparent',\n    borderBottom: open ? '0' : '14px solid transparent',\n  }} />\n);\n\nconst DocIcon: React.FC = () => (\n  <div style={{ width: 44, height: 54, border: '5px solid #9a9aa0', borderRadius: 8, position: 'relative' }}>\n    <div style={{ position: 'absolute', left: 7, top: 10, width: 22, height: 5, background: '#b6b6bc' }} />\n    <div style={{ position: 'absolute', left: 7, top: 22, width: 22, height: 5, background: '#b6b6bc' }} />\n  </div>\n);\n\nconst FolderIcon: React.FC = () => (\n  <div style={{ width: 54, height: 42, background: '#8f8f95', borderRadius: 7, position: 'relative' }}>\n    <div style={{ position: 'absolute', left: 0, top: -10, width: 24, height: 12, background: '#8f8f95', borderRadius: '6px 6px 0 0' }} />\n  </div>\n);\n\nconst TreeRow: React.FC<{\n  depth: number; label: string; icon?: 'tri' | 'triOpen' | 'doc' | 'folder' | 'dash' | 'chip';\n  chip?: string; count?: string; size?: number; dim?: boolean;\n}> = ({ depth, label, icon, chip, count, size = 58, dim }) => (\n  <div style={{\n    display: 'flex', alignItems: 'center', gap: 30, paddingLeft: 40 + depth * 90,\n    height: size * 2.1, opacity: dim ? 0.35 : 1,\n  }}>\n    {icon === 'tri' && <Tri />}\n    {icon === 'triOpen' && <Tri open />}\n    {icon === 'doc' && <DocIcon />}\n    {icon === 'folder' && <FolderIcon />}\n    {icon === 'dash' && <div style={{ width: 40, height: 40, border: '6px dashed #a2a2a8', borderRadius: 10 }} />}\n    {chip && <Chip letter={chip} tone=\"#d4d4da\" />}\n    <div style={{ fontFamily: FONT, fontSize: size, color: INK, fontWeight: 500 }}>{label}</div>\n    {count && <div style={{ marginLeft: 'auto', marginRight: 80, fontFamily: FONT, fontSize: size * 0.85, color: MID }}>{count}</div>}\n  </div>\n);\n\nconst RecentCard: React.FC<{ title: string; sub: string; w?: number }> = ({ title, sub, w = 880 }) => (\n  <div style={{\n    width: w, border: `4px solid ${FAINT}`, borderRadius: 24, padding: '36px 44px',\n    display: 'flex', flexDirection: 'column', gap: 18, background: '#fbfbfa',\n  }}>\n    <div style={{ display: 'flex', alignItems: 'center', gap: 26 }}>\n      <div style={{ width: 44, height: 44, border: '6px solid #85858b', borderRadius: 8 }} />\n      <div style={{ fontFamily: FONT, fontSize: 52, color: INK, fontWeight: 600 }}>{title}</div>\n    </div>\n    <div style={{ fontFamily: FONT, fontSize: 42, color: MID, paddingLeft: 70 }}>{sub}</div>\n  </div>\n);\n\n/* 场景 A：侧栏 SPACES 树 + 右侧 Recent 卡（对标截图 1/2/4）\n * v2：树行/卡片按镜头行进方向（自上而下）先后从空中落贴回界面 */\nconst SceneTree: React.FC<{ t?: number }> = ({ t = 1 }) => {\n  // 树行落地时刻：镜头由树顶滑向树底，行 index 越大越晚落\n  const L = (i: number, n = 14) => liftOf(t, 0.22 + (i / n) * 0.62, 130);\n  const rows: [number, string, 'tri' | 'triOpen' | 'doc' | 'folder' | 'dash' | undefined, string | undefined, string | undefined][] = [\n    [0, 'People & Teams', 'doc', undefined, undefined],\n    [0, 'Goals', 'doc', undefined, undefined],\n    [0, 'Docs', 'doc', undefined, undefined],\n    [0, 'More', 'dash', undefined, undefined],\n    [0, 'EPD', 'tri', 'E', undefined],\n    [0, 'Product roadmap', 'tri', 'P', undefined],\n    [0, 'Design', 'triOpen', 'D', undefined],\n    [1, 'Designer handbook', 'doc', undefined, undefined],\n    [1, '3.0', 'folder', undefined, undefined],\n    [1, 'Design system', 'folder', undefined, undefined],\n    [2, 'Design system', 'doc', undefined, undefined],\n    [2, 'Components', 'dash', undefined, '56'],\n    [2, 'Patterns', 'dash', undefined, '8'],\n    [2, 'Tokens', 'dash', undefined, '256'],\n  ];\n  return (\n    <div style={{ width: 2900, height: 2400, background: '#f5f5f4', display: 'flex' }}>\n      <div style={{ width: 1500, borderRight: `4px solid ${FAINT}`, paddingTop: 60, background: '#f2f2f1' }}>\n        {rows.slice(0, 4).map((r, i) => (\n          <FloatWrap key={r[1] + i} h={L(i)}>\n            <TreeRow depth={r[0]} label={r[1]} icon={r[2]} chip={r[3]} count={r[4]} />\n          </FloatWrap>\n        ))}\n        <div style={{ height: 90 }} />\n        <FloatWrap h={L(4)}>\n          <div style={{ paddingLeft: 48, fontFamily: FONT, fontSize: 46, letterSpacing: 6, color: MID, fontWeight: 600 }}>SPACES</div>\n        </FloatWrap>\n        <div style={{ height: 30 }} />\n        {rows.slice(4).map((r, i) => (\n          <FloatWrap key={r[1] + i} h={L(i + 4.6)}>\n            <TreeRow depth={r[0]} label={r[1]} icon={r[2]} chip={r[3]} count={r[4]} />\n          </FloatWrap>\n        ))}\n      </div>\n      <div style={{ flex: 1, paddingTop: 100, paddingLeft: 110 }}>\n        <FloatWrap h={liftOf(t, 0.3, 150)}>\n          <div style={{ display: 'flex', gap: 90, fontFamily: FONT, fontSize: 52 }}>\n            <div style={{ color: INK, fontWeight: 700 }}>Recent</div>\n            <div style={{ color: MID }}>Favorites</div>\n          </div>\n        </FloatWrap>\n        <div style={{ height: 64 }} />\n        <div style={{ display: 'flex', flexDirection: 'column', gap: 44 }}>\n          <FloatWrap h={liftOf(t, 0.42, 170)}>\n            <RecentCard title=\"Logo\" sub=\"Brand refresh\" />\n          </FloatWrap>\n          <FloatWrap h={liftOf(t, 0.55, 170)}>\n            <RecentCard title=\"Split.io Access for Oleg\" sub=\"Team credentials\" />\n          </FloatWrap>\n        </div>\n        <div style={{ height: 110 }} />\n        <FloatWrap h={liftOf(t, 0.68, 150)}>\n          <div style={{ display: 'flex', gap: 90, fontFamily: FONT, fontSize: 50 }}>\n            <div style={{ color: INK, fontWeight: 700 }}>Todo</div>\n            <div style={{ color: MID }}>Comments</div>\n            <div style={{ color: MID }}>Done</div>\n          </div>\n        </FloatWrap>\n        <div style={{ height: 50 }} />\n        <FloatWrap h={liftOf(t, 0.8, 150)}>\n          <div style={{ display: 'flex', gap: 40, alignItems: 'center' }}>\n            <div style={{\n              padding: '22px 46px', background: '#e3e3ec', borderRadius: 18,\n              fontFamily: FONT, fontSize: 46, color: INK, fontWeight: 600,\n            }}>≡ List</div>\n            <div style={{ fontFamily: FONT, fontSize: 46, color: MID }}>▦ Gallery</div>\n          </div>\n        </FloatWrap>\n      </div>\n    </div>\n  );\n};\n\n/* 场景 B：顶部 tab 条 + 左上侧栏导航（对标截图 6/7）\n * v2：tab 条、logo、Home 行、侧栏项先后从空中贴落 */\nconst SceneTopNav: React.FC<{ t?: number }> = ({ t = 1 }) => (\n  <div style={{ width: 3000, height: 2100, background: '#f5f5f4', borderRadius: 48 }}>\n    <div style={{\n      height: 150, borderBottom: `4px solid ${FAINT}`, display: 'flex', alignItems: 'center',\n      gap: 120, paddingLeft: 90, fontFamily: FONT, fontSize: 54, color: INK,\n    }}>\n      {['Product analytics', 'ClickUp 3.0', 'Widget brainstorm', 'Design system'].map((tb, i) => (\n        <FloatWrap key={tb} h={liftOf(t, 0.2 + i * 0.1, 140)}>\n          <div style={{ display: 'flex', alignItems: 'center', gap: 26, fontWeight: i === 1 ? 700 : 400, opacity: i > 1 ? 0.75 : 1 }}>\n            <div style={{\n              width: 40, height: 40, borderRadius: i === 1 ? 14 : 8,\n              background: i === 1 ? '#7d7d84' : 'transparent',\n              border: i === 1 ? 'none' : '5px solid #9a9aa0',\n            }} />\n            {tb}\n          </div>\n        </FloatWrap>\n      ))}\n    </div>\n    <div style={{ display: 'flex' }}>\n      <div style={{ width: 1250, padding: '70px 70px 0' }}>\n        <FloatWrap h={liftOf(t, 0.34, 150)}>\n          <div style={{ display: 'flex', alignItems: 'center', gap: 30 }}>\n            <div style={{ width: 74, height: 74, borderRadius: 20, background: 'linear-gradient(135deg,#a9a9af,#6f6f76)' }} />\n            <div style={{ fontFamily: FONT, fontSize: 66, fontWeight: 800, color: INK }}>ClickUp</div>\n            <div style={{ marginLeft: 'auto', width: 130, height: 90, border: `4px solid ${FAINT}`, borderRadius: 22, background: '#fff' }} />\n          </div>\n        </FloatWrap>\n        <div style={{ height: 60 }} />\n        <FloatWrap h={liftOf(t, 0.46, 160)}>\n          <div style={{\n            display: 'flex', alignItems: 'center', gap: 34, background: '#e6e6f0',\n            border: '4px solid #c5c5d4', borderRadius: 24, padding: '30px 44px',\n          }}>\n            <div style={{ width: 48, height: 44, border: '6px solid #5f5f66', borderBottom: 'none', borderRadius: '10px 10px 0 0' }} />\n            <div style={{ fontFamily: FONT, fontSize: 56, color: '#4a4a55', fontWeight: 600 }}>Home</div>\n            <div style={{\n              marginLeft: 'auto', width: 62, height: 62, borderRadius: 31, background: '#a5a5ab',\n              color: '#fff', fontFamily: FONT, fontSize: 38, fontWeight: 700,\n              display: 'flex', alignItems: 'center', justifyContent: 'center',\n            }}>3</div>\n          </div>\n        </FloatWrap>\n        {['Inbox', 'Company', 'People & Teams', 'Goals', 'Docs'].map((tb, i) => (\n          <FloatWrap key={tb} h={liftOf(t, 0.55 + i * 0.08, 140)}>\n            <div style={{ display: 'flex', alignItems: 'center', gap: 34, height: 128, paddingLeft: 44 }}>\n              <div style={{ width: 44, height: 44, border: '6px solid #93939a', borderRadius: 10 }} />\n              <div style={{ fontFamily: FONT, fontSize: 54, color: INK }}>{tb}</div>\n            </div>\n          </FloatWrap>\n        ))}\n      </div>\n      <div style={{ flex: 1, borderLeft: `4px solid ${FAINT}`, padding: '70px 90px 0' }}>\n        <FloatWrap h={liftOf(t, 0.4, 150)}>\n          <div style={{ display: 'flex', alignItems: 'center', gap: 40, color: MID, fontFamily: FONT, fontSize: 52 }}>\n            <div>‹</div><div>›</div>\n            <div style={{ width: 44, height: 40, border: '6px solid #93939a', borderBottom: 'none', borderRadius: '10px 10px 0 0' }} />\n            <div style={{ color: INK }}>Home</div>\n          </div>\n        </FloatWrap>\n        <div style={{ height: 80 }} />\n        <FloatWrap h={liftOf(t, 0.58, 180)}>\n          <div style={{ fontFamily: FONT, fontSize: 130, fontWeight: 750, color: INK }}>Home</div>\n        </FloatWrap>\n        <div style={{ height: 70 }} />\n        <FloatWrap h={liftOf(t, 0.74, 160)}>\n          <div style={{\n            display: 'flex', alignItems: 'center', gap: 36, border: `4px solid ${FAINT}`,\n            borderRadius: 26, padding: '34px 46px', background: '#fff', width: 1100,\n          }}>\n            <div style={{ width: 44, height: 44, borderRadius: 22, border: '6px solid #9a9aa0' }} />\n            <div style={{ fontFamily: FONT, fontSize: 48, color: MID }}>Search by app, filetype…</div>\n          </div>\n        </FloatWrap>\n      </div>\n    </div>\n  </div>\n);\n\n/* 场景 C：列表行（对标截图 4 右下 TODAY/TASK NAME 区）\n * v2：Todo 头/TODAY 徽章/任务行自上而下先后贴落 */\nconst SceneListRows: React.FC<{ t?: number }> = ({ t = 1 }) => (\n  <div style={{ width: 2900, height: 2200, background: '#f6f6f5', paddingTop: 60 }}>\n    <FloatWrap h={liftOf(t, 0.22, 150)}>\n      <div style={{ display: 'flex', gap: 100, paddingLeft: 120, fontFamily: FONT, fontSize: 54 }}>\n        <div style={{ color: INK, fontWeight: 700 }}>Todo</div>\n        <div style={{ color: MID }}>Comments</div>\n        <div style={{ color: MID }}>Done</div>\n        <div style={{ color: MID }}>Delegated</div>\n      </div>\n    </FloatWrap>\n    <div style={{ height: 56 }} />\n    <FloatWrap h={liftOf(t, 0.32, 150)}>\n      <div style={{ display: 'flex', gap: 44, alignItems: 'center', paddingLeft: 120 }}>\n        <div style={{\n          padding: '24px 52px', background: '#e2e2ea', borderRadius: 20,\n          fontFamily: FONT, fontSize: 50, color: INK, fontWeight: 600,\n        }}>≡ List</div>\n        <div style={{ fontFamily: FONT, fontSize: 50, color: MID }}>▦ Gallery</div>\n        <div style={{ marginLeft: 500, display: 'flex', gap: 80, color: MID, fontFamily: FONT, fontSize: 46 }}>\n          <div>Filter</div><div>Group</div><div>Sort</div>\n        </div>\n      </div>\n    </FloatWrap>\n    <div style={{ height: 40, borderBottom: `4px solid ${FAINT}`, marginLeft: 120, marginRight: 120 }} />\n    <div style={{ height: 60 }} />\n    <FloatWrap h={liftOf(t, 0.44, 160)}>\n      <div style={{\n        marginLeft: 120, display: 'inline-block', padding: '20px 48px', background: '#e4e4e3',\n        borderRadius: 16, fontFamily: FONT, fontSize: 44, letterSpacing: 4, color: '#6f6f75', fontWeight: 600,\n      }}>TODAY</div>\n    </FloatWrap>\n    <div style={{ height: 60 }} />\n    <FloatWrap h={liftOf(t, 0.54, 150)}>\n      <div style={{ paddingLeft: 120, fontFamily: FONT, fontSize: 42, letterSpacing: 5, color: MID }}>TASK NAME</div>\n    </FloatWrap>\n    <div style={{ height: 30 }} />\n    {['New Bugs Per Week', 'Designer handbook', 'Mobile screens', 'Product roadmap'].map((tb, i) => (\n      <FloatWrap key={tb} h={liftOf(t, 0.62 + i * 0.09, 160)}>\n        <div style={{\n          display: 'flex', alignItems: 'center', gap: 44, height: 170, marginLeft: 120, marginRight: 120,\n          borderBottom: '3px solid #e6e6e4',\n        }}>\n          <div style={{ width: 40, height: 40, borderRadius: 12, background: '#88888e' }} />\n          <div style={{ fontFamily: FONT, fontSize: 58, color: INK, fontWeight: 550 }}>{tb}</div>\n          <div style={{ marginLeft: 'auto', width: 220, height: 20, background: '#e2e2e8', borderRadius: 10 }} />\n          <div style={{ color: MID, fontSize: 60 }}>…</div>\n        </div>\n      </FloatWrap>\n    ))}\n  </div>\n);\n\n/* ---------- 3D 摄影 ---------- */\n\ntype Cam = {\n  rx: number; ry: number; rz: number; scale: number;\n  x: [number, number]; y: [number, number];\n};\n\nconst Plane: React.FC<{\n  cam: Cam; t: number; edge?: 'left' | 'top'; children: React.ReactNode;\n}> = ({ cam, t, edge = 'left', children }) => {\n  const x = interpolate(t, [0, 1], cam.x, { easing: easeIO });\n  const y = interpolate(t, [0, 1], cam.y, { easing: easeIO });\n  return (\n    <AbsoluteFill style={{ perspective: 1050, perspectiveOrigin: '50% 46%' }}>\n      <div style={{\n        position: 'absolute', left: '50%', top: '50%', width: 0, height: 0,\n        transformStyle: 'preserve-3d',\n        transform: `scale(${cam.scale}) rotateX(${cam.rx}deg) rotateY(${cam.ry}deg) rotateZ(${cam.rz}deg)`,\n      }}>\n        <div style={{ position: 'absolute', transform: `translate3d(${x}px, ${y}px, 0)` }}>\n          <div style={{ position: 'relative', transform: 'translate(-50%, -50%)' }}>\n            {/* 屏幕边缘霓虹晕染 */}\n            {edge === 'left' ? (\n              <>\n                <div style={{\n                  position: 'absolute', left: -70, top: -40, width: 110, height: '104%',\n                  background: 'linear-gradient(185deg, #ff7ab8, #b06cff 55%, #6d4dff)',\n                  filter: 'blur(70px)', opacity: 0.9,\n                }} />\n                <div style={{\n                  position: 'absolute', left: -10, top: 0, width: 8, height: '100%',\n                  background: 'linear-gradient(180deg, #ffb0d5, #b78cff)', filter: 'blur(3px)', opacity: 0.95,\n                }} />\n              </>\n            ) : (\n              <>\n                <div style={{\n                  position: 'absolute', left: -40, top: -70, width: '104%', height: 110,\n                  background: 'linear-gradient(90deg, #ff7ab8, #b06cff 55%, #6d4dff)',\n                  filter: 'blur(70px)', opacity: 0.85,\n                }} />\n                <div style={{\n                  position: 'absolute', left: 0, top: -10, width: '100%', height: 8,\n                  background: 'linear-gradient(90deg, #ffb0d5, #b78cff)', filter: 'blur(3px)', opacity: 0.9,\n                }} />\n              </>\n            )}\n            {children}\n            {/* 远端压暗：贴面透视里远处渐暗 */}\n            <div style={{\n              position: 'absolute', inset: 0,\n              background: edge === 'left'\n                ? 'linear-gradient(105deg, rgba(0,0,10,0) 30%, rgba(0,0,10,0.35) 75%, rgba(0,0,10,0.6) 100%)'\n                : 'linear-gradient(175deg, rgba(0,0,10,0) 35%, rgba(0,0,10,0.3) 80%, rgba(0,0,10,0.55) 100%)',\n              pointerEvents: 'none',\n            }} />\n          </div>\n        </div>\n      </div>\n    </AbsoluteFill>\n  );\n};\n\n/* 背景霓虹描边矩形（暗场道具） */\nconst NeonRects: React.FC<{ drift: number }> = ({ drift }) => (\n  <AbsoluteFill style={{ overflow: 'hidden' }}>\n    <div style={{\n      position: 'absolute', left: -140 + drift * 40, top: 240, width: 620, height: 380,\n      border: '4px solid #c04dff', borderRadius: 34, filter: 'blur(7px)', opacity: 0.5,\n    }} />\n    <div style={{\n      position: 'absolute', left: 60 + drift * 25, top: 700, width: 420, height: 260,\n      border: '4px solid #ff4da8', borderRadius: 28, filter: 'blur(10px)', opacity: 0.4,\n    }} />\n    <div style={{\n      position: 'absolute', right: -180 - drift * 30, top: -80, width: 560, height: 340,\n      border: '4px solid #7b4dff', borderRadius: 30, filter: 'blur(12px)', opacity: 0.35,\n    }} />\n  </AbsoluteFill>\n);\n\n// 平移目标以内容坐标 (cx,cy) 给出：translate = (W/2-cx, H/2-cy)\nconst SEGS: { cam: Cam; edge: 'left' | 'top'; render: (t: number) => React.ReactNode }[] = [\n  {\n    // 侧栏树：从树顶（SPACES 附近）贴面滑到树底（Components/Patterns/Tokens）\n    cam: { rx: 12, ry: 30, rz: -6, scale: 0.95, x: [1450 - 950, 1450 - 880], y: [1200 - 1050, 1200 - 1850] },\n    edge: 'left', render: (t) => <SceneTree t={t} />,\n  },\n  {\n    // 顶栏 tab 条 → 右区 Home 大标题\n    cam: { rx: 20, ry: -20, rz: 6, scale: 0.95, x: [1500 - 800, 1500 - 2000], y: [1050 - 350, 1050 - 800] },\n    edge: 'top', render: (t) => <SceneTopNav t={t} />,\n  },\n  {\n    // 列表行：沿 TASK NAME 行右扫\n    cam: { rx: 14, ry: 28, rz: -5, scale: 1.05, x: [1450 - 900, 1450 - 680], y: [1100 - 620, 1100 - 1240] },\n    edge: 'left', render: (t) => <SceneListRows t={t} />,\n  },\n];\n\nconst SEG_LEN = 50; // 每段 50 帧，总 150\nconst FADE = 7;\n\nconst Stage: React.FC = () => {\n  const frame = useCurrentFrame();\n  return (\n    <AbsoluteFill style={{ background: '#060608' }}>\n      {SEGS.map((s, i) => {\n        const start = i * SEG_LEN;\n        const local = frame - start;\n        if (local < -FADE || local > SEG_LEN + FADE) return null;\n        const t = Math.min(1, Math.max(0, local / SEG_LEN));\n        // 相邻段交叉淡化：后段在边界前 [-FADE,0] 淡入、前段在 [SEG_LEN-FADE, SEG_LEN] 淡出，边界帧无纯黑\n        const opacity = interpolate(local, [-FADE, 0, SEG_LEN - FADE, SEG_LEN], [i === 0 ? 1 : 0, 1, 1, i === SEGS.length - 1 ? 1 : 0]);\n        return (\n          <AbsoluteFill key={i} style={{ opacity }}>\n            <NeonRects drift={t} />\n            <Plane cam={s.cam} t={t} edge={s.edge}>{s.render(t)}</Plane>\n          </AbsoluteFill>\n        );\n      })}\n    </AbsoluteFill>\n  );\n};\n\nexport const GrazeFaceTour: React.FC = () => {\n  const frame = useCurrentFrame();\n  // 呼吸感的焦点带（浅景深）：焦点椭圆随段落略移\n  const seg = Math.min(2, Math.floor(frame / SEG_LEN));\n  const focusX = [44, 40, 46][seg];\n  const focusY = [46, 40, 50][seg];\n  return (\n    <AbsoluteFill style={{ background: '#060608' }}>\n      <Stage />\n      {/* 屏幕空间浅景深：焦点带外整体模糊 */}\n      <AbsoluteFill style={{\n        filter: 'blur(16px) brightness(0.92)',\n        WebkitMaskImage: `radial-gradient(ellipse 58% 52% at ${focusX}% ${focusY}%, transparent 34%, rgba(0,0,0,0.85) 72%, black 92%)`,\n        maskImage: `radial-gradient(ellipse 58% 52% at ${focusX}% ${focusY}%, transparent 34%, rgba(0,0,0,0.85) 72%, black 92%)`,\n      }}>\n        <Stage />\n      </AbsoluteFill>\n      {/* 暗角 + 冷调压暗 */}\n      <AbsoluteFill style={{\n        background: 'radial-gradient(ellipse 90% 80% at 50% 45%, transparent 40%, rgba(2,2,6,0.55) 78%, rgba(1,1,4,0.9) 100%)',\n        pointerEvents: 'none',\n      }} />\n    </AbsoluteFill>\n  );\n};\n\n\n"
}