// cursor-flyover — Cursor Flyover 四角巡览指点(motion-lab 定稿转原生 Remotion) // 产品截图平铺,相机先整体俯瞰淡入,再依次飞到四个角落 zoom-in 特写;一枚带阴影 // 的 SVG 光标跟到对应区域指点并留下点击涟漪。每步过渡+停留等长,easeInOutCubic。 // 设计坐标 480×270(DesignStage 等比放大),参数表数值以此坐标系标定。 import React from 'react'; import { DesignStage, E, lerp, rand, seg, useT } from '../../_fixtures/Motion'; export const CURSOR_FLYOVER_DURATION = 180; // 6000ms @30fps // 关键帧累加器:base 起步,每段 [at0,at1] 内朝 to 推进(效果源文件级共享 helper) type Vec = Record; const acc = (t: number, base: Vec, kfs: { at: number[]; to: Vec }[], keys: string[], ease: (x: number) => number) => { const out: Vec = {}; for (const k of keys) out[k] = base[k]; let prev = base; for (const kf of kfs) { const u = seg(t, kf.at[0], kf.at[1], ease); for (const k of keys) out[k] += u * (kf.to[k] - prev[k]); prev = kf.to; } return out; }; // 相机 Step:overview + 四角;光标目标(world %) const CAM = [ { tx: 50, ty: 50, s: 0.8, cx: 50, cy: 52 }, { tx: 37, ty: 32, s: 1.72, cx: 41, cy: 38 }, { tx: 77, ty: 32, s: 1.72, cx: 82, cy: 36 }, { tx: 77, ty: 72, s: 1.72, cx: 71, cy: 76 }, { tx: 37, ty: 72, s: 1.72, cx: 33, cy: 78 }, ]; const WIN = [[0.20, 0.32], [0.40, 0.52], [0.60, 0.72], [0.79, 0.91]]; const KEY = ['tx', 'ty', 's', 'cx', 'cy']; // 右上折线图路径(种子与 effect.js 相同) const LINE_D = (() => { let dd = ''; for (let i = 0; i <= 14; i++) dd += `${i === 0 ? 'M' : 'L'}${i * 7.14},${40 - (6 + rand(i * 5) * 22 + i * 0.7)} `; return dd; })(); // 四象限容器(截图内相对定位) const Quad: React.FC<{ x: number; y: number; w: number; h: number; children?: React.ReactNode }> = ({ x, y, w, h, children, }) => (
{children}
); export const CursorFlyover: React.FC = () => { const t = useT(); const v = acc(t, CAM[0], WIN.map((w, i) => ({ at: w, to: CAM[i + 1] })), KEY, E.inOutCubic); const fade = seg(t, 0, 0.14, E.outCubic); // 每次落位时的点击涟漪 let click = 0; let hold = 0; for (let i = 0; i < WIN.length; i++) { const c = seg(t, WIN[i][1], WIN[i][1] + 0.055, E.outCubic); if (c > 0 && c < 1) { click = c; hold = 1; } else if (c >= 1 && t < (WIN[i + 1] ? WIN[i + 1][0] : 1.01)) { click = 1; hold = 1; } } return (
{/* world:相机 translate+scale 的载体,淡入由 opacity 承担 */}
{/* ---- 截图占位:窗口 chrome + 侧栏 + 四象限控件 ---- */}
{/* 顶栏:红黄绿灯 + URL 胶囊(原采集页 content-box:border-bottom 在 9% 高度之外) */}
{['#ff6058', '#ffbd2e', '#28ca42'].map((c, i) => (
))}
app.example.com/overview
{/* 侧栏导航(content-box:border-right 在 15% 宽度之外) */}
{['Overview', 'Traffic', 'Revenue', 'Cohorts', 'Alerts'].map((s, i) => (
{s}
))}
{/* 左上:stat tiles */} {['12.4k', '+38%', '4.1s'].map((s, i) => (
{s}
METRIC {i + 1}
))}
{/* 右上:折线图 */} {/* 右下:柱状图 */} {Array.from({ length: 9 }, (_, i) => (
))} {/* 左下:表格行 */} {Array.from({ length: 5 }, (_, i) => (
))}
{/* ---- 点击涟漪(world 内锚定,反缩放保持屏幕尺寸;content-box:外径 26+2×2=30px 与原采集页一致) ---- */}
{/* ---- 光标(world 内锚定,逐帧反缩放保持屏幕尺寸) ---- */} 0 && click < 0.4 ? '#dfe6ff' : '#fff'} stroke="#101320" strokeWidth={1.1} />
); };