{
  "slug": "wipe-transitions",
  "title": "Geometric wipe transitions",
  "tagline": "Two geometric wipe transitions — clock-wipe, a radar-hand sweep changing the page, and blinds-slice, 12 vertical strips flipping in staggered waves.",
  "description": "Two geometric wipe transitions — clock-wipe, a radar-hand sweep changing the page, and blinds-slice, 12 vertical strips flipping in staggered waves.",
  "category": "transition",
  "tags": [
    "shotcraft",
    "remotion",
    "transition"
  ],
  "tier": "free",
  "license": "Apache-2.0",
  "spec": {
    "width": 1920,
    "height": 1080,
    "fps": 30,
    "duration": 5.06,
    "aspect": "16:9"
  },
  "kind": "remotion",
  "params_schema": {
    "type": "object",
    "properties": {}
  },
  "default_params": {},
  "preview": {
    "poster": "https://gimgs.net/motion/media/wipe-transitions/poster-0d551976d813.jpg",
    "video": "https://gimgs.net/motion/media/wipe-transitions/video-9e7afa5633df.mp4"
  },
  "variants": [
    {
      "key": "clock-wipe",
      "label": "clock-wipe",
      "description": "Page B sits underneath a fan-shaped clip-path polygon; a hand sweeps clockwise from screen center at constant speed through 360°, revealing B as it passes, its scanning edge carrying multiple bright lines",
      "use": "Data/status refresh meaning; dashboard-type pages",
      "video": "https://gimgs.net/motion/media/wipe-transitions/video-9e7afa5633df.mp4",
      "poster": "https://gimgs.net/motion/media/wipe-transitions/poster-0d551976d813.jpg"
    },
    {
      "key": "blinds-slice",
      "label": "blinds-slice",
      "description": "12 vertical 160px strips with overflow hidden and an inner full page offset by a negative margin to align; inside each strip, A's left edge shrinks via scaleX(1-p) while B's right edge expands via scaleX(p), staggered in delay to form a wave, with a bright line sweeping along the seam",
      "use": "Page-forward/advancing meaning; pages with a horizontal reading flow",
      "video": "https://gimgs.net/motion/media/wipe-transitions/video-2672dab7e8a6.mp4",
      "poster": "https://gimgs.net/motion/media/wipe-transitions/poster-blinds-slice-62cca7a3d83a.jpg"
    }
  ],
  "credits": {
    "author": "video-shotcraft (Vincent Wei)",
    "url": "https://github.com/Vincentwei1021/video-shotcraft"
  },
  "page_url": "https://gimgs.net/motion/wipe-transitions.html",
  "json_url": "https://gimgs.net/motion/wipe-transitions.json",
  "template_url": "https://gimgs.net/motion/wipe-transitions/template.html",
  "play_url": null,
  "markdown_url": "https://gimgs.net/motion/wipe-transitions.md",
  "updated_at": "2026-09-24T07:40:35.603Z",
  "prompt": "## When to use\n\nA universal transition where neither the old nor new page moves and a geometric boundary sweeps across to complete the handoff; works anywhere, with no need for a suitable element already in the composition\n\n## Intention\n\nThe transition library already has three families: travel-through (drilling in), hidden-cut (concealed), and solid-body (the page as a physical object). This card is the fourth family, **geometric wipe** — neither the old nor new page moves, a geometric boundary sweeps across to complete the handoff, and the wipe's shape carries the meaning: style A's circular sweep is \"a dashboard refreshing its data\"; style B's strip sweep is \"venetian blinds flipping open to change the page.\" The difference from shot-transitions style F's element-mask wipe: F uses a real element inside the page as the mask (depends on the composition), this card is pure geometry — its universality is the whole point. Choose by meaning: A for data refresh, B for a horizontal page-forward feel.\n\n## Duration & energy\n\n- Duration: Single style: pre-state ≥20f + wipe 32–60f + tail ≥40f, ~5s (150f)\n- Energy: Medium\n\n## Shot recipe (original Chinese — timing, easing, parameter table, known pitfalls)\n\n## 意图\n转场库已有三族：穿越系（钻进去）、藏切系（遮住切）、体块系（页面是\n实体）。本卡是第四族**几何擦除系**——新旧页都不动，一条几何边界扫过\n完成交接，擦除的形状即语义：A 圆扫是\"仪表盘刷新了一屏数据\"，B 条扫\n是\"百叶窗逐叶翻面换页\"。与 shot-transitions F 元素遮罩擦除的区别：\nF 用页面内真实元素当遮罩（依赖构图），本卡是纯几何形——通用性即定位。\n按语义选：数据刷新用 A，横向推进翻页用 B。\n\n## 两式选型\n| 式 | 做法 | 适用 |\n|----|------|------|\n| A clock-wipe | B 页上层套扇形 clip-path polygon，指针从屏心 12 点顺时针匀速扫 360°，扫过处露 B；扫描沿带多层亮线 | 数据/状态刷新语义；仪表盘类页面 |\n| B blinds-slice | 12 根 160px 竖条 overflow hidden + 内层整页负 margin 对位；条内 A scaleX(1-p) 左缘收缩、B scaleX(p) 右缘展开，错峰 delay 成波，缝上亮线随波扫 | 翻页/推进语义；横向阅读动线页面 |\n\n## 参数表\n| 参数 | 典型值 | 调节手感 |\n|------|--------|----------|\n| A 扇形 | 中心屏心(960,540)、半径 1400 盖满四角，固定 73 顶点(SEGS=72) | 顶点数恒定且够密（40+）防锯齿跳变——命门 |\n| A 扫速 | 30–90f 指针 0→360° 纯 linear，双向 clamp | 雷达要匀速；加缓动就不是雷达是钟摆 |\n| A 亮线 | 四层 SVG line：26px 白 0.35 柔光 + 13px 白 0.60 + 9px 黑 0.55 暗描边 + 4px 白核 | 白底判例：纯提亮不可见，必须加暗描边；首渲 3px 白线太弱，加码 1.5x 才过 |\n| B 波 | 20–52f：delay=列号×2f，每条 10f Easing.in(cubic) | 交接点恒为 x+160(1-p)，数学上无露底 |\n| B 缝亮线 | 三层 SVG：16px 白 0.45 柔光 + 6px 黑 0.55 暗描边 + 3px 白核，进出各 2f 淡入淡出 | 同 A 白底判例 |\n| 摘罩 | A 96f / B 52f 起条件卸载全部擦除结构，B 页直出 | opacity 0 不算摘，残留 clip-path 毁真静止 |\n| 收尾 | A 54f / B 98f 真静止（≥40f） | R1 |\n\n## 已知坑\n- demo 在灰阶/占位素材上调校通过——参数是调校起点非实战定稿，\n  首次实战须以真实素材回验\n- 擦除边界必须带亮线/高光——无亮线的 wipe 读作 PPT 转场，命门；\n  亮线在浅色区靠黑描边、深色区靠白核，两侧都要可读\n- A 与 circle-match-iris 的区别：iris 是从锚点炸开的圆（半径在长），\n  clock 是角度在扫（半径恒定）——别因为都是圆混用\n- 几何擦除全片 ≤2 次，且 A/B 别同片连用（两次\"边界扫过\"读作模板感）\n- 亮线淡出与摘罩必须同帧衔接（A 90–96f 淡出、96f 卸载），\n  差一帧就是残线穿帮\n\n## 参考实现\ndemos/transition/wipe-transitions/\n（BlindsSlice.tsx / ClockWipe.tsx）\n\n## Reference implementation (Remotion)\n\n- demos/transition/wipe-transitions/BlindsSlice.tsx — https://raw.githubusercontent.com/Vincentwei1021/video-shotcraft/main/demos/transition/wipe-transitions/BlindsSlice.tsx\n- demos/transition/wipe-transitions/ClockWipe.tsx — https://raw.githubusercontent.com/Vincentwei1021/video-shotcraft/main/demos/transition/wipe-transitions/ClockWipe.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/transition/wipe-transitions/BlindsSlice.tsx, demos/transition/wipe-transitions/ClockWipe.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/transition/wipe-transitions/BlindsSlice.tsx",
      "url": "https://raw.githubusercontent.com/Vincentwei1021/video-shotcraft/main/demos/transition/wipe-transitions/BlindsSlice.tsx"
    },
    {
      "path": "demos/transition/wipe-transitions/ClockWipe.tsx",
      "url": "https://raw.githubusercontent.com/Vincentwei1021/video-shotcraft/main/demos/transition/wipe-transitions/ClockWipe.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": 152,
    "repo": "https://github.com/Vincentwei1021/video-shotcraft",
    "files": [
      "https://raw.githubusercontent.com/Vincentwei1021/video-shotcraft/main/demos/transition/wipe-transitions/BlindsSlice.tsx",
      "https://raw.githubusercontent.com/Vincentwei1021/video-shotcraft/main/demos/transition/wipe-transitions/ClockWipe.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/transition/wipe-transitions/BlindsSlice.tsx =====\n// blinds-slice｜百叶窗切条错峰擦除\n// FakeDashboard A → B。12 根 160px 竖条，从左到右 delay=列号×2f，\n// 每条 10f 内完成翻换：条内 A scaleX 1→0（origin 左缘）与 B scaleX 0→1\n// （origin 右缘）共用同一进度 p（Easing.in(cubic)），交接缝恒等于\n// x+160(1-p)，数学上无露底。缝上亮线（柔光+暗描边+白核）随波扫过。\n// 波 20–52f；52f 起摘罩（整页 B 直出、条结构与亮线全部卸载），\n// 52–150f 真静止 98f ≥ 40f。帧确定，无随机源。\nimport React from 'react';\nimport { AbsoluteFill, useCurrentFrame, interpolate, Easing } from 'remotion';\nimport { FakeDashboard } from '../../_fixtures/Fixtures';\n\nconst STRIPS = 12;\nconst W = 160; // 每条宽 12×160 = 1920\nconst WAVE_START = 20;\nconst STAGGER = 2; // 列号 × 2f\nconst FLIP = 10; // 每条 10f 完成翻换\nconst WAVE_END = WAVE_START + (STRIPS - 1) * STAGGER + FLIP; // 52\n\n// 条内某页的切片：外层 160 宽裁剪，内层整页 1920 负 margin 对位\nconst Slice: React.FC<{ x: number; variant: 'A' | 'B' }> = ({ x, variant }) => (\n  <div style={{ width: 1920, height: 1080, marginLeft: -x }}>\n    <FakeDashboard variant={variant} />\n  </div>\n);\n\nexport const BlindsSlice: React.FC = () => {\n  const frame = useCurrentFrame();\n\n  // 摘罩：波完成后条结构全部卸载，B 整页直出\n  if (frame >= WAVE_END) {\n    return (\n      <AbsoluteFill style={{ background: '#ececea' }}>\n        <FakeDashboard variant=\"B\" />\n      </AbsoluteFill>\n    );\n  }\n\n  const seams: { x: number; opacity: number }[] = [];\n\n  const strips = Array.from({ length: STRIPS }).map((_, i) => {\n    const x = i * W;\n    const start = WAVE_START + i * STAGGER;\n    const end = start + FLIP;\n\n    // 未开始：纯 A 切片；已完成：纯 B 切片\n    if (frame < start) {\n      return (\n        <div key={i} style={{ position: 'absolute', left: x, top: 0, width: W, height: 1080, overflow: 'hidden' }}>\n          <Slice x={x} variant=\"A\" />\n        </div>\n      );\n    }\n    if (frame >= end) {\n      return (\n        <div key={i} style={{ position: 'absolute', left: x, top: 0, width: W, height: 1080, overflow: 'hidden' }}>\n          <Slice x={x} variant=\"B\" />\n        </div>\n      );\n    }\n\n    // 翻换中：A、B 共用同一进度 p——A 宽 160(1-p) 靠左，B 宽 160p 靠右，\n    // 交接点恒为 x+160(1-p)，无露底\n    const p = interpolate(frame, [start, end], [0, 1], {\n      extrapolateLeft: 'clamp',\n      extrapolateRight: 'clamp',\n      easing: Easing.in(Easing.cubic),\n    });\n\n    // 缝亮线：进出各 2f 线性淡入淡出\n    const seamOpacity = Math.min(\n      interpolate(frame, [start, start + 2], [0, 1], { extrapolateLeft: 'clamp', extrapolateRight: 'clamp' }),\n      interpolate(frame, [end - 2, end], [1, 0], { extrapolateLeft: 'clamp', extrapolateRight: 'clamp' }),\n    );\n    seams.push({ x: x + W * (1 - p), opacity: seamOpacity });\n\n    return (\n      <div key={i} style={{ position: 'absolute', left: x, top: 0, width: W, height: 1080, overflow: 'hidden' }}>\n        {/* A：向左缘收缩 */}\n        <div style={{ position: 'absolute', inset: 0, overflow: 'hidden', transform: `scaleX(${1 - p})`, transformOrigin: '0% 50%' }}>\n          <Slice x={x} variant=\"A\" />\n        </div>\n        {/* B：从右缘展开 */}\n        <div style={{ position: 'absolute', inset: 0, overflow: 'hidden', transform: `scaleX(${p})`, transformOrigin: '100% 50%' }}>\n          <Slice x={x} variant=\"B\" />\n        </div>\n      </div>\n    );\n  });\n\n  return (\n    <AbsoluteFill style={{ background: '#ececea' }}>\n      {strips}\n      {/* 缝亮线：白底判例——纯提亮不可见，柔光 + 暗描边 + 白核三层 */}\n      {seams.length > 0 && (\n        <svg width={1920} height={1080} style={{ position: 'absolute', inset: 0, pointerEvents: 'none' }}>\n          {seams.map((s, i) => (\n            <g key={i} opacity={s.opacity}>\n              <line x1={s.x} y1={0} x2={s.x} y2={1080} stroke=\"rgba(255,255,255,0.45)\" strokeWidth={16} />\n              <line x1={s.x} y1={0} x2={s.x} y2={1080} stroke=\"rgba(0,0,0,0.55)\" strokeWidth={6} />\n              <line x1={s.x} y1={0} x2={s.x} y2={1080} stroke=\"rgba(255,255,255,0.95)\" strokeWidth={3} />\n            </g>\n          ))}\n        </svg>\n      )}\n    </AbsoluteFill>\n  );\n};\n\n\n// ===== demos/transition/wipe-transitions/ClockWipe.tsx =====\n// clock-wipe｜时钟扫描擦除\n// FakeDashboard A → B。30–90f 一根隐形雷达指针从 12 点方向顺时针扫一圈，\n// B 页在上层用大扇形 clip-path polygon 逐帧张开；扫描沿带亮线（白核+暗描边+柔光）。\n// 90–96f 亮线淡出，96f 起摘罩（B 直接满屏、无 clip-path、亮线卸载），\n// 96–150f 真静止 54f ≥ 40f。帧确定，无随机。\nimport React from 'react';\nimport { AbsoluteFill, useCurrentFrame, interpolate } from 'remotion';\nimport { FakeDashboard } from '../../_fixtures/Fixtures';\n\nconst CX = 960;\nconst CY = 540;\nconst R = 1400; // 大于中心到角的距离 ~1101，扇形完全盖角\nconst SEGS = 72; // 顶点数固定且够密，避免锯齿跳变\n\n// 12 点方向为 0°，顺时针（屏幕坐标 y 向下）\nconst polar = (deg: number, r: number): [number, number] => {\n  const a = (deg * Math.PI) / 180;\n  return [CX + r * Math.sin(a), CY - r * Math.cos(a)];\n};\n\nconst fanClip = (theta: number): string => {\n  const pts: string[] = [`${CX}px ${CY}px`];\n  for (let i = 0; i <= SEGS; i++) {\n    const [x, y] = polar((theta * i) / SEGS, R);\n    pts.push(`${x.toFixed(1)}px ${y.toFixed(1)}px`);\n  }\n  return `polygon(${pts.join(', ')})`;\n};\n\nexport const ClockWipe: React.FC = () => {\n  const frame = useCurrentFrame();\n\n  // 30–90f 指针 0→360°，linear（时钟扫描要匀速才像雷达）\n  const theta = interpolate(frame, [30, 90], [0, 360], {\n    extrapolateLeft: 'clamp',\n    extrapolateRight: 'clamp',\n  });\n  const wipeDone = frame >= 90;\n\n  // 亮线：扫描期间常亮，90–96f 线性淡出，96f 起条件卸载（摘罩判例）\n  const lineOpacity = interpolate(frame, [90, 96], [1, 0], {\n    extrapolateLeft: 'clamp',\n    extrapolateRight: 'clamp',\n  });\n  const lineMounted = frame >= 30 && frame < 96;\n\n  const [x2, y2] = polar(theta, R);\n\n  return (\n    <AbsoluteFill style={{ background: '#ececea' }}>\n      {/* 底层：A 页。擦完后卸载（上层 B 已满屏） */}\n      {!wipeDone && (\n        <AbsoluteFill>\n          <FakeDashboard variant=\"A\" />\n        </AbsoluteFill>\n      )}\n\n      {/* 上层：B 页。扫描期挂扇形 clip-path，擦完后摘罩直出 */}\n      {frame >= 30 && (\n        <AbsoluteFill style={wipeDone ? undefined : { clipPath: fanClip(theta) }}>\n          <FakeDashboard variant=\"B\" />\n        </AbsoluteFill>\n      )}\n\n      {/* 扫描亮线：柔光 + 暗描边 + 白核，从屏心指向当前角度 */}\n      {lineMounted && (\n        <svg\n          width={1920}\n          height={1080}\n          style={{ position: 'absolute', inset: 0, opacity: lineOpacity, pointerEvents: 'none' }}\n        >\n          {/* 柔光带（QA 后加码 1.5x：白底看不清就加深+加宽） */}\n          <line x1={CX} y1={CY} x2={x2} y2={y2} stroke=\"rgba(255,255,255,0.35)\" strokeWidth={26} strokeLinecap=\"round\" />\n          <line x1={CX} y1={CY} x2={x2} y2={y2} stroke=\"rgba(255,255,255,0.60)\" strokeWidth={13} strokeLinecap=\"round\" />\n          {/* 暗描边：白底上\"提亮\"不可见，加深保证浅色区也读得出指针 */}\n          <line x1={CX} y1={CY} x2={x2} y2={y2} stroke=\"rgba(0,0,0,0.55)\" strokeWidth={9} strokeLinecap=\"round\" />\n          {/* 白核 */}\n          <line x1={CX} y1={CY} x2={x2} y2={y2} stroke=\"rgba(255,255,255,0.95)\" strokeWidth={4} strokeLinecap=\"round\" />\n        </svg>\n      )}\n    </AbsoluteFill>\n  );\n};\n\n\n"
}