{
  "slug": "scan-bracket-sweep",
  "title": "Scan bracket sweep",
  "tagline": "A skeleton document pops in, viewfinder brackets frame it, and a scan line sweeps it 5 times while it stays completely still.",
  "description": "A skeleton document pops in, viewfinder brackets frame it, and a scan line sweeps it 5 times while it stays completely still.",
  "category": "effects",
  "tags": [
    "shotcraft",
    "remotion",
    "effects"
  ],
  "tier": "free",
  "license": "Apache-2.0",
  "spec": {
    "width": 1920,
    "height": 1080,
    "fps": 30,
    "duration": 5.5,
    "aspect": "16:9"
  },
  "kind": "remotion",
  "params_schema": {
    "type": "object",
    "properties": {}
  },
  "default_params": {},
  "preview": {
    "poster": "https://gimgs.net/motion/media/scan-bracket-sweep/poster-fc38300e64e5.jpg",
    "video": "https://gimgs.net/motion/media/scan-bracket-sweep/video-467a4f7ce07d.mp4"
  },
  "variants": [
    {
      "key": "scan-bracket-sweep",
      "label": "scan-bracket-sweep",
      "description": "A skeleton document pops into the center, L-shaped viewfinder brackets drop onto the four corners, and a 2.5px solid line with a gradient trail sweeps back and forth across the document 5 times — the document stays completely still throughout, only the light is reading it.",
      "use": "",
      "video": "https://gimgs.net/motion/media/scan-bracket-sweep/video-467a4f7ce07d.mp4",
      "poster": "https://gimgs.net/motion/media/scan-bracket-sweep/poster-fc38300e64e5.jpg"
    }
  ],
  "credits": {
    "author": "video-shotcraft (Vincent Wei)",
    "url": "https://github.com/Vincentwei1021/video-shotcraft"
  },
  "page_url": "https://gimgs.net/motion/scan-bracket-sweep.html",
  "json_url": "https://gimgs.net/motion/scan-bracket-sweep.json",
  "template_url": "https://gimgs.net/motion/scan-bracket-sweep/template.html",
  "play_url": null,
  "markdown_url": "https://gimgs.net/motion/scan-bracket-sweep.md",
  "updated_at": "2026-09-24T07:40:19.344Z",
  "prompt": "## When to use\n\nA process shot for \"analyzing/validating this content\"; a capability demo for document products; the middle beat of an upload → analyze pipeline\n\n## Intention\n\nLet the viewer read \"this thing is being read through line by line by a machine.\" The viewfinder brackets frame the target first (announcing the inspection subject), then the scan line sweeps back and forth across the whole document (announcing the inspection process). The critical rule is that **the document itself stays completely still** — the moment the document also moves, the viewer can no longer tell if it's \"being inspected\" or \"loading.\"\n\n## Duration & energy\n\n- Duration: About 5.0s (150f@30fps)\n- Energy: Medium-low (mechanical, restrained, the rhythm lives entirely in the breathing of the back-and-forth sweep)\n\n## Shot recipe (original Chinese — timing, easing, parameter table, known pitfalls)\n\n## 意图\n让观众看懂\"这份东西正在被机器逐行读过\"。取景括号先把目标框住（宣布检查\n对象），扫描光带再往复走完全篇（宣布检查过程）。命门是**文档本身完全静止**：\n一旦文档也在动，观众就分不清是\"被检查\"还是\"在加载\"。\n\n## 动效核心\n- 文档 `seg(0→0.11, outCubic)` 从 scale 0.86 弹到 1，opacity 走 `dp*3`\n  提前满亮，落位比缩放早——先\"在了\"再\"稳了\"\n- 四角 34px 的 L 形括号按 `i*0.022` stagger 依次落位，各 0.055 走完，\n  沿内外对角带 8px 位移收敛（`(1-p)*8*dx/dy`）\n- 扫描窗口 `t 0.17→0.95` 严格 linear 切成 5 趟，趟内 `inOutSine`\n  两端慢中间快，末尾留 12% 完全停顿（`local=(raw-pi)/0.88`）\n- 拖尾 82px、`rgba(20,20,22,.5)→0`，方向随 `dir` 翻转：下行时挂在光带\n  上方（`top:-82px`）、上行时挂在下方（`top:2.5px`），渐变同步反向\n- 整条光带裁在与文档同 10px 圆角的 `overflow:hidden` 层里，\n  `0.16→0.20` 淡入、`0.93→0.99` 淡出\n\n## 参数表\n| 参数 | 典型值 | 调节手感 |\n|------|--------|----------|\n| 文档弹入 | scale 0.86→1，t 0→0.11 outCubic | 起始值低于 0.8 就成了\"从远处飞来\"，弹入要小幅度才像 UI |\n| 取景括号 | 臂长 34px、描边 2px、外扩 −7px；stagger 0.022 / 各 0.055 | 四角同时出现读作静态边框；差 0.022 就够读出\"依次落下\" |\n| 扫描趟数 | PASSES=5，窗口 t 0.17→0.95 linear | 3 趟不够读作\"往复\"，7 趟以上在 5s 内每趟太快 |\n| 趟内缓动 | inOutSine + 末尾 12% 停顿 | 停顿是\"看清了这一趟\"的换气位；去掉就成了匀速来回摆 |\n| 光带 | 2.5px 实线 + 82px 拖尾 rgba(20,20,22,.5)→0 | 线太粗（>4px）读作遮罩边缘而不是光 |\n| 拖尾方向 | 下行 top:−82px / 上行 top:2.5px，渐变一并翻转 | 只翻其一 → 拖尾跑到运动前方，读作\"光在往回吸\" |\n| 裁切层 | 与文档同 10px 圆角 overflow:hidden | 漏到文档外就是\"扫画面\"不是\"扫文档\" |\n\n## 已知坑\n- 光带必须裁在文档 bbox 内，且圆角与文档一致——漏出一点就丢掉\"扫描\n  这份文档\"的语义\n- 拖尾的 `top` 和 gradient 方向是两处独立写法，改扫描方向时必须同改，\n  漏一处就出现\"尾迹在前\"的反物理观感\n- 括号的 34px 臂长与 −7px 外扩是按 300×178 的文档尺寸配的；换真实截图\n  要按新尺寸重算，臂长超过短边 1/4 就压住内容\n- INK 是唯一的颜色变量（括号 + 光带 + 拖尾共用），换肤只改它\n\n## 参考实现\ndemos/effects/scan-bracket-sweep/\n（ScanBracketSweep.tsx）\n\n## Reference implementation (Remotion)\n\n- demos/effects/scan-bracket-sweep/ScanBracketSweep.tsx — https://raw.githubusercontent.com/Vincentwei1021/video-shotcraft/main/demos/effects/scan-bracket-sweep/ScanBracketSweep.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/scan-bracket-sweep/ScanBracketSweep.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/scan-bracket-sweep/ScanBracketSweep.tsx",
      "url": "https://raw.githubusercontent.com/Vincentwei1021/video-shotcraft/main/demos/effects/scan-bracket-sweep/ScanBracketSweep.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": 165,
    "repo": "https://github.com/Vincentwei1021/video-shotcraft",
    "files": [
      "https://raw.githubusercontent.com/Vincentwei1021/video-shotcraft/main/demos/effects/scan-bracket-sweep/ScanBracketSweep.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/scan-bracket-sweep/ScanBracketSweep.tsx =====\n// scan-bracket-sweep — Scan Bracket 取景括号扫描光带（motion-lab 定稿转原生 Remotion）\n// 骨架文档弹到画面中央，四角落下黑色 L 形取景括号（向内位移 8px），随后一条 2.5px\n// 黑实线带着 80px 深灰→透明拖尾在文档上往复扫 5 趟，两端慢中间快，尾迹方向始终朝\n// 运动来向，文档本身完全静止。设计坐标 480×270（DesignStage 等比放大）。\nimport React from 'react';\nimport { DesignStage, E, lerp, rand, seg, useT } from '../../_fixtures/Motion';\n\nexport const SCAN_BRACKET_SWEEP_DURATION = 150; // 5000ms @30fps\n\n// —— b09 批共用量（源自 motion-lab/fx/b09.js，按规则内联，不 export）——\nconst BG = '#F1F1F3'; // 页面浅灰\nconst INK = '#0B0B0C'; // 纯黑\nconst LINE = '#E6E6EA'; // 描边\nconst SKEL = '#DEDEE3'; // 骨架灰\n\nconst clamp01 = (v: number) => (v < 0 ? 0 : v > 1 ? 1 : v);\nconst inOutSin = (x: number) => 0.5 - Math.cos(Math.PI * clamp01(x)) / 2;\n\n// 文档几何：440×240 定尺画布内居中的 300×178 骨架文档\nconst DW = 300;\nconst DH = 178;\nconst DX = (440 - DW) / 2;\nconst DY = (240 - DH) / 2;\n\n// 骨架列参数（与 effect.js mkDoc 完全一致：4 列 ×7 行，rand 种子相同可逐条复现）\nconst COLS = 4;\nconst COL_W = (DW - 28 - (COLS - 1) * 10) / COLS;\nconst SKEL_COLS = Array.from({ length: COLS }, (_, c) => ({\n  x: 14 + c * (COL_W + 10),\n  rows: Array.from({ length: 7 }, (_, r) => ({\n    top: 58 + r * 13,\n    w: Number((COL_W * (0.55 + rand(c * 13 + r * 7) * 0.45)).toFixed(1)),\n  })),\n}));\n\n// 四角取景括号：L 形两边 2px 黑边，落位时从外侧 8px 位移收进来\nconst CS = 34;\nconst CB = `2px solid ${INK}`;\nconst CORNERS: { pos: React.CSSProperties; dx: number; dy: number }[] = [\n  { pos: { left: -7, top: -7, borderLeft: CB, borderTop: CB }, dx: 1, dy: 1 },\n  { pos: { right: -7, top: -7, borderRight: CB, borderTop: CB }, dx: -1, dy: 1 },\n  { pos: { right: -7, bottom: -7, borderRight: CB, borderBottom: CB }, dx: -1, dy: -1 },\n  { pos: { left: -7, bottom: -7, borderLeft: CB, borderBottom: CB }, dx: 1, dy: -1 },\n];\n\nconst PASSES = 5;\n\nexport const ScanBracketSweep: React.FC = () => {\n  const t = useT();\n\n  // 文档弹入：scale 0.86→1 + 快速淡入\n  const dp = seg(t, 0, 0.11, E.outCubic);\n\n  // 扫描往复：5 趟，每趟末尾留 12% 停顿，两端慢中间快（inOutSine）\n  const sp = seg(t, 0.17, 0.95, E.linear);\n  const raw = sp * PASSES;\n  const pi = Math.min(PASSES - 1, Math.floor(raw));\n  const local = clamp01((raw - pi) / 0.88);\n  const dir = pi % 2 === 0 ? 1 : -1;\n  const prog = inOutSin(local);\n  const y = dir > 0 ? prog * DH : DH - prog * DH;\n  // 光带整体淡入（0.16–0.2）淡出（0.93–0.99）\n  const clipOp = Number((seg(t, 0.16, 0.2) * (1 - seg(t, 0.93, 0.99))).toFixed(3));\n\n  return (\n    <DesignStage bg={BG}>\n      {/* 440×240 定尺画布（mkSheet），居中于 480×270 设计坐标 */}\n      <div\n        style={{\n          position: 'absolute',\n          left: '50%',\n          top: '50%',\n          width: 440,\n          height: 240,\n          margin: '-120px 0 0 -220px',\n        }}\n      >\n        {/* holder：文档 + 光带 + 括号共用的定位容器 */}\n        <div style={{ position: 'absolute', left: DX, top: DY, width: DW, height: DH }}>\n          {/* 骨架文档卡片（原采集页为 content-box：1px 边框长在 300×178 之外） */}\n          <div\n            style={{\n              position: 'absolute',\n              left: 0,\n              top: 0,\n              width: DW,\n              height: DH,\n              boxSizing: 'content-box',\n              background: '#fff',\n              border: `1px solid ${LINE}`,\n              borderRadius: 10,\n              boxShadow: '0 8px 26px rgba(0,0,0,.06)',\n              overflow: 'hidden',\n              transformOrigin: '50% 50%',\n              transform: `scale(${lerp(dp, 0.86, 1)})`,\n              opacity: clamp01(dp * 3),\n            }}\n          >\n            <div style={{ position: 'absolute', left: 0, top: 0, right: 0 }}>\n              {/* 标题条 + 副标题条 */}\n              <div\n                style={{\n                  position: 'absolute',\n                  left: 14,\n                  top: 12,\n                  width: Math.round(DW * 0.34),\n                  height: 7,\n                  borderRadius: 3,\n                  background: INK,\n                  opacity: 0.85,\n                }}\n              />\n              <div\n                style={{\n                  position: 'absolute',\n                  left: 14,\n                  top: 25,\n                  width: Math.round(DW * 0.2),\n                  height: 5,\n                  borderRadius: 3,\n                  background: SKEL,\n                }}\n              />\n              {/* 4 列骨架：列头 + 7 行随机宽占位条 */}\n              {SKEL_COLS.map((col, c) => (\n                <React.Fragment key={c}>\n                  <div\n                    style={{\n                      position: 'absolute',\n                      left: col.x,\n                      top: 44,\n                      width: Number((COL_W * 0.72).toFixed(1)),\n                      height: 6,\n                      borderRadius: 3,\n                      background: '#9A9AA2',\n                    }}\n                  />\n                  {col.rows.map((row, r) => (\n                    <div\n                      key={r}\n                      style={{\n                        position: 'absolute',\n                        left: col.x,\n                        top: row.top,\n                        width: row.w,\n                        height: 5,\n                        borderRadius: 2.5,\n                        background: SKEL,\n                      }}\n                    />\n                  ))}\n                </React.Fragment>\n              ))}\n            </div>\n          </div>\n\n          {/* 扫描光带（裁在文档圆角内）：2.5px 黑实线 + 82px 朝来向的渐变拖尾 */}\n          <div\n            style={{\n              position: 'absolute',\n              left: 0,\n              top: 0,\n              width: DW,\n              height: DH,\n              borderRadius: 10,\n              overflow: 'hidden',\n              pointerEvents: 'none',\n              opacity: clipOp,\n            }}\n          >\n            <div\n              style={{\n                position: 'absolute',\n                left: 0,\n                right: 0,\n                top: 0,\n                height: 0,\n                transform: `translateY(${y.toFixed(2)}px)`,\n              }}\n            >\n              <div\n                style={{\n                  position: 'absolute',\n                  left: 0,\n                  right: 0,\n                  height: 82,\n                  top: dir > 0 ? -82 : 2.5,\n                  background:\n                    dir > 0\n                      ? 'linear-gradient(180deg,rgba(20,20,22,0),rgba(20,20,22,.5))'\n                      : 'linear-gradient(180deg,rgba(20,20,22,.5),rgba(20,20,22,0))',\n                }}\n              />\n              <div\n                style={{\n                  position: 'absolute',\n                  left: 0,\n                  right: 0,\n                  top: 0,\n                  height: 2.5,\n                  background: INK,\n                }}\n              />\n            </div>\n          </div>\n\n          {/* 四角取景括号：交错落位，向内收 8px（原页 content-box，2px 边长在 34×34 之外） */}\n          {CORNERS.map((c, i) => {\n            const p = seg(t, 0.08 + i * 0.022, 0.08 + i * 0.022 + 0.055, E.outCubic);\n            return (\n              <div\n                key={i}\n                style={{\n                  position: 'absolute',\n                  width: CS,\n                  height: CS,\n                  boxSizing: 'content-box',\n                  opacity: p,\n                  transform: `translate(${(1 - p) * 8 * c.dx}px,${(1 - p) * 8 * c.dy}px)`,\n                  ...c.pos,\n                }}\n              />\n            );\n          })}\n        </div>\n      </div>\n    </DesignStage>\n  );\n};\n\n\n"
}