{
  "slug": "cursor-flyover",
  "title": "Cursor Flyover Tour",
  "tagline": "After a full-page overhead view fades in, the camera flies in turn to zoom-in close-ups on four corners, an SVG cursor tracking along to point and leave click ripples.",
  "description": "After a full-page overhead view fades in, the camera flies in turn to zoom-in close-ups on four corners, an SVG cursor tracking along to point and leave click",
  "category": "camera",
  "tags": [
    "shotcraft",
    "remotion",
    "camera"
  ],
  "tier": "free",
  "license": "Apache-2.0",
  "spec": {
    "width": 1920,
    "height": 1080,
    "fps": 30,
    "duration": 6.5,
    "aspect": "16:9"
  },
  "kind": "remotion",
  "params_schema": {
    "type": "object",
    "properties": {}
  },
  "default_params": {},
  "preview": {
    "poster": "https://gimgs.net/motion/media/cursor-flyover/poster-d4e5049b76e9.jpg",
    "video": "https://gimgs.net/motion/media/cursor-flyover/video-1b99520ae1b4.mp4"
  },
  "variants": [
    {
      "key": "cursor-flyover",
      "label": "cursor-flyover",
      "description": "After a full-page overhead view fades in, the camera flies in turn to zoom-in close-ups on four corners, an SVG cursor tracking along to point and leave click ripples.",
      "use": "",
      "video": "https://gimgs.net/motion/media/cursor-flyover/video-1b99520ae1b4.mp4",
      "poster": "https://gimgs.net/motion/media/cursor-flyover/poster-d4e5049b76e9.jpg"
    }
  ],
  "credits": {
    "author": "video-shotcraft (Vincent Wei)",
    "url": "https://github.com/Vincentwei1021/video-shotcraft"
  },
  "page_url": "https://gimgs.net/motion/cursor-flyover.html",
  "json_url": "https://gimgs.net/motion/cursor-flyover.json",
  "template_url": "https://gimgs.net/motion/cursor-flyover/template.html",
  "play_url": null,
  "markdown_url": "https://gimgs.net/motion/cursor-flyover.md",
  "updated_at": "2026-09-24T07:39:53.468Z",
  "prompt": "## When to use\n\nA single-page product feature tour: one continuous shot walking viewers through four functional areas, with the cursor acting as a \"tour guide's finger.\"\n\n## Intention\n\nA shot-saving alternative to \"four features, four cuts\": one continuous camera strings the four corners into a single tour route, with the cursor acting as the viewer's attention proxy — wherever it goes and clicks, that's where the viewer looks. Spatial continuity also lets the positional relationships between features get remembered along the way.\n\n## Duration & energy\n\n- Duration: About 6.0s (180f@30fps; overhead 0–1.2s · four steps, each 0.7s transition + 0.5s dwell).\n- Energy: Medium (a steady cruise, with rhythm punctuated by click ripples).\n\n## Shot recipe (original Chinese — timing, easing, parameter table, known pitfalls)\n\n## 意图\n替代\"四个功能四段剪辑\"的省镜方案：一个连续相机把四个角串成一条参观动线，光标是观众的注意力代理——它到哪、点哪，观众就看哪。空间连续让功能之间的位置关系也被顺带记住。\n\n## 动效核心\n- 相机是查表步进：CAM[0] 俯瞰（scale 0.8 居中）→ 四角特写（scale 1.72，tx/ty 各角），五通道 `tx/ty/s/cx/cy` 用 `acc` + inOutCubic 插值\n- 变换公式 `translate(50-tx*s%, 50-ty*s%) scale(s)`——tx/ty 是\"相机对准点\"，改表即换巡览路线\n- 四段窗口 `[0.20,0.32][0.40,0.52][0.60,0.72][0.79,0.91]`：过渡 0.12 + 停留 0.08，等长节奏像\"参观脚步\"\n- 光标（SVG 箭头 + 投影）坐标 cx/cy 与相机同表同插值——**相机和光标是一个系统**，永远同时到位；光标 `scale(1/s)` 抵消画面缩放保持视觉等大\n- 每段落位瞬间（WIN[i][1] 起 0.055）触发点击涟漪：26px 圆环从 0.3 扩到 1.9 倍并淡出，光标头同拍闪白\n- 开场 0–0.14 整页从 blur 5px 淡入聚焦\n\n## 参数表\n| 参数 | 典型值 | 调节手感 |\n|------|--------|----------|\n| 特写倍率 | s=1.72（俯瞰 0.8） | >2.2 时占位截图纹理会糊，换真实高分截图后可推到 2.5 |\n| 步进节奏 | 过渡 0.12 / 停留 0.08 | 停留是读功能位，短于 0.05 变\"扫过\"没有\"参观\" |\n| 点击涟漪 | 0.055 时长，scale 0.3→1.9 | 涟漪是节拍器，四次点击均匀落在 0.32/0.52/0.72/0.91——配 SFX click 四连 |\n| 光标补偿 | scale(1/s) | 砍掉则特写时光标巨大化；保持\"光标是 UI 层\"的错觉必需 |\n| 巡览顺序 | 左上→右上→右下→左下（Z 形反向） | 改 CAM 表即换路线；保持相邻角相邻访问，对角跳跃会晕 |\n| 开场聚焦 | 0–0.14 blur 5px→0 | 与俯瞰同拍完成\"这是全景\"的交代；直接从特写开跳过它 |\n\n## 已知坑\n- 占位截图（窗口 chrome+侧栏+四象限控件）必须换成真实产品截图，四个 cx/cy 光标落点要重新标到真实控件中心\n- 相机表的 tx/ty 是内容百分比坐标，截图宽高比变了（如换竖屏）需整表重标\n- 光标与相机绑定同一插值意味着\"光标先飞相机后到\"这类编排做不了——需要解耦时给光标单独一张同结构的表\n- 停留段完全静止，1080p 大屏下会显得\"卡住\"——可给 world 加 ±0.5px 的呼吸位移（确定性 sin），但幅度别过 1px\n\n## 参考实现\ndemos/camera/cursor-flyover/\n（CursorFlyover.tsx）\n\n## Reference implementation (Remotion)\n\n- demos/camera/cursor-flyover/CursorFlyover.tsx — https://raw.githubusercontent.com/Vincentwei1021/video-shotcraft/main/demos/camera/cursor-flyover/CursorFlyover.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/cursor-flyover/CursorFlyover.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/cursor-flyover/CursorFlyover.tsx",
      "url": "https://raw.githubusercontent.com/Vincentwei1021/video-shotcraft/main/demos/camera/cursor-flyover/CursorFlyover.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": 195,
    "repo": "https://github.com/Vincentwei1021/video-shotcraft",
    "files": [
      "https://raw.githubusercontent.com/Vincentwei1021/video-shotcraft/main/demos/camera/cursor-flyover/CursorFlyover.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/cursor-flyover/CursorFlyover.tsx =====\n// cursor-flyover — Cursor Flyover 四角巡览指点（motion-lab 定稿转原生 Remotion）\n// 产品截图平铺，相机先整体俯瞰淡入，再依次飞到四个角落 zoom-in 特写；一枚带阴影\n// 的 SVG 光标跟到对应区域指点并留下点击涟漪。每步过渡+停留等长，easeInOutCubic。\n// 设计坐标 480×270（DesignStage 等比放大），参数表数值以此坐标系标定。\nimport React from 'react';\nimport { DesignStage, E, lerp, rand, seg, useT } from '../../_fixtures/Motion';\n\nexport const CURSOR_FLYOVER_DURATION = 180; // 6000ms @30fps\n\n// 关键帧累加器：base 起步，每段 [at0,at1] 内朝 to 推进（效果源文件级共享 helper）\ntype Vec = Record<string, number>;\nconst acc = (t: number, base: Vec, kfs: { at: number[]; to: Vec }[], keys: string[], ease: (x: number) => number) => {\n  const out: Vec = {};\n  for (const k of keys) out[k] = base[k];\n  let prev = base;\n  for (const kf of kfs) {\n    const u = seg(t, kf.at[0], kf.at[1], ease);\n    for (const k of keys) out[k] += u * (kf.to[k] - prev[k]);\n    prev = kf.to;\n  }\n  return out;\n};\n\n// 相机 Step：overview + 四角；光标目标（world %）\nconst CAM = [\n  { tx: 50, ty: 50, s: 0.8,  cx: 50, cy: 52 },\n  { tx: 37, ty: 32, s: 1.72, cx: 41, cy: 38 },\n  { tx: 77, ty: 32, s: 1.72, cx: 82, cy: 36 },\n  { tx: 77, ty: 72, s: 1.72, cx: 71, cy: 76 },\n  { tx: 37, ty: 72, s: 1.72, cx: 33, cy: 78 },\n];\nconst WIN = [[0.20, 0.32], [0.40, 0.52], [0.60, 0.72], [0.79, 0.91]];\nconst KEY = ['tx', 'ty', 's', 'cx', 'cy'];\n\n// 右上折线图路径（种子与 effect.js 相同）\nconst LINE_D = (() => {\n  let dd = '';\n  for (let i = 0; i <= 14; i++) dd += `${i === 0 ? 'M' : 'L'}${i * 7.14},${40 - (6 + rand(i * 5) * 22 + i * 0.7)} `;\n  return dd;\n})();\n\n// 四象限容器（截图内相对定位）\nconst Quad: React.FC<{ x: number; y: number; w: number; h: number; children?: React.ReactNode }> = ({\n  x, y, w, h, children,\n}) => (\n  <div\n    style={{\n      position: 'absolute',\n      left: `${x}%`,\n      top: `${y}%`,\n      width: `${w}%`,\n      height: `${h}%`,\n      borderRadius: 8,\n      background: '#191d29',\n      boxShadow: 'inset 0 0 0 1px #262c3b',\n    }}\n  >\n    {children}\n  </div>\n);\n\nexport const CursorFlyover: React.FC = () => {\n  const t = useT();\n\n  const v = acc(t, CAM[0], WIN.map((w, i) => ({ at: w, to: CAM[i + 1] })), KEY, E.inOutCubic);\n  const fade = seg(t, 0, 0.14, E.outCubic);\n\n  // 每次落位时的点击涟漪\n  let click = 0;\n  let hold = 0;\n  for (let i = 0; i < WIN.length; i++) {\n    const c = seg(t, WIN[i][1], WIN[i][1] + 0.055, E.outCubic);\n    if (c > 0 && c < 1) { click = c; hold = 1; }\n    else if (c >= 1 && t < (WIN[i + 1] ? WIN[i + 1][0] : 1.01)) { click = 1; hold = 1; }\n  }\n\n  return (\n    <DesignStage bg=\"#080910\" raster=\"zoom\">\n      <div style={{ position: 'absolute', inset: 0, background: '#080910', overflow: 'hidden' }}>\n        {/* world：相机 translate+scale 的载体，淡入由 opacity 承担 */}\n        <div\n          style={{\n            position: 'absolute',\n            inset: 0,\n            transformOrigin: '0 0',\n            transform: `translate(${50 - v.tx * v.s}%,${50 - v.ty * v.s}%) scale(${v.s})`,\n            opacity: fade,\n          }}\n        >\n          {/* ---- 截图占位：窗口 chrome + 侧栏 + 四象限控件 ---- */}\n          <div\n            style={{\n              position: 'absolute',\n              left: '4%',\n              top: '5%',\n              width: '92%',\n              height: '90%',\n              borderRadius: 10,\n              background: '#12141d',\n              boxShadow: '0 24px 60px rgba(0,0,0,.6),inset 0 0 0 1px #262c3b',\n              overflow: 'hidden',\n              filter: `blur(${(1 - fade) * 5}px)`,\n            }}\n          >\n            {/* 顶栏：红黄绿灯 + URL 胶囊（原采集页 content-box：border-bottom 在 9% 高度之外） */}\n            <div style={{ position: 'absolute', left: 0, top: 0, width: '100%', height: '9%', background: '#1a1d28', borderBottom: '1px solid #262c3b', boxSizing: 'content-box' }}>\n              {['#ff6058', '#ffbd2e', '#28ca42'].map((c, i) => (\n                <div\n                  key={c}\n                  style={{\n                    position: 'absolute', left: 8 + i * 12, top: '50%', width: 6, height: 6, marginTop: -3,\n                    borderRadius: '50%', background: c,\n                  }}\n                />\n              ))}\n              <div\n                style={{\n                  position: 'absolute', left: 56, top: '50%', transform: 'translateY(-50%)', height: 13, lineHeight: '13px',\n                  padding: '0 10px', borderRadius: 7, background: '#0f1119', color: '#5d6580',\n                  font: '500 8px/13px ui-monospace,monospace', letterSpacing: 0.4,\n                }}\n              >\n                app.example.com/overview\n              </div>\n            </div>\n            {/* 侧栏导航（content-box：border-right 在 15% 宽度之外） */}\n            <div style={{ position: 'absolute', left: 0, top: '9%', width: '15%', height: '91%', background: '#161923', borderRight: '1px solid #232937', boxSizing: 'content-box' }}>\n              {['Overview', 'Traffic', 'Revenue', 'Cohorts', 'Alerts'].map((s, i) => (\n                <div\n                  key={s}\n                  style={{\n                    position: 'absolute', left: '8%', top: `${8 + i * 15}%`, width: '84%', height: '11%', borderRadius: 5,\n                    font: '600 7px/1 -apple-system,sans-serif', color: i === 0 ? '#cfd6ea' : '#5a6280',\n                    display: 'flex', alignItems: 'center', paddingLeft: 6,\n                    boxSizing: 'content-box', // 原采集页 content-box：paddingLeft 加在 84% 之外，激活项底色更宽\n                    background: i === 0 ? '#222839' : 'transparent',\n                  }}\n                >\n                  {s}\n                </div>\n              ))}\n            </div>\n            {/* 左上：stat tiles */}\n            <Quad x={18} y={14} w={38} h={36}>\n              {['12.4k', '+38%', '4.1s'].map((s, i) => (\n                <div\n                  key={s}\n                  style={{\n                    position: 'absolute', left: `${5 + i * 31.5}%`, top: '16%', width: '28%', height: '44%', borderRadius: 6,\n                    background: '#202634', padding: '8px 0 0 8px', boxSizing: 'border-box',\n                  }}\n                >\n                  <div style={{ font: '700 13px/1 -apple-system,sans-serif', color: '#e7ecfb' }}>{s}</div>\n                  <div style={{ marginTop: 4, font: '600 6px/1 -apple-system,sans-serif', letterSpacing: 1, color: '#5d6580' }}>\n                    METRIC {i + 1}\n                  </div>\n                </div>\n              ))}\n            </Quad>\n            {/* 右上：折线图 */}\n            <Quad x={59} y={14} w={37} h={36}>\n              <svg viewBox=\"0 0 100 46\" style={{ position: 'absolute', left: '6%', top: '16%', width: '88%', height: '70%' }}>\n                <path d={LINE_D} fill=\"none\" stroke=\"#6c8cff\" strokeWidth={1.6} strokeLinejoin=\"round\" />\n              </svg>\n            </Quad>\n            {/* 右下：柱状图 */}\n            <Quad x={59} y={55} w={37} h={34}>\n              {Array.from({ length: 9 }, (_, i) => (\n                <div\n                  key={i}\n                  style={{\n                    position: 'absolute', left: `${7 + i * 9.8}%`, bottom: '14%', width: '6.4%',\n                    height: `${22 + rand(i * 3 + 1) * 55}%`,\n                    borderRadius: 2, background: 'linear-gradient(180deg,#c86cff,#5c4bd6)',\n                  }}\n                />\n              ))}\n            </Quad>\n            {/* 左下：表格行 */}\n            <Quad x={18} y={55} w={38} h={34}>\n              {Array.from({ length: 5 }, (_, i) => (\n                <div\n                  key={i}\n                  style={{\n                    position: 'absolute', left: '6%', top: `${12 + i * 17}%`, width: '88%', height: '11%', borderRadius: 3,\n                    background: '#202634',\n                    boxShadow: `inset ${30 + rand(i + 2) * 45}% 0 0 rgba(108,140,255,.35)`,\n                  }}\n                />\n              ))}\n            </Quad>\n          </div>\n\n          {/* ---- 点击涟漪（world 内锚定，反缩放保持屏幕尺寸；content-box：外径 26+2×2=30px 与原采集页一致） ---- */}\n          <div\n            style={{\n              position: 'absolute', left: `${v.cx}%`, top: `${v.cy}%`, width: 26, height: 26,\n              boxSizing: 'content-box',\n              borderRadius: '50%', border: '2px solid #8fa7ff', zIndex: 39, transformOrigin: '50% 50%',\n              opacity: hold ? (1 - click) * 0.85 : 0,\n              transform: `translate(-50%,-50%) scale(${(0.3 + click * 1.6) / v.s})`,\n            }}\n          />\n          {/* ---- 光标（world 内锚定，逐帧反缩放保持屏幕尺寸） ---- */}\n          <svg\n            viewBox=\"0 0 24 24\"\n            style={{\n              position: 'absolute', left: `${v.cx}%`, top: `${v.cy}%`, width: 22, height: 22, transformOrigin: '0 0',\n              filter: 'drop-shadow(0 3px 5px rgba(0,0,0,.75))', pointerEvents: 'none', zIndex: 40,\n              transform: `scale(${1 / v.s})`,\n            }}\n          >\n            <path\n              d=\"M4 2 L4 19 L9 14.4 L12.2 21.5 L15.4 20 L12.2 13 L19 12.6 Z\"\n              fill={click > 0 && click < 0.4 ? '#dfe6ff' : '#fff'}\n              stroke=\"#101320\"\n              strokeWidth={1.1}\n            />\n          </svg>\n        </div>\n      </div>\n    </DesignStage>\n  );\n};\n\n\n"
}