{
  "slug": "before-after-slider-scrub",
  "title": "Before/After Slider Scrub",
  "tagline": "A before/after comparison bar — \"before\" and \"after\" versions stacked, the divider whips fast then scrubs slow, revealing the new version as it \"develops\" past.",
  "description": "A before/after comparison bar — \"before\" and \"after\" versions stacked, the divider whips fast then scrubs slow, revealing the new version as it \"develops\" pas",
  "category": "data",
  "tags": [
    "shotcraft",
    "remotion",
    "data",
    "interaction"
  ],
  "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/before-after-slider-scrub/poster-e8e48ddbb5cb.jpg",
    "video": "https://gimgs.net/motion/media/before-after-slider-scrub/video-37b2fed87cb4.mp4"
  },
  "variants": [
    {
      "key": "before-after-slider-scrub",
      "label": "before-after-slider-scrub",
      "description": "A before/after comparison bar — \"before\" and \"after\" versions stacked, the divider whips fast then scrubs slow, revealing the new version as it \"develops\" past.",
      "use": "",
      "video": "https://gimgs.net/motion/media/before-after-slider-scrub/video-37b2fed87cb4.mp4",
      "poster": "https://gimgs.net/motion/media/before-after-slider-scrub/poster-e8e48ddbb5cb.jpg"
    }
  ],
  "credits": {
    "author": "video-shotcraft (Vincent Wei)",
    "url": "https://github.com/Vincentwei1021/video-shotcraft"
  },
  "page_url": "https://gimgs.net/motion/before-after-slider-scrub.html",
  "json_url": "https://gimgs.net/motion/before-after-slider-scrub.json",
  "template_url": "https://gimgs.net/motion/before-after-slider-scrub/template.html",
  "play_url": null,
  "markdown_url": "https://gimgs.net/motion/before-after-slider-scrub.md",
  "updated_at": "2026-09-24T07:39:44.636Z",
  "prompt": "## When to use\n\nThe effect-comparison segment for AI enhancement/optimization/refactor features (\"before vs. after\" told in one shot).\n\n## Intention\n\nThe standard comparison grammar of AI-image launch films: the same frame's \"before\" (low-contrast, hazy gray) and \"after\" (clear) stacked, with a round-handled vertical divider whipping fast from the left edge to 70% (overshoot and spring back), holding a beat, then scrubbing back slowly at about 1/5 the speed so viewers can see the difference clearly — the speed contrast between the fast whip and slow scrub is the rhythm itself: the fast whip announces \"it changed,\" the slow scrub proves \"where it changed.\"\n\n## Duration & energy\n\n- Duration: 4–5s\n- Energy: Medium (the fast whip is the impact beat, the slow scrub is the reading period).\n\n## Shot recipe (original Chinese — timing, easing, parameter table, known pitfalls)\n\n## 意图\nAI 图像产品发布片的标准对比语法：同一画面\"处理前\"（低对比灰蒙）\n与\"处理后\"（清晰版）叠放，带圆手柄的竖分割杆先从左端猛甩到 70%\n（overshoot 回弹）、停一拍，再以约 1/5 的速度慢扫回来让观众看清\n差异——**快甩慢扫的速度对比就是节奏**：快甩宣告\"变了\"，\n慢扫证明\"变在哪\"。\n\n## 动作阶段\n| 阶段 | 帧段参考 | 内容 |\n|------|------|------|\n| 1 静置 | f0–14 | 杆停 8%，画面几乎全是\"处理前\" |\n| 2 快甩 | 12f | 8%→76%（out cubic）过冲 |\n| 3 回弹 | 12f | 76%→70% |\n| 4 停顿 | ~18f | 让观众看清 after |\n| 5 慢扫 | ~48f | 70%→40% 二次展示差异 |\n| 6 定格 | 末 ≥40f | 真静止 |\n\n## 参数表\n| 参数 | 典型值 | 调节手感 |\n|------|--------|----------|\n| 速度比 | 快甩 12f vs 慢扫 48f，约 5:1 | 比例 <3:1 节奏对比不可感 |\n| 揭示 | after 层 clip-path inset 右边界跟随杆 x | 杆和揭示边界错位即穿帮 |\n| 手柄 | 圆形手柄 + 速度差分驱动 scaleX 微拉伸（峰值 1.18），静止自动归 1 | 拉伸给手柄\"被甩\"的重量感 |\n| before 处理 | 低对比灰蒙版 | before 太难看是自黑，\"差\"要可信（真实旧版/未优化态） |\n| 角标 | BEFORE/AFTER 角标放内容区 | demo 判例：初版压住侧栏/头像穿帮 |\n\n## 已知坑\n- demo 在灰阶/占位素材上调校通过——参数是调校起点非实战定稿，\n  首次实战须以真实素材回验\n- 两版必须同布局同机位——布局不同读作两个页面，对比失效\n  （theme-switch-moves 同款约束）\n- 实战 before/after 都用真实截图（Q1）：before 是注入旧数据/关闭\n  功能的真实态，不手搓\"故意丑\"的假 before\n- 慢扫终点停 40% 而非 0%——留一段 after 在画面上定格（结论可见）\n- 声音：快甩一声 whoosh + 回弹轻 tick，慢扫无声或极轻摩擦音（S4）\n\n## 参考实现\ndemos/data/before-after-slider-scrub/\n（BeforeAfterSliderScrub.tsx）\n\n## Reference implementation (Remotion)\n\n- demos/data/before-after-slider-scrub/BeforeAfterSliderScrub.tsx — https://raw.githubusercontent.com/Vincentwei1021/video-shotcraft/main/demos/data/before-after-slider-scrub/BeforeAfterSliderScrub.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/data/before-after-slider-scrub/BeforeAfterSliderScrub.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/data/before-after-slider-scrub/BeforeAfterSliderScrub.tsx",
      "url": "https://raw.githubusercontent.com/Vincentwei1021/video-shotcraft/main/demos/data/before-after-slider-scrub/BeforeAfterSliderScrub.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/data/before-after-slider-scrub/BeforeAfterSliderScrub.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/data/before-after-slider-scrub/BeforeAfterSliderScrub.tsx =====\n// before-after-slider-scrub —— 前后对比拉杆\n// FakeDashboard 两版叠放：before = 低对比灰蒙版（\"处理前\"），after = 正常清晰版。\n// 竖分割杆带圆手柄：从左端 8% 快甩到 70%（overshoot 至 76% 回弹），\n// 再慢速回扫到 40% 停住。杆经过处 after 揭出（clip-path inset 跟随杆 x）。\n// 先快甩后慢扫速度对比是节奏关键。f=110 后全静止（40f）。\nimport React from 'react';\nimport { useCurrentFrame, interpolate, Easing } from 'remotion';\nimport { G, FakeDashboard } from '../../_fixtures/Fixtures';\n\nconst CL = { extrapolateLeft: 'clamp', extrapolateRight: 'clamp' } as const;\n\n// 时间轴（杆位置以画面宽度百分比表示）\nconst T0 = 14; // 前 14f 初始静置，杆停在 8%\nconst FLING = 26; // 快甩：14→26（12f 冲到 76%）\nconst BOUNCE = 38; // 回弹：26→38 落到 70%\nconst HOLD = 56; // 停顿看清\nconst SCRUB = 104; // 慢扫：56→104（48f 回到 40%）——速度约为快甩的 1/5\n\nconst posAt = (f: number): number => {\n  if (f < FLING)\n    return interpolate(f, [T0, FLING], [8, 76], { easing: Easing.out(Easing.cubic), ...CL });\n  if (f < BOUNCE)\n    return interpolate(f, [FLING, BOUNCE], [76, 70], { easing: Easing.inOut(Easing.cubic), ...CL });\n  if (f < HOLD) return 70;\n  return interpolate(f, [HOLD, SCRUB], [70, 40], { easing: Easing.inOut(Easing.quad), ...CL });\n};\n\nexport const BeforeAfterSliderScrub: React.FC = () => {\n  const frame = useCurrentFrame();\n  const p = posAt(frame); // 杆位置 %\n  const x = (p / 100) * 1920;\n\n  // 手柄挤压：速度差分驱动（快甩期手柄微拉伸，静止归 1）\n  const v = Math.abs(posAt(frame) - posAt(frame - 1)); // %/frame\n  const squish = 1 + Math.min(v / 8, 1) * 0.18;\n\n  return (\n    <div style={{ width: 1920, height: 1080, position: 'relative', overflow: 'hidden', background: G.bg }}>\n      {/* before：低对比灰蒙版模拟\"处理前\" */}\n      <div style={{ position: 'absolute', inset: 0, filter: 'contrast(0.55) brightness(1.06) grayscale(1)' }}>\n        <FakeDashboard variant=\"A\" />\n      </div>\n      <div style={{ position: 'absolute', inset: 0, background: 'rgba(160,160,158,0.35)' }} />\n\n      {/* after：正常清晰版，杆左侧揭出 */}\n      <div style={{ position: 'absolute', inset: 0, clipPath: `inset(0 ${1920 - x}px 0 0)` }}>\n        <FakeDashboard variant=\"A\" />\n      </div>\n\n      {/* 分割杆 + 圆手柄 */}\n      <div\n        style={{\n          position: 'absolute',\n          left: x - 3,\n          top: 0,\n          width: 6,\n          height: 1080,\n          background: '#ffffff',\n          boxShadow: '0 0 14px rgba(0,0,0,0.35)',\n        }}\n      />\n      <div\n        style={{\n          position: 'absolute',\n          left: x - 44,\n          top: 540 - 44,\n          width: 88,\n          height: 88,\n          borderRadius: 44,\n          background: '#ffffff',\n          border: `3px solid ${G.border}`,\n          boxShadow: '0 6px 24px rgba(0,0,0,0.3)',\n          transform: `scaleX(${squish})`,\n          display: 'flex',\n          alignItems: 'center',\n          justifyContent: 'center',\n          gap: 10,\n          boxSizing: 'border-box',\n        }}\n      >\n        {/* 左右箭头示意（灰阶三角） */}\n        <div style={{ width: 0, height: 0, borderTop: '10px solid transparent', borderBottom: '10px solid transparent', borderRight: `14px solid ${G.mid}` }} />\n        <div style={{ width: 0, height: 0, borderTop: '10px solid transparent', borderBottom: '10px solid transparent', borderLeft: `14px solid ${G.mid}` }} />\n      </div>\n\n      {/* 角标：BEFORE / AFTER 灰块标签 */}\n      <div style={{ position: 'absolute', left: 260, top: 100, padding: '10px 22px', borderRadius: 10, background: 'rgba(47,47,47,0.85)', display: 'flex', gap: 8 }}>\n        {Array.from({ length: 5 }).map((_, i) => (\n          <div key={i} style={{ width: 16, height: 20, borderRadius: 4, background: '#e6e6e4' }} />\n        ))}\n      </div>\n      <div style={{ position: 'absolute', right: 60, top: 100, padding: '10px 22px', borderRadius: 10, background: 'rgba(240,240,238,0.9)', border: `2px solid ${G.border}`, display: 'flex', gap: 8 }}>\n        {Array.from({ length: 5 }).map((_, i) => (\n          <div key={i} style={{ width: 16, height: 20, borderRadius: 4, background: G.ink }} />\n        ))}\n      </div>\n    </div>\n  );\n};\n\n\n"
}