{
  "slug": "scanline-annotate-focus",
  "title": "Scanline annotate focus",
  "tagline": "A bright scan line sweeps down the page, framing each element it passes and labeling it with monospace annotations.",
  "description": "A bright scan line sweeps down the page, framing each element it passes and labeling it with monospace annotations.",
  "category": "effects",
  "tags": [
    "shotcraft",
    "remotion",
    "effects"
  ],
  "tier": "free",
  "license": "Apache-2.0",
  "spec": {
    "width": 1920,
    "height": 1080,
    "fps": 30,
    "duration": 5.1,
    "aspect": "16:9"
  },
  "kind": "remotion",
  "params_schema": {
    "type": "object",
    "properties": {}
  },
  "default_params": {},
  "preview": {
    "poster": "https://gimgs.net/motion/media/scanline-annotate-focus/poster-9677d0de21b8.jpg",
    "video": "https://gimgs.net/motion/media/scanline-annotate-focus/video-b0f361227e69.mp4"
  },
  "variants": [
    {
      "key": "scanline-annotate-focus",
      "label": "scanline-annotate-focus",
      "description": "A bright scan line sweeps top to bottom across the page; wherever it passes, camera-style viewfinder frames pop in one after another (1.75x contraction into alignment with a slight overshoot), followed by monospace annotation labels off to the side, while a status line at the top counts in sync from 00/06 to 06/06.",
      "use": "",
      "video": "https://gimgs.net/motion/media/scanline-annotate-focus/video-b0f361227e69.mp4",
      "poster": "https://gimgs.net/motion/media/scanline-annotate-focus/poster-9677d0de21b8.jpg"
    }
  ],
  "credits": {
    "author": "video-shotcraft (Vincent Wei)",
    "url": "https://github.com/Vincentwei1021/video-shotcraft"
  },
  "page_url": "https://gimgs.net/motion/scanline-annotate-focus.html",
  "json_url": "https://gimgs.net/motion/scanline-annotate-focus.json",
  "template_url": "https://gimgs.net/motion/scanline-annotate-focus/template.html",
  "play_url": null,
  "markdown_url": "https://gimgs.net/motion/scanline-annotate-focus.md",
  "updated_at": "2026-09-24T07:40:19.541Z",
  "prompt": "## When to use\n\nAn analysis shot for \"AI is reading your page/brand\"; a breakdown intro for design systems/brand guidelines; a self-explanatory capability section for a product\n\n## Intention\n\nMake the abstract act of \"analysis\" into a visible causal chain: wherever the scan line reaches, that area gets framed and named. The viewer is reading \"the machine's gaze,\" so the scan line must move at constant speed and each annotation must strictly lag behind the scan line's passage — **scan first, then pop the frame** — if the order ever gets scrambled it reads as a pre-choreographed animation instead.\n\n## Duration & energy\n\n- Duration: About 4.6s (138f@30fps)\n- Energy: Medium (mechanically calm, the rhythm driven by the scan line's constant speed, the annotations are the beats)\n\n## Shot recipe (original Chinese — timing, easing, parameter table, known pitfalls)\n\n## 意图\n把\"分析\"这件抽象事做成可见的因果链：扫描线走到哪，那块就被框住、被\n命名。观众读的是\"机器的视线\"，所以扫描线必须匀速、标注必须严格滞后于\n扫描线过境——**先扫到再弹框**，顺序一旦错乱就变成了预先编排的动画。\n\n## 动效核心\n- 扫描线 `t 0.06→0.66` 匀速（无缓动）从 y=−30 走到 300，\n  0.04→0.09 淡入、0.66→0.71 淡出\n- 触发时刻由目标 bbox 反算：`ft = 0.06 + ((y+h+30)/330)*0.60`，即扫描线\n  越过 bbox **下缘**的那一刻；再按 y 排序钳制最小间隔 0.05\n  （`ft = max(rawT, prev+0.05)`）避免两框同时弹\n- 取景框由 4 个 9px L 角组成，`outBack` 驱动 `scale 1.75→1`（0.13 内\n  走完，过冲后回稳），opacity 走 `min(1, a*1.6)` 抢先满亮\n- 框内白色 `fill` 层闪一下：0.07 峰值，`ft+0.04→+0.09` 起、\n  `+0.09→+0.22` 落——像相机对焦成功的确认闪\n- 标注 `ft+0.05→+0.16 outCubic` 淡入 + `translateY 4→0`，\n  6.5px MONO、`letter-spacing:1.5px`、`#b8bdc7`\n- 状态行实时数已触发的框（`fired`）显示 `SCAN · 0n/06`，\n  `0.74→0.80` 后切 `ANALYSIS · COMPLETE` 并转强调色\n\n## 参数表\n| 参数 | 典型值 | 调节手感 |\n|------|--------|----------|\n| 扫描线 | t 0.06→0.66 匀速，y −30→300 | **零缓动**：加 ease 就读作\"有人在拖进度条\" |\n| 触发换算 | ft = 0.06 + ((y+h+30)/330)·0.60 | 分母 330 = 扫描行程；改扫描窗口必须同改，否则框在扫描线到达前就弹 |\n| 最小间隔 | prev + 0.05（≈1.5f） | 两个 y 接近的目标会同时弹；钳制后才逐个读得清 |\n| 取景框收拢 | scale 1.75→1，outBack，0.13 | 1.75 倍是\"从外面收进来\"的下限；1.2 倍看不出对准动作 |\n| 角标 | 臂长 9px、描边 1.5px | 臂长超过目标短边 1/3 就变成完整边框，丢掉\"取景\"语义 |\n| 对焦闪 | fill opacity 峰值 0.07 | 超过 0.15 就盖住内容，读作\"选中高亮\"而非对焦确认 |\n| 标注 | ft+0.05 起、0.11 走完、translateY 4→0 | 滞后 0.05 是\"框稳了才命名\"的因果；同时出现就乱 |\n| 计数行 | 按 fired 实时统计，7px MONO，2px 字距 | 写死时间轴的计数一改 bbox 就对不上；必须实时数 |\n\n## 已知坑\n- **扫描线触发时刻与目标 bbox 强绑定**：换页面内容后 targets 的\n  `x/y/w/h` 与标注锚点 `lx/ly` 都要重测，否则框和内容错位\n- bbox 是手动微调留边的（不是自动量元素），改页面模板不会自动跟随\n- 标注用 `white-space:nowrap` + 手写 `lx/ly`，词变长会顶出画外；\n  右侧目标的标注要主动改成左置\n- 与 `scanline-assemble-flyin` 分工：那张是\"扫到即装配\"（页面从空到有），\n  本卡是\"扫到即标注\"（页面已存在，只做分析）\n- ACCENT（`#9fb6e8`）与 `A_RGB` 是同一个色的两种写法，换肤要同改两处\n\n## 参考实现\ndemos/effects/scanline-annotate-focus/\n（ScanlineAnnotateFocus.tsx）\n\n## Reference implementation (Remotion)\n\n- demos/effects/scanline-annotate-focus/ScanlineAnnotateFocus.tsx — https://raw.githubusercontent.com/Vincentwei1021/video-shotcraft/main/demos/effects/scanline-annotate-focus/ScanlineAnnotateFocus.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/effects/scanline-annotate-focus/ScanlineAnnotateFocus.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/effects/scanline-annotate-focus/ScanlineAnnotateFocus.tsx",
      "url": "https://raw.githubusercontent.com/Vincentwei1021/video-shotcraft/main/demos/effects/scanline-annotate-focus/ScanlineAnnotateFocus.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": 153,
    "repo": "https://github.com/Vincentwei1021/video-shotcraft",
    "files": [
      "https://raw.githubusercontent.com/Vincentwei1021/video-shotcraft/main/demos/effects/scanline-annotate-focus/ScanlineAnnotateFocus.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/effects/scanline-annotate-focus/ScanlineAnnotateFocus.tsx =====\n// scanline-annotate-focus — Scanline Annotate 扫描分析取景标注（motion-lab 定稿转原生 Remotion）\n// 一条亮扫描线自上而下掠过页面，扫过之处按先后顺序弹出相机取景框：四角括号从约 1.75 倍\n// 大小快速收拢对准目标区块（对准瞬间轻微过冲再回稳），随后旁侧打出等宽小字标注。\n// 顶部状态行同步计数 00/06→06/06，扫完切换 ANALYSIS · COMPLETE。\n// 页面内容为中性占位模板，标注词与强调色均可按项目替换。设计坐标 480×270（DesignStage 等比放大）。\nimport React from 'react';\nimport { DesignStage, E, lerp, seg, useT } from '../../_fixtures/Motion';\n\nexport const SCANLINE_ANNOTATE_FOCUS_DURATION = 138; // 4600ms @30fps\n\nconst MONO = \"'SF Mono',Menlo,Consolas,monospace\";\nconst SERIF = \"Georgia,'Times New Roman',serif\";\nconst ACCENT = '#9fb6e8'; // 模板强调色，可按项目替换\nconst A_RGB = '159,182,232';\n\n/* ---- 分析目标（bbox 手动微调留边）：ft=取景框触发时刻，按扫描顺序推导 ---- */\ntype Target = { x: number; y: number; w: number; h: number; label: string; lx: number; ly: number; ft: number };\nconst TARGETS: Target[] = (() => {\n  const ts: Target[] = [\n    { x: 18, y: 30, w: 108, h: 22, label: 'LOGO · MARK + WORDMARK', lx: 132, ly: 38, ft: 0 },\n    { x: 292, y: 48, w: 170, h: 158, label: 'MODULE · KINETIC TYPE', lx: 292, ly: 36, ft: 0 },\n    { x: 18, y: 58, w: 242, h: 78, label: 'H1 · SERIF DISPLAY', lx: 266, ly: 92, ft: 0 },\n    { x: 18, y: 160, w: 136, h: 32, label: 'CTA · PRIMARY + GHOST', lx: 160, ly: 172, ft: 0 },\n    { x: 14, y: 240, w: 224, h: 18, label: 'FOOTER · LEGAL', lx: 242, ly: 246, ft: 0 },\n    { x: 348, y: 235, w: 116, h: 23, label: 'SOCIAL · BRAND VOICE', lx: 348, ly: 224, ft: 0 },\n  ];\n  // 触发时刻：扫描线（0.06→0.66 纵扫 -30→300）越过 bbox 下缘\n  const rawT = (tg: Target) => 0.06 + ((tg.y + tg.h + 30) / 330) * 0.6;\n  let prev = -1;\n  for (const tg of [...ts].sort((a, b) => a.y + a.h - (b.y + b.h))) {\n    tg.ft = Math.max(rawT(tg), prev + 0.05); // 依序钳制最小间隔\n    prev = tg.ft;\n  }\n  return ts;\n})();\n\n// 取景框四角括号：每角朝外的两条边框\n// 注意 boxSizing content-box：原采集页 9×9 内容 + 1.5px 外扩边框（外框 10.5），\n// Remotion 模板全局 border-box 会把边框内收导致括号臂变短、右/下角内移。\nconst C_BORDER = '1.5px solid #f2f3f5';\nconst CORNERS: React.CSSProperties[] = [\n  { left: 0, top: 0, borderTop: C_BORDER, borderLeft: C_BORDER },\n  { right: 0, top: 0, borderTop: C_BORDER, borderRight: C_BORDER },\n  { left: 0, bottom: 0, borderBottom: C_BORDER, borderLeft: C_BORDER },\n  { right: 0, bottom: 0, borderBottom: C_BORDER, borderRight: C_BORDER },\n];\n\nexport const ScanlineAnnotateFocus: React.FC = () => {\n  const t = useT();\n\n  // 扫描线纵扫 + 首尾淡入淡出\n  const ly = lerp(seg(t, 0.06, 0.66), -30, 300);\n  const lineOpacity = seg(t, 0.04, 0.09) * (1 - seg(t, 0.66, 0.71));\n\n  // 已触发的取景框计数（a>0 即已弹出）\n  const fired = TARGETS.reduce((acc, tg) => acc + (seg(t, tg.ft, tg.ft + 0.11, E.outCubic) > 0 ? 1 : 0), 0);\n  const done = seg(t, 0.74, 0.8);\n\n  return (\n    <DesignStage bg=\"#0a0b0e\">\n      {/* ---- 中性占位页面（静态底） ---- */}\n      <div style={{ position: 'absolute', inset: 0, background: 'linear-gradient(180deg,#101116,#0c0d11)' }}>\n        {/* 顶栏（url pill + 状态）——对照原样片实测整体高 0.5px（亚像素基线取整差），\n            分数 top 会被取整吞掉，改用 translateY 补偿（transform 不参与像素取整） */}\n        <div style={{ position: 'absolute', left: 0, top: 0, width: 480, height: 30, transform: 'translateY(0.5px)' }}>\n          <div style={{ position: 'absolute', left: 18, top: 11, padding: '3px 10px', border: '1px solid #2a2c33', borderRadius: 9, font: `500 7px ${MONO}`, color: '#8d93a0', letterSpacing: 1 }}>app.example.com</div>\n          <div style={{ position: 'absolute', right: 18, top: 14, font: `500 7px ${MONO}`, color: '#565b66', letterSpacing: 1.5 }}>200 OK · TLS</div>\n        </div>\n        {/* logo：点阵 mark + 衬线字标 */}\n        <div style={{ position: 'absolute', left: 24, top: 35, width: 100, height: 18 }}>\n          {[0, 1, 2, 3].map((i) => (\n            <div key={i} style={{ position: 'absolute', width: 4, height: 4, borderRadius: '50%', background: '#e8e9ee', left: (i % 2) * 6, top: 2 + (i >> 1) * 6 }} />\n          ))}\n          <div style={{ position: 'absolute', left: 17, top: 0, font: `400 13px ${SERIF}`, color: '#eceef2' }}>\n            Acme <i>Studio</i>\n          </div>\n        </div>\n        {/* H1 两行 + 注脚 */}\n        <div style={{ position: 'absolute', left: 22, top: 64, width: 242, height: 84 }}>\n          {/* 两行 29px 衬线对照原样片实测低 0.5px（行盒取整差），translateY 补偿（分数 top 会被取整吞掉） */}\n          <div style={{ position: 'absolute', left: 0, top: 0, font: `400 29px ${SERIF}`, color: '#f2f3f6', letterSpacing: 0.3, transform: 'translateY(-0.5px)' }}>The headline for</div>\n          <div style={{ position: 'absolute', left: 0, top: 36, font: `italic 400 29px ${SERIF}`, color: '#f2f3f6', letterSpacing: 0.3, transform: 'translateY(-0.5px)' }}>your product here</div>\n          <div style={{ position: 'absolute', left: 1, top: 79, font: `500 6.5px ${MONO}`, color: '#565b66', letterSpacing: 1.5 }}>H1 · UI-SERIF / GEORGIA</div>\n        </div>\n        {/* CTA 行 */}\n        <div style={{ position: 'absolute', left: 22, top: 166, width: 136, height: 26 }}>\n          <div style={{ position: 'absolute', left: 0, top: 0, padding: '6px 13px', border: '1px solid #3a3d46', borderRadius: 12, font: `600 7.5px ${MONO}`, color: '#e8e9ee', letterSpacing: 1.5 }}>GET STARTED</div>\n          <div style={{ position: 'absolute', left: 100, top: 7, font: `500 7.5px ${MONO}`, color: '#6a707c', letterSpacing: 1.5 }}>DOCS</div>\n        </div>\n        {/* 右侧模块卡（boxSizing content-box：原采集页边框外扩，内容盒保持 162×150） */}\n        <div style={{ position: 'absolute', left: 296, top: 52, width: 162, height: 150, boxSizing: 'content-box', background: '#121319', border: '1px solid #23252d', borderRadius: 5 }}>\n          <div style={{ position: 'absolute', left: 10, top: 9, font: `500 6.5px ${MONO}`, color: '#7c828e', letterSpacing: 1.5 }}>WORK</div>\n          <div style={{ position: 'absolute', right: 10, top: 9, font: `500 6.5px ${MONO}`, color: '#565b66', letterSpacing: 1.5 }}>04 / 08</div>\n          <div style={{ position: 'absolute', left: 0, top: 24, width: '100%', height: 1, background: '#1e2028' }} />\n          <div style={{ position: 'absolute', left: 0, top: 44, width: '100%', textAlign: 'center', font: `italic 400 36px ${SERIF}`, color: '#f4f5f8' }}>sample</div>\n          <div style={{ position: 'absolute', left: 0, top: 104, width: '100%', height: 1, background: '#1e2028' }} />\n          <div style={{ position: 'absolute', left: 10, top: 112, font: `500 6px ${MONO}`, color: '#6a707c', letterSpacing: 1.5 }}>KINETIC TYPE · 04</div>\n          {[0, 1, 2, 3].map((i) => (\n            <div key={i} style={{ position: 'absolute', left: 10 + i * 30, top: 124, width: 24, height: 16, background: '#1a1c23', borderRadius: 2 }} />\n          ))}\n          <div style={{ position: 'absolute', right: 8, top: 129, font: `500 6px ${MONO}`, color: '#565b66' }}>00:30</div>\n        </div>\n        {/* 页脚 + 社交 chip */}\n        <div style={{ position: 'absolute', left: 18, top: 243, width: 220, height: 14 }}>\n          <div style={{ position: 'absolute', left: 0, top: 3, font: `500 6.5px ${MONO}`, color: '#4c515c', letterSpacing: 1.5 }}>A PRODUCT OF ACME · ACME LABS, INC.</div>\n        </div>\n        <div style={{ position: 'absolute', left: 352, top: 239, width: 108, height: 16 }}>\n          {/* boxSizing content-box：原采集页 11×11 内容 + 1px 外扩边框 */}\n          <div style={{ position: 'absolute', left: 0, top: 2, width: 11, height: 11, boxSizing: 'content-box', border: '1px solid #3a3d46', borderRadius: 2, font: `600 7px ${MONO}`, color: '#c9cdd6', textAlign: 'center', lineHeight: '11px' }}>x</div>\n          <div style={{ position: 'absolute', left: 17, top: 3, font: `600 7.5px ${MONO}`, color: '#c9cdd6', letterSpacing: 1.5 }}>@USERNAME</div>\n        </div>\n      </div>\n\n      {/* ---- 取景框 + 标注（按扫描顺序触发） ---- */}\n      {TARGETS.map((tg, i) => {\n        const a = seg(t, tg.ft, tg.ft + 0.11, E.outCubic); // 弹出进度\n        const s = lerp(E.outBack(seg(t, tg.ft, tg.ft + 0.13)), 1.75, 1); // 1.75 倍收拢 + 过冲回稳\n        const fillO = 0.07 * seg(t, tg.ft + 0.04, tg.ft + 0.09) * (1 - seg(t, tg.ft + 0.09, tg.ft + 0.22)); // 对准瞬间白色微闪\n        const la = seg(t, tg.ft + 0.05, tg.ft + 0.16, E.outCubic); // 标注淡入上移\n        return (\n          <React.Fragment key={i}>\n            <div\n              style={{\n                position: 'absolute',\n                left: tg.x,\n                top: tg.y,\n                width: tg.w,\n                height: tg.h,\n                opacity: Math.min(1, a * 1.6),\n                transform: `scale(${a > 0 ? s : 1.75})`,\n              }}\n            >\n              {CORNERS.map((c, k) => (\n                <div key={k} style={{ position: 'absolute', width: 9, height: 9, boxSizing: 'content-box', ...c }} />\n              ))}\n              <div style={{ position: 'absolute', inset: 1, background: '#fff', opacity: fillO }} />\n            </div>\n            <div\n              style={{\n                position: 'absolute',\n                left: tg.lx,\n                top: tg.ly,\n                font: `500 6.5px ${MONO}`,\n                color: '#b8bdc7',\n                letterSpacing: 1.5,\n                whiteSpace: 'nowrap',\n                opacity: la,\n                transform: `translateY(${lerp(la, 4, 0)}px)`,\n              }}\n            >\n              {tg.label}\n            </div>\n          </React.Fragment>\n        );\n      })}\n\n      {/* ---- 扫描线（渐变拖尾 + 亮芯） ---- */}\n      <div\n        style={{\n          position: 'absolute',\n          left: 0,\n          top: 0,\n          width: '100%',\n          height: 40,\n          background: `linear-gradient(180deg,transparent,rgba(${A_RGB},.07) 55%,rgba(${A_RGB},.02) 96%,transparent)`,\n          transform: `translateY(${ly - 40}px)`,\n          opacity: lineOpacity,\n        }}\n      >\n        <div\n          style={{\n            position: 'absolute',\n            bottom: 0,\n            left: 0,\n            width: '100%',\n            height: 1.5,\n            background: 'rgba(238,244,255,.9)',\n            boxShadow: `0 0 7px ${ACCENT},0 0 18px rgba(${A_RGB},.35)`,\n          }}\n        />\n      </div>\n\n      {/* ---- 顶部状态行：SCAN 计数 → ANALYSIS · COMPLETE ---- */}\n      <div\n        style={{\n          position: 'absolute',\n          left: '50%',\n          top: 14,\n          transform: 'translateX(-50%)',\n          font: `600 7px ${MONO}`,\n          letterSpacing: 2,\n          color: done >= 1 ? ACCENT : '#8d93a0',\n          opacity: seg(t, 0.03, 0.08),\n        }}\n      >\n        {done >= 1 ? 'ANALYSIS · COMPLETE' : `SCAN · 0${fired}/0${TARGETS.length}`}\n      </div>\n    </DesignStage>\n  );\n};\n\n\n"
}