{
  "slug": "picker-carousel-feature-cycle",
  "title": "Picker carousel feature cycle",
  "tagline": "A mobile-style vertical picker — the focus pill stays still while content scrolls through it, snapping to a full stop each time.",
  "description": "A mobile-style vertical picker — the focus pill stays still while content scrolls through it, snapping to a full stop each time.",
  "category": "interaction",
  "tags": [
    "shotcraft",
    "remotion",
    "interaction"
  ],
  "tier": "free",
  "license": "Apache-2.0",
  "spec": {
    "width": 1920,
    "height": 1080,
    "fps": 30,
    "duration": 4.1,
    "aspect": "16:9"
  },
  "kind": "remotion",
  "params_schema": {
    "type": "object",
    "properties": {}
  },
  "default_params": {},
  "preview": {
    "poster": "https://gimgs.net/motion/media/picker-carousel-feature-cycle/poster-b3f4452d8b86.jpg",
    "video": "https://gimgs.net/motion/media/picker-carousel-feature-cycle/video-d93d373c3aa1.mp4"
  },
  "variants": [
    {
      "key": "picker-carousel-feature-cycle",
      "label": "picker-carousel-feature-cycle",
      "description": "A mobile-style vertical picker — the focus pill stays fixed while content passes through it, each item decelerating with a pronounced outQuint ease before coming to a complete stop; opacity/font size/grayscale are all tiered by distance from center, and the pill does a very slight scaleY breathe once settled.",
      "use": "",
      "video": "https://gimgs.net/motion/media/picker-carousel-feature-cycle/video-d93d373c3aa1.mp4",
      "poster": "https://gimgs.net/motion/media/picker-carousel-feature-cycle/poster-b3f4452d8b86.jpg"
    }
  ],
  "credits": {
    "author": "video-shotcraft (Vincent Wei)",
    "url": "https://github.com/Vincentwei1021/video-shotcraft"
  },
  "page_url": "https://gimgs.net/motion/picker-carousel-feature-cycle.html",
  "json_url": "https://gimgs.net/motion/picker-carousel-feature-cycle.json",
  "template_url": "https://gimgs.net/motion/picker-carousel-feature-cycle/template.html",
  "play_url": null,
  "markdown_url": "https://gimgs.net/motion/picker-carousel-feature-cycle.md",
  "updated_at": "2026-09-24T07:40:13.595Z",
  "prompt": "## When to use\n\nA shot that reads off feature/scene names one by one; a \"pick one\" interaction demo; a picker-control showcase for mobile products\n\n## Intention\n\nGive the physical feel of \"selecting\" something: the focus frame is fixed, it's the content that scrolls and gets snapped into it. Each item must **actually go still for a few frames** after stopping — continuous scrolling reads as a loading animation, only a pause reads as \"this item is now selected.\" Distance-based decay (opacity + font size + grayscale all fading together with distance from center) is the only way to keep the viewer's eye locked on the middle row.\n\n## Duration & energy\n\n- Duration: About 3.6s (108f@30fps)\n- Energy: Medium (every snap is a beat, five beats advancing at a steady pace)\n\n## Shot recipe (original Chinese — timing, easing, parameter table, known pitfalls)\n\n## 意图\n把\"选择\"这件事的物理感做出来：焦点框是固定的，是内容在滚动并被吸住。\n每项停下后必须**真的静止几帧**——连续滚动读作 loading 动画，停顿才读作\n\"这一项被选中了\"。距离衰减（透明度 + 字号 + 灰度三通道同时衰减）是让\n观众视线始终锁在中间那一行的唯一手段。\n\n## 动效核心\n- 视口 300×170px（5 行 × rowH 34），焦点药丸固定在第 3 行位置\n  （`top: rowH*2`），白底 + 1px `#E3E3E6` 描边 + `0 2px 8px rgba(0,0,0,.06)`\n- 全程 `t 0.05→0.95` 匀速切成 STEPS=5 步；步内位移\n  `outQuint(min(1, local/(1−HOLD)))`，HOLD=5/14 的尾段完全不动\n  （每步≈0.65s = 走位≈0.42s + 静止≈0.23s）\n- 列表整体 `translateY(rowH*2 − pos*rowH)`——移动的是内容，药丸零位移\n- 距离衰减（`d = |i − pos|`）分三段：opacity `d≤1` 时 1→0.55、\n  `1<d≤2` 时 0.55→0.18；fontSize 17→14px（按 `d/2`）；\n  color `d<0.5` 用 INK、`d<1.5` 用 MID、更远用 `#B9B9BE`\n- 行内图标 opacity `max(0, 1 − d*1.6)`——只有焦点行看得见图标\n- 落定后药丸 `scaleY 1 + sin(min(1, land/0.6)·π)*0.06` 呼吸一下\n  （`land` 只在 HOLD 段内 >0，即吸附完成才呼吸）\n- 视口上下各 26% 的 PAPER→透明渐变遮罩，让进出项自然消隐\n\n## 参数表\n| 参数 | 典型值 | 调节手感 |\n|------|--------|----------|\n| 行高/可见行数 | rowH 34px × 5 行，焦点在第 3 行 | 可见行数必须是奇数，否则焦点行不在视觉正中 |\n| 步数 | STEPS=5，窗口 t 0.05→0.95 匀速 | 5 拍是 3.6s 的舒适上限；7 拍每拍就压到 0.46s 读不清 |\n| HOLD | 5/14 ≈ 0.357（每步尾段静止≈0.23s） | **命门**：HOLD 归零就变成匀速滚动，完全丢掉\"吸附\"语义 |\n| 吸附缓动 | outQuint（作用在 local/(1−HOLD) 上） | outQuint 的急减速就是\"被磁铁吸住\"的手感，outCubic 太软 |\n| opacity 衰减 | 1 → 0.55（d≤1）→ 0.18（d≤2） | 相邻行留 0.55 是让观众感知\"上下还有内容\"；降到 0.3 就孤立了 |\n| fontSize 衰减 | 17→14px，按 min(1, d/2) | 字号差是第二重景深；只做透明度会显得扁 |\n| 图标可见性 | max(0, 1 − d·1.6) | 只有焦点行带图标，是\"当前项\"的额外标记 |\n| 药丸呼吸 | scaleY 1→1.06→1，落定后 0.6·HOLD 内走完 | 0.06 已到上限；再大就读作药丸自己在动，抢了内容的戏 |\n\n## 已知坑\n- 药丸必须是 **`translateY` 零位移的固定层**，`scaleY` 呼吸也不能带\n  位移，否则焦点框漂移，\"内容穿过焦点\"的语义立刻失效\n- 距离衰减写在 `fontSize` 上会触发逐帧重排（行高固定所以布局不跳），\n  行高若改成 auto 就会抖；rowH 必须写死\n- 渐变遮罩的颜色要与背景 PAPER 完全一致，差一档就露出边界带\n- 左外侧 26×22 的方形 AI 徽标用 `margin:-11px 0 0 -186px` 相对画面中心\n  定位；改视口宽度必须同改 186px\n- ITEMS 是 7 条占位功能名（\"Data Cleanup\" 等），落地换真实功能名时\n  字长变化会影响居中观感——单行超过 300px 就要缩字号而不是换行\n\n## 参考实现\ndemos/interaction/picker-carousel-feature-cycle/\n（PickerCarouselFeatureCycle.tsx）\n\n## Reference implementation (Remotion)\n\n- demos/interaction/picker-carousel-feature-cycle/PickerCarouselFeatureCycle.tsx — https://raw.githubusercontent.com/Vincentwei1021/video-shotcraft/main/demos/interaction/picker-carousel-feature-cycle/PickerCarouselFeatureCycle.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/picker-carousel-feature-cycle/PickerCarouselFeatureCycle.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/picker-carousel-feature-cycle/PickerCarouselFeatureCycle.tsx",
      "url": "https://raw.githubusercontent.com/Vincentwei1021/video-shotcraft/main/demos/interaction/picker-carousel-feature-cycle/PickerCarouselFeatureCycle.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": 123,
    "repo": "https://github.com/Vincentwei1021/video-shotcraft",
    "files": [
      "https://raw.githubusercontent.com/Vincentwei1021/video-shotcraft/main/demos/interaction/picker-carousel-feature-cycle/PickerCarouselFeatureCycle.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/picker-carousel-feature-cycle/PickerCarouselFeatureCycle.tsx =====\n// picker-carousel-feature-cycle — Picker Carousel 功能名吸附轮播（motion-lab 定稿转原生 Remotion）\n// 移动端风竖向选择器：焦点药丸不动、内容穿过它，每项停靠 0.45s 且带明显 outQuint\n// 减速吸附 + 4–5 帧静止；按到中心距离分层控制 opacity/字号/灰度，落定时药丸做\n// scaleY 1→1.06→1 极轻呼吸，左外侧固定方形 AI 徽标。\n// 设计坐标 480×270（DesignStage 等比放大），参数表数值以此坐标系标定。\nimport React from 'react';\nimport { DesignStage, E, lerp, seg, useT } from '../../_fixtures/Motion';\n\nexport const PICKER_CAROUSEL_FEATURE_CYCLE_DURATION = 108; // 3600ms @30fps\n\nconst PAPER = '#F3F3F1';\nconst INK = '#111113';\nconst MID = '#8A8A8F';\nconst ROW_H = 34; // 单行高度\nconst ITEMS = [\n  'Data Cleanup',\n  'Direct Message',\n  'Smart Segments',\n  'Batch Actions',\n  'Reward Program',\n  'Automated Flows',\n  'Variant Testing',\n];\nconst ICONS = ['◎', '✉', '◧', '◈', '★', '↺', '⚑'];\nconst STEPS = 5; // 五次吸附推进\nconst HOLD = 5 / 14; // 每步末段静止占比（≈4–5 帧不动）\n\nexport const PickerCarouselFeatureCycle: React.FC = () => {\n  const t = useT();\n\n  // 主时间轴：t∈[0.05,0.95] 均分 5 步；步内前 (1-HOLD) 用 outQuint 吸附，末段静止\n  const g = seg(t, 0.05, 0.95) * STEPS;\n  const step = Math.min(STEPS - 1, Math.floor(g));\n  const local = Math.min(1, g - step);\n  const mv = E.outQuint(Math.min(1, local / (1 - HOLD)));\n  const pos = step + mv;\n\n  // 落定呼吸：吸附完成进入 HOLD 后，药丸 scaleY 1→1.06→1 极轻脉冲\n  const land = Math.max(0, (local - (1 - HOLD)) / HOLD);\n  const breath = land > 0 ? Math.sin(Math.min(1, land / 0.6) * Math.PI) * 0.06 : 0;\n\n  return (\n    <DesignStage bg={PAPER}>\n      {/* 纸面底 —— 与原采集页 paper() 容器一致 */}\n      <div\n        style={{\n          position: 'absolute',\n          inset: 0,\n          overflow: 'hidden',\n          background: PAPER,\n          fontFamily: \"-apple-system,'Helvetica Neue',Helvetica,Arial,sans-serif\",\n        }}\n      >\n        {/* 选择器视口：300×(34×5) 居中，整体在开头 5% 内淡入 */}\n        <div\n          style={{\n            position: 'absolute',\n            left: '50%',\n            top: '50%',\n            width: 300,\n            height: ROW_H * 5,\n            transform: 'translate(-50%,-50%)',\n            overflow: 'hidden',\n            opacity: seg(t, 0, 0.05),\n          }}\n        >\n          {/* 焦点药丸：位置不动，仅落定时 scaleY 呼吸。\n              原采集页是 content-box，带 1px 边框需显式声明，否则高度差 2px */}\n          <div\n            style={{\n              position: 'absolute',\n              left: 0,\n              right: 0,\n              top: ROW_H * 2,\n              height: ROW_H,\n              boxSizing: 'content-box',\n              borderRadius: 999,\n              background: '#fff',\n              border: '1px solid #E3E3E6',\n              boxShadow: '0 2px 8px rgba(0,0,0,.06)',\n              transform: `scaleY(${1 + breath})`,\n            }}\n          />\n          {/* 内容列：整列 translateY 穿过焦点药丸 */}\n          <div\n            style={{\n              position: 'absolute',\n              left: 0,\n              right: 0,\n              top: 0,\n              transform: `translateY(${ROW_H * 2 - pos * ROW_H}px)`,\n            }}\n          >\n            {ITEMS.map((txt, i) => {\n              // 按到中心距离分层：透明度两段线性、字号 17→14、颜色三档灰度\n              const d = Math.abs(i - pos);\n              const k2 = Math.min(2, d);\n              const o = k2 <= 1 ? lerp(k2, 1, 0.55) : lerp(k2 - 1, 0.55, 0.18);\n              return (\n                <div\n                  key={i}\n                  style={{\n                    height: ROW_H,\n                    display: 'flex',\n                    alignItems: 'center',\n                    justifyContent: 'center',\n                    gap: 8,\n                    fontWeight: 600,\n                    fontSize: lerp(Math.min(1, d / 2), 17, 14),\n                    lineHeight: 1,\n                    fontFamily: \"-apple-system,'Helvetica Neue',sans-serif\",\n                    color: d < 0.5 ? INK : d < 1.5 ? MID : '#B9B9BE',\n                    opacity: o,\n                  }}\n                >\n                  {/* 图标仅在贴近焦点时浮现（d<0.625 内可见） */}\n                  <span style={{ fontSize: 14, opacity: Math.max(0, 1 - d * 1.6) }}>\n                    {ICONS[i]}\n                  </span>\n                  <span>{txt}</span>\n                </div>\n              );\n            })}\n          </div>\n          {/* 上下渐隐罩：纸色→透明→纸色，盖在列表之上 */}\n          <div\n            style={{\n              position: 'absolute',\n              inset: 0,\n              pointerEvents: 'none',\n              background: `linear-gradient(180deg,${PAPER} 0%,rgba(243,243,241,0) 26%,rgba(243,243,241,0) 74%,${PAPER} 100%)`,\n            }}\n          />\n        </div>\n        {/* 左外侧固定 AI 徽标：黑底白字方形圆角，开头略滞后淡入 */}\n        <div\n          style={{\n            position: 'absolute',\n            left: '50%',\n            top: '50%',\n            margin: '-11px 0 0 -186px',\n            width: 26,\n            height: 22,\n            borderRadius: 6,\n            background: INK,\n            color: '#fff',\n            display: 'flex',\n            alignItems: 'center',\n            justifyContent: 'center',\n            fontWeight: 700,\n            fontSize: 10,\n            lineHeight: 1,\n            fontFamily: '-apple-system,sans-serif',\n            letterSpacing: 0.5,\n            opacity: seg(t, 0.02, 0.09),\n          }}\n        >\n          AI\n        </div>\n      </div>\n    </DesignStage>\n  );\n};\n\n\n"
}