{
  "slug": "segmented-thumb-hero",
  "title": "Segmented thumb hero",
  "tagline": "A segmented control's thumb shift becomes the star of a close-up — a cursor clicks it and a new icon springs in.",
  "description": "A segmented control's thumb shift becomes the star of a close-up — a cursor clicks it and a new icon springs in.",
  "category": "interaction",
  "tags": [
    "shotcraft",
    "remotion",
    "interaction"
  ],
  "tier": "free",
  "license": "Apache-2.0",
  "spec": {
    "width": 1920,
    "height": 1080,
    "fps": 30,
    "duration": 3.71,
    "aspect": "16:9"
  },
  "kind": "remotion",
  "params_schema": {
    "type": "object",
    "properties": {}
  },
  "default_params": {},
  "preview": {
    "poster": "https://gimgs.net/motion/media/segmented-thumb-hero/poster-44347a918455.jpg",
    "video": "https://gimgs.net/motion/media/segmented-thumb-hero/video-e5c617fba850.mp4"
  },
  "variants": [
    {
      "key": "segmented-thumb-hero",
      "label": "segmented-thumb-hero",
      "description": "A segmented control's thumb shift becomes the star of a close-up — an oversized pill-shaped segmented control springs into view, an outlined arrow cursor slides in from off-screen and clicks, the white thumb slides to the other segment over 8f with ease-out, and the instant it lands a new icon springs in while the old one retracts.",
      "use": "",
      "video": "https://gimgs.net/motion/media/segmented-thumb-hero/video-e5c617fba850.mp4",
      "poster": "https://gimgs.net/motion/media/segmented-thumb-hero/poster-44347a918455.jpg"
    }
  ],
  "credits": {
    "author": "video-shotcraft (Vincent Wei)",
    "url": "https://github.com/Vincentwei1021/video-shotcraft"
  },
  "page_url": "https://gimgs.net/motion/segmented-thumb-hero.html",
  "json_url": "https://gimgs.net/motion/segmented-thumb-hero.json",
  "template_url": "https://gimgs.net/motion/segmented-thumb-hero/template.html",
  "play_url": null,
  "markdown_url": "https://gimgs.net/motion/segmented-thumb-hero.md",
  "updated_at": "2026-09-24T07:40:20.687Z",
  "prompt": "## When to use\n\nAn announcement shot for a \"mode switch / pick one of two\" feature (Ask→Computer, Chat→Agent style); a close-up technique that lets one micro-interaction carry a whole shot\n\n## Intention\n\nWhen a product switches to a new mode, the usual shot swaps out the whole interface. This card does the opposite: it blows the segmented control up to 1080px wide for a close-up, and **the thumb's 8-frame shift itself is the narrative** — \"we went from A to B.\" With no page context at all, the control itself is the stage. The causal chain is complete: the cursor slides in (someone arrived) → clicks with a ripple (a decision was made) → the thumb slides (the world responds) → the new icon pops (a new identity is confirmed). None of the four beats can be dropped: skip the cursor and it looks like the control is moving on its own, skip the icon pop and the switch has no payoff.\n\n## Duration & energy\n\n- Duration: ~3.5s (demo 110f: float-in 18f + cursor 24f + click + slide 8f + icon pop + hold)\n- Energy: Medium (a refined micro-interaction close-up, not a big hit)\n\n## Shot recipe (original Chinese — timing, easing, parameter table, known pitfalls)\n\n## 意图\n产品切到新模式，通常拍法是整个界面换掉。本卡反着来：把 segmented\ncontrol 放大到 1080px 宽拍特写，**thumb 那 8 帧位移本身就是叙事**——\n\"我们从 A 走到了 B\"。没有任何页面上下文，控件即舞台。因果链完整：\n光标滑入（有人来了）→ 按下+涟漪（做了决定）→ thumb 滑动（世界响应）\n→ 新图标弹出（新身份确立）。四拍缺一不可，缺了光标就是 UI 自己在动，\n缺了图标弹出就是切换没有奖励。\n\n## 动效核心\n- 控件特写尺寸 1080×220，从下 200px 弹簧浮入\n  （spring damping 14 / stiffness 120），大阴影随落位收小（24→12px）\n- 光标是超大描边箭头（130px，白底墨描边）：24f Easing.out(cubic)\n  从右下画外滑到目标段上；按下 scale 1→0.86→1（3f 下 4f 回）\n- **thumb 严格 8f** Easing.out(cubic)（demo 52→60f）——还原原片手感；\n  这 8f 是全镜的心跳，前后各拍都在为它服务\n- 到位瞬间双动作同帧起：新图标 spring(damping 10, stiffness 220)\n  过冲弹出；旧图标 6f Easing.in 塌缩且 width 同步归零让文字自然回流\n- 点击涟漪：4px 描边圆环 12f 从点击点扩散淡出（Easing.out(quad)）\n  ——涟漪确认\"按下\"发生过，无涟漪点击读作光标路过\n\n## 参数表\n| 参数 | 典型值 | 调节手感 |\n|------|--------|----------|\n| 控件尺寸 | 1080×220，thumb 内缩 16px | 缩到 <600px 宽就不是特写了，thumb 位移读不出重量 |\n| thumb 时长 | 8f Easing.out(cubic) | >14f 读作慢动作演示视频；<5f 读作瞬移，\"滑\"没了 |\n| 光标滑入 | 24f 从画外，ease-out | linear 或 <14f 读作光标被扔进来；起点必须画外 |\n| 按下 | scale 0.86 + 涟漪 12f | 无按下直接滑 thumb，因果链断，读作自动播放 |\n| 图标弹出 | spring damping 10/stiffness 220 | damping <8 弹三下抢 thumb 的戏；旧图标不收起文字会挤出控件 |\n| 收尾 hold | 新态静置 ≥30f | 切换完立刻切镜，\"新模式\"没被看清 |\n\n## 已知坑\n- demo 在灰阶/占位素材上调校通过——参数是调校起点非实战定稿，\n  首次实战须以真实素材回验\n- 与 input-trigger-moves 分工：cursor-performance 是光标在完整页面里\n  点击推近（有上下文），本卡是控件脱离页面拍无背景特写——同片可共存，\n  但同一次点击别既拍页面版又拍特写版\n- 图标塌缩用 width 归零而不是只 scale——只 scale 会留空位，文字不回流，\n  控件内出现\"幽灵缝\"（demo 里 width 与 scale 同步驱动）\n- 二段以上（3+ 段）控件慎用：thumb 跨多段滑行 8f 太快读不出停靠，\n  拉长又破手感——本卡吃\"二选一\"最顺\n- 涟漪圆环要 zIndex 压在 thumb 之上、圆心锁点击点——跟着 thumb 走\n  就成了 thumb 的拖尾，语义变错\n- 实战替换素材：段标签/图标换成真功能名即可，但\"新段图标带表情\"\n  （demo 笑脸 laptop）是奖励感的一半，纯线框图标会淡不少\n\n## 参考实现\ndemos/interaction/segmented-thumb-hero/\n（SegmentedThumbHero.tsx）\n原片出处：perplexity-promo 2.3–5s\n\n## Reference implementation (Remotion)\n\n- demos/interaction/segmented-thumb-hero/SegmentedThumbHero.tsx — https://raw.githubusercontent.com/Vincentwei1021/video-shotcraft/main/demos/interaction/segmented-thumb-hero/SegmentedThumbHero.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/interaction/segmented-thumb-hero/SegmentedThumbHero.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/interaction/segmented-thumb-hero/SegmentedThumbHero.tsx",
      "url": "https://raw.githubusercontent.com/Vincentwei1021/video-shotcraft/main/demos/interaction/segmented-thumb-hero/SegmentedThumbHero.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": 111,
    "repo": "https://github.com/Vincentwei1021/video-shotcraft",
    "files": [
      "https://raw.githubusercontent.com/Vincentwei1021/video-shotcraft/main/demos/interaction/segmented-thumb-hero/SegmentedThumbHero.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/interaction/segmented-thumb-hero/SegmentedThumbHero.tsx =====\n// segmented-thumb-hero —— 分段控件 thumb 位移当叙事主角拍特写\n// 源：perplexity-promo 2.3–5s。胶囊分段控件（Ask/Computer）带阴影浮入，\n// 超大描边箭头光标滑入点击，白 thumb 左→右 ~8f ease-out 滑动，\n// 到位瞬间新选项前弹出小图标、旧图标收起。\nimport React from 'react';\nimport { AbsoluteFill, useCurrentFrame, interpolate, Easing, spring, useVideoConfig } from 'remotion';\nimport { G } from '../../_fixtures/Fixtures';\n\nconst FONT = 'Helvetica, Arial, sans-serif';\n\n// 超大描边箭头光标\nconst ArrowCursor: React.FC<{ x: number; y: number; press: number }> = ({ x, y, press }) => (\n  <svg\n    width={130}\n    height={150}\n    viewBox=\"0 0 26 30\"\n    style={{\n      position: 'absolute',\n      left: x,\n      top: y,\n      transform: `scale(${1 - press * 0.14})`,\n      transformOrigin: '15% 10%',\n      filter: 'drop-shadow(0 8px 16px rgba(0,0,0,0.25))',\n    }}\n  >\n    <path\n      d=\"M4 2 L4 24 L9.5 18.5 L13 27 L16.8 25.4 L13.3 17 L21 17 Z\"\n      fill=\"#fff\"\n      stroke={G.ink}\n      strokeWidth={1.8}\n      strokeLinejoin=\"round\"\n    />\n  </svg>\n);\n\nconst SmileLaptop: React.FC<{ size: number }> = ({ size }) => (\n  <svg width={size} height={size} viewBox=\"0 0 40 40\">\n    <rect x={7} y={7} width={26} height={19} rx={3} fill=\"none\" stroke={G.ink} strokeWidth={3} />\n    <circle cx={15.5} cy={14.5} r={1.9} fill={G.ink} />\n    <circle cx={24.5} cy={14.5} r={1.9} fill={G.ink} />\n    <path d=\"M14.5 19 Q20 23.5 25.5 19\" stroke={G.ink} strokeWidth={2.6} fill=\"none\" strokeLinecap=\"round\" />\n    <path d=\"M4 31 L36 31\" stroke={G.ink} strokeWidth={3.4} strokeLinecap=\"round\" />\n  </svg>\n);\n\nconst AskIcon: React.FC<{ size: number }> = ({ size }) => (\n  <svg width={size} height={size} viewBox=\"0 0 40 40\">\n    <circle cx={20} cy={20} r={13} fill=\"none\" stroke={G.ink} strokeWidth={3} />\n    <path d=\"M16 17 q0-5 4.5-5 q4.5 0 4.5 4.2 q0 3-3.4 4.4 q-1.6 0.7-1.6 2.6\" stroke={G.ink} strokeWidth={2.8} fill=\"none\" strokeLinecap=\"round\" />\n    <circle cx={20} cy={28} r={1.8} fill={G.ink} />\n  </svg>\n);\n\nexport const SegmentedThumbHero: React.FC = () => {\n  const frame = useCurrentFrame();\n  const { fps } = useVideoConfig();\n\n  // ---- 时间轴 ----\n  const FLOAT_IN = 0; // 控件浮入 0–18\n  const CURSOR_IN = 20; // 光标滑入 20–44\n  const CLICK = 48; // 按下\n  const SLIDE = 52; // thumb 开始滑 52–60 (~8f)\n  const SLIDE_END = 60;\n\n  // 控件浮入：从下 160px 带阴影弹簧浮入\n  const floatT = spring({ frame: frame - FLOAT_IN, fps, config: { damping: 14, stiffness: 120, mass: 0.9 } });\n  const ctrlY = interpolate(floatT, [0, 1], [200, 0]);\n\n  // 控件几何（特写尺寸）\n  const CW = 1080; // 控件宽\n  const CH = 220;\n  const PAD = 16;\n  const SEGW = (CW - PAD * 2) / 2;\n\n  // 光标滑入：ease-out 从右下画外飘到 Computer 段上\n  const curT = interpolate(frame, [CURSOR_IN, CURSOR_IN + 24], [0, 1], {\n    extrapolateLeft: 'clamp',\n    extrapolateRight: 'clamp',\n    easing: Easing.out(Easing.cubic),\n  });\n  const curX = interpolate(curT, [0, 1], [1780, 1210]);\n  const curY = interpolate(curT, [0, 1], [1020, 620]);\n  // 按下动作\n  const press = interpolate(frame, [CLICK, CLICK + 3, CLICK + 7], [0, 1, 0], {\n    extrapolateLeft: 'clamp',\n    extrapolateRight: 'clamp',\n  });\n\n  // thumb 位移：~8f ease-out\n  const thumbT = interpolate(frame, [SLIDE, SLIDE_END], [0, 1], {\n    extrapolateLeft: 'clamp',\n    extrapolateRight: 'clamp',\n    easing: Easing.out(Easing.cubic),\n  });\n  const thumbX = PAD + thumbT * SEGW;\n\n  // 到位瞬间：Computer 图标弹出（过冲），Ask 图标收起\n  const iconIn = spring({ frame: frame - SLIDE_END, fps, config: { damping: 10, stiffness: 220, mass: 0.6 } });\n  const laptopScale = frame >= SLIDE_END ? iconIn : 0;\n  const askScale = interpolate(frame, [SLIDE, SLIDE + 6], [1, 0], {\n    extrapolateLeft: 'clamp',\n    extrapolateRight: 'clamp',\n    easing: Easing.in(Easing.cubic),\n  });\n\n  // 点击涟漪\n  const rippleT = interpolate(frame, [CLICK, CLICK + 12], [0, 1], {\n    extrapolateLeft: 'clamp',\n    extrapolateRight: 'clamp',\n    easing: Easing.out(Easing.quad),\n  });\n\n  const labelStyle = (active: boolean): React.CSSProperties => ({\n    fontFamily: FONT,\n    fontWeight: 700,\n    fontSize: 72,\n    color: active ? G.ink : G.mid,\n    transition: 'none',\n    display: 'flex',\n    alignItems: 'center',\n    justifyContent: 'center',\n    gap: 22,\n    width: SEGW,\n    height: CH - PAD * 2,\n    position: 'relative',\n    zIndex: 2,\n  });\n\n  const askActive = thumbT < 0.5;\n\n  return (\n    <AbsoluteFill style={{ background: G.bg, alignItems: 'center', justifyContent: 'center' }}>\n      {/* 分段控件 */}\n      <div\n        style={{\n          width: CW,\n          height: CH,\n          borderRadius: CH / 2,\n          background: '#e4e4e2',\n          border: `3px solid ${G.border}`,\n          boxShadow: `0 ${24 - floatT * 12}px ${70 - floatT * 20}px rgba(0,0,0,0.22)`,\n          transform: `translateY(${ctrlY}px)`,\n          opacity: Math.min(1, floatT * 1.5),\n          position: 'relative',\n          display: 'flex',\n          alignItems: 'center',\n          padding: PAD,\n          boxSizing: 'border-box',\n        }}\n      >\n        {/* 白色 thumb */}\n        <div\n          style={{\n            position: 'absolute',\n            left: thumbX,\n            top: PAD,\n            width: SEGW,\n            height: CH - PAD * 2,\n            borderRadius: (CH - PAD * 2) / 2,\n            background: '#fff',\n            boxShadow: '0 6px 20px rgba(0,0,0,0.18)',\n            zIndex: 1,\n          }}\n        />\n        {/* 点击涟漪 */}\n        {rippleT > 0 && rippleT < 1 && (\n          <div\n            style={{\n              position: 'absolute',\n              left: PAD + SEGW + SEGW / 2 - 130 * rippleT,\n              top: CH / 2 - PAD - 130 * rippleT + (CH - PAD * 2) / 2 - (CH / 2 - PAD),\n              width: 260 * rippleT,\n              height: 260 * rippleT,\n              borderRadius: '50%',\n              border: `4px solid ${G.mid}`,\n              opacity: 1 - rippleT,\n              zIndex: 3,\n            }}\n          />\n        )}\n        {/* Ask 段 */}\n        <div style={labelStyle(askActive)}>\n          <span\n            style={{\n              display: 'inline-flex',\n              transform: `scale(${askScale})`,\n              width: askScale < 0.05 ? 0 : 78,\n              overflow: 'visible',\n            }}\n          >\n            <AskIcon size={78} />\n          </span>\n          Ask\n        </div>\n        {/* Computer 段 */}\n        <div style={labelStyle(!askActive)}>\n          <span\n            style={{\n              display: 'inline-flex',\n              transform: `scale(${laptopScale})`,\n              width: laptopScale < 0.05 ? 0 : 78,\n              overflow: 'visible',\n            }}\n          >\n            <SmileLaptop size={78} />\n          </span>\n          Computer\n        </div>\n      </div>\n      <ArrowCursor x={curX} y={curY} press={press} />\n    </AbsoluteFill>\n  );\n};\n\n\n"
}