{
  "slug": "chip-grid-single-select-blackout",
  "title": "Chip Grid Blackout Select",
  "tagline": "Five option chips fade in one by one in a centered 3+2 layout; the selected one flashes a gray press frame, then within a few frames turns solid black with white text and a subtle 1→1.04→1 bounce, while other chips fade to 18% without moving; remaining chips then clear, and the black chip rises and narrows as a calculation row appears below.",
  "description": "Five option chips fade in one by one in a centered 3+2 layout; the selected one flashes a gray press frame, then within a few frames turns solid black with wh",
  "category": "interaction",
  "tags": [
    "shotcraft",
    "remotion",
    "interaction"
  ],
  "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/chip-grid-single-select-blackout/poster-71beb1a07a15.jpg",
    "video": "https://gimgs.net/motion/media/chip-grid-single-select-blackout/video-83bd5681aef9.mp4"
  },
  "variants": [
    {
      "key": "chip-grid-single-select-blackout",
      "label": "chip-grid-single-select-blackout",
      "description": "Five option chips fade in one by one in a centered 3+2 layout; the selected one flashes a gray press frame, then within a few frames turns solid black with white text and a subtle 1→1.04→1 bounce, while other chips fade to 18% without moving; remaining chips then clear, and the black chip rises and narrows as a calculation row appears below.",
      "use": "",
      "video": "https://gimgs.net/motion/media/chip-grid-single-select-blackout/video-83bd5681aef9.mp4",
      "poster": "https://gimgs.net/motion/media/chip-grid-single-select-blackout/poster-71beb1a07a15.jpg"
    }
  ],
  "credits": {
    "author": "video-shotcraft (Vincent Wei)",
    "url": "https://github.com/Vincentwei1021/video-shotcraft"
  },
  "page_url": "https://gimgs.net/motion/chip-grid-single-select-blackout.html",
  "json_url": "https://gimgs.net/motion/chip-grid-single-select-blackout.json",
  "template_url": "https://gimgs.net/motion/chip-grid-single-select-blackout/template.html",
  "play_url": null,
  "markdown_url": "https://gimgs.net/motion/chip-grid-single-select-blackout.md",
  "updated_at": "2026-09-24T07:39:49.332Z",
  "prompt": "## When to use\n\nInteractive demos of single-select/plan/tier picking; the \"what happens after you pick it\" causal shot; pricing/parameter-checkout flows.\n\n## Intention\n\nOne gray flash frame plus a three-frame reverse-to-black split apart real UI's `:active → :selected` two-stage state into separate beats. Viewers see a \"finger press\" and then a \"system confirms,\" rather than one gradient. Other chips drop to 18% opacity but their positions never move — if they reflowed, viewers would think the page reloaded, and the semantics of \"single select\" would be lost.\n\n## Duration & energy\n\n- Duration: About 5.0s (150f@30fps).\n- Energy: Medium-low (the only burst is that one gray flash frame; the rest is settling).\n\n## Shot recipe (original Chinese — timing, easing, parameter table, known pitfalls)\n\n## 意图\n一帧灰闪 + 三帧反黑，是把真实 UI 的 `:active → :selected` 两级状态\n拆开演。观众看到的是\"手指按下\"再\"系统确认\"，而不是一次渐变。其余\nchip 降到 18% 而**位置绝不移动**：一旦重排，观众会以为页面刷新了，\n\"单选\"的语义就丢了。\n\n## 动效核心\n- 5 个 chip 分 3+2 两行居中（top 96/136，gap 10，高 30、圆角 15），\n  按 `d = 0.05 + i*0.028` 逐个 outQuad 淡入（各 0.04）\n- **1 帧按压灰闪**：`seg(FS, FS+0.006) * (1 − seg(FS+0.006, FS+0.014))`，\n  `rgba(120,120,120,.5)` 覆盖层（FS=0.44，0.006×5000ms ≈ 1 帧）\n- **反黑**：`seg(FS+0.008 → FS+0.04, linear)`（≈5 帧）驱动\n  背景 `#fff→INK`、描边 `LINE→INK`、文字 `TXT→#fff`\n- **按压回弹**：`sc = 1 + sin(pr·π)*0.04`，`pr` 走 FS+0.008→FS+0.075\n  ——正弦保证起止都精确回到 1，不需要额外关键帧\n- **余项降级**：`fade = seg(FS+0.008 → FS+0.075, outQuad)` 把 opacity\n  压到 `lerp(fade, 1, 0.18)` = 18%，`transform` 恒为 `none`\n- **收束**：`lift = seg(FS+0.30 → FS+0.42, inOutCubic)`（选中后≈1.5s）\n  余项 opacity 归零，黑 chip `translate(cx·lift, −46·lift)` + `scale→0.82`；\n  `cx = 220 − (offsetLeft + width/2)` 只测一次，让它上移时同时回到中线\n- 算式行 `FS+0.36→FS+0.42` 浮现，随后逐词加深到 t≈0.98\n\n## 参数表\n| 参数 | 典型值 | 调节手感 |\n|------|--------|----------|\n| 入场 | 间隔 0.028、各 0.04、outQuad | 5 个 chip 在前 0.2 铺完，把时间留给选中段 |\n| 选中时刻 | FS = 0.44（约全片中点） | 前半读题、后半看结果；FS 早于 0.3 观众还没读完选项 |\n| 灰闪 | 0.006 亮 + 0.008 落，rgba(120,120,120,.5) | **就一帧**：给 2 帧以上就读作 hover 高亮，不是按下 |\n| 反黑 | FS+0.008→FS+0.04 linear（≈5f） | linear 是故意的：加 ease 会让\"变黑\"这件事有过程感，太软 |\n| 按压回弹 | 1 + sin(pr·π)·0.04，跨 0.067 | 0.04 是极轻；到 0.1 就成了弹跳按钮，抢戏 |\n| 余项降级 | 降到 18%，位置 transform:none | **位置锁死是命门**：降到 18% 仍在原位，才读作\"没被选中\"而非\"消失了\" |\n| 收束时刻 | FS+0.30（选中后≈1.5s）起，0.12 inOutCubic | 1.5s 是让观众看清选中态的最短驻留；缩到 0.5s 就赶 |\n| 上移收窄 | translateY −46px、scale→0.82，横向补 cx 回中线 | 不补 cx 的话上移后会偏在左边，读作\"被拖走\" |\n\n## 已知坑\n- `cx` 用 `measured` 标志只测一次（首帧 `offsetWidth` 可能为 0 所以要\n  判空）——每帧重测会在 chip 自身缩放后越算越偏\n- 其余 chip 的 `transform` 必须显式写 `'none'`，靠 flex 自动重排会让\n  它们在余项归零时横向滑动\n- 选中项要在同一个 flex 行里做 translate，不能改 `position`，\n  否则触发行内重排、其余 chip 位置跟着变\n- 占位内容：5 条 \"Option … plan\" 选项名 + 算式行\n  \"18% off · 42.00 → 34.44\"，落地全部替换；选项名过长会撑破单行\n  （第三条已是长名压力测试）\n- 与 `chip-lift-to-user-pill` 分工：那张选中后**横向生长成新对象**，\n  本卡选中后**上移收窄让位给结果**；同一片子里两者不要连用\n\n## 参考实现\ndemos/interaction/chip-grid-single-select-blackout/\n（ChipGridSingleSelectBlackout.tsx）\n\n## Reference implementation (Remotion)\n\n- demos/interaction/chip-grid-single-select-blackout/ChipGridSingleSelectBlackout.tsx — https://raw.githubusercontent.com/Vincentwei1021/video-shotcraft/main/demos/interaction/chip-grid-single-select-blackout/ChipGridSingleSelectBlackout.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/chip-grid-single-select-blackout/ChipGridSingleSelectBlackout.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/chip-grid-single-select-blackout/ChipGridSingleSelectBlackout.tsx",
      "url": "https://raw.githubusercontent.com/Vincentwei1021/video-shotcraft/main/demos/interaction/chip-grid-single-select-blackout/ChipGridSingleSelectBlackout.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/interaction/chip-grid-single-select-blackout/ChipGridSingleSelectBlackout.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/chip-grid-single-select-blackout/ChipGridSingleSelectBlackout.tsx =====\n// chip-grid-single-select-blackout — Single Select 1 帧灰闪单选反黑（motion-lab 定稿转原生 Remotion）\n// 五个选项 chip 以 3+2 居中排布逐个淡入；选中帧先插一帧灰色按压块，紧接 3 帧内底色变纯黑、\n// 文字变白并做 1→1.04→1 的极轻回弹，同窗口其余 chip 淡到 18% 但位置锁死；1s 后余项归零，\n// 黑 chip 上移缩小，下方浮现算式行。\n// 设计坐标 480×270（DesignStage 等比放大），440×240 定尺画布居中排版。\nimport React, { useLayoutEffect, useRef, useState } from 'react';\nimport { DesignStage, E, lerp, seg, useT } from '../../_fixtures/Motion';\n\nexport const CHIP_GRID_SINGLE_SELECT_BLACKOUT_DURATION = 150; // 5000ms @30fps\n\n// ---- 本卡共享量（浅灰瑞士极简系配色 / 字体；BG/DIM/clamp01/h2r 取自 motion-lab/fx/b09.js 同批定义） ----\nconst SANS = '-apple-system,BlinkMacSystemFont,\"Segoe UI\",Helvetica,Arial,sans-serif';\nconst BG = '#F1F1F3'; // 页面浅灰\nconst INK = '#0B0B0C'; // 纯黑\nconst TXT = '#111111'; // 正文黑\nconst DIM = '#C9C9CE'; // 浅灰占位字\nconst LINE = '#E6E6EA'; // 描边\n\nconst clamp01 = (v: number) => (v < 0 ? 0 : v > 1 ? 1 : v);\nconst h2r = (h: string) => [parseInt(h.slice(1, 3), 16), parseInt(h.slice(3, 5), 16), parseInt(h.slice(5, 7), 16)];\n// 颜色插值：mix(p,'#C9C9CE','#111')\nconst mix = (p: number, a: string, b: string) => {\n  const A = h2r(a), B = h2r(b), q = clamp01(p);\n  return `rgb(${Math.round(A[0] + (B[0] - A[0]) * q)},${Math.round(A[1] + (B[1] - A[1]) * q)},${Math.round(A[2] + (B[2] - A[2]) * q)})`;\n};\n\n// ---- 参数表（与 effect.js 完全一致） ----\nconst NAMES = ['Option one plan', 'Option two plan', 'Option three long name', 'Option four', 'Option five variant'];\nconst TI = 0;   // 选中项下标\nconst FS = 0.44; // 灰闪起点\n\n// 算式行（逐词加深语法）。注意原文含双空格：split(' ') 产生的空串\"词\"\n// 也各占一个 4.5px margin 的空 span，是间距配方的一部分，必须保留。\nconst CAP_TEXT = '18% off  ·  42.00  →  34.44';\nconst CAP_WORDS = CAP_TEXT.split(' ');\nconst CAP_N = CAP_WORDS.length;\nconst CAP_ST = 0.78 / CAP_N;\nconst CAP_WIN = CAP_ST * 1.5;\n\nexport const ChipGridSingleSelectBlackout: React.FC = () => {\n  const t = useT();\n\n  // 选中 chip 上移时同步回到水平中线：cx = 220 - 选中 chip 在 440 画布内的中心 x。\n  // chip 宽度由文本 + padding 决定（flex 布局），挂载后实测一次；149 为兜底估算\n  // （\"Option one plan\" 600 12px ≈ 90px 文本 + 30 padding + 2 border，行首起点 ≈ 11）。\n  const targetRef = useRef<HTMLDivElement>(null);\n  const [cx, setCx] = useState(149);\n  useLayoutEffect(() => {\n    const el = targetRef.current;\n    if (el && el.offsetWidth) setCx(220 - (el.offsetLeft + el.offsetWidth / 2));\n  }, []);\n\n  const capShow = seg(t, FS + 0.36, FS + 0.42);\n  const capInn = seg(t, FS + 0.37, 0.98);\n\n  return (\n    <DesignStage bg={BG}>\n      {/* 页面 + 440×240 定尺画布（居中） */}\n      <div\n        style={{\n          position: 'absolute',\n          inset: 0,\n          background: BG,\n          overflow: 'hidden',\n          fontFamily: SANS,\n          WebkitFontSmoothing: 'antialiased',\n        }}\n      >\n        <div style={{ position: 'absolute', left: '50%', top: '50%', width: 440, height: 240, margin: '-120px 0 0 -220px' }}>\n          {/* 标题 */}\n          <div\n            style={{\n              position: 'absolute',\n              left: 0,\n              right: 0,\n              top: 26,\n              textAlign: 'center',\n              font: `600 11px/1 ${SANS}`,\n              letterSpacing: 2.4,\n              color: '#9A9AA2',\n              opacity: seg(t, 0.02, 0.1, E.outQuad),\n            }}\n          >\n            OPTION GROUP\n          </div>\n\n          {/* 两行 chip：3+2 居中排布（flex 布局，位置全程锁死不重排） */}\n          {[0, 1].map((row) => (\n            <div\n              key={row}\n              style={{\n                position: 'absolute',\n                left: 0,\n                right: 0,\n                top: row === 0 ? 96 : 136,\n                display: 'flex',\n                justifyContent: 'center',\n                gap: 10,\n              }}\n            >\n              {NAMES.map((n, i) => {\n                if ((i < 3 ? 0 : 1) !== row) return null;\n                const d = 0.05 + i * 0.028;\n                const inP = seg(t, d, d + 0.04, E.outQuad);\n                let bg = '#fff';\n                let borderColor = LINE;\n                let labelColor: string = TXT;\n                let flashO = '0';\n                let opacity: string | number;\n                let transform: string;\n                if (i === TI) {\n                  // 灰闪 1 帧\n                  flashO = (seg(t, FS, FS + 0.006) * (1 - seg(t, FS + 0.006, FS + 0.014))).toFixed(3);\n                  const bk = seg(t, FS + 0.008, FS + 0.04, E.linear);\n                  bg = mix(bk, '#ffffff', INK);\n                  borderColor = mix(bk, LINE, INK);\n                  labelColor = mix(bk, TXT, '#ffffff');\n                  // 按压回弹 1→1.04→1\n                  const pr = seg(t, FS + 0.008, FS + 0.075, E.linear);\n                  const sc = 1 + Math.sin(pr * Math.PI) * 0.04 * (pr > 0 ? 1 : 0);\n                  const lift = seg(t, FS + 0.30, FS + 0.42, E.inOutCubic);\n                  opacity = inP;\n                  transform =\n                    `translate(${(cx * lift).toFixed(2)}px,${(-46 * lift).toFixed(2)}px) ` +\n                    `scale(${(sc * lerp(lift, 1, 0.82)).toFixed(4)})`;\n                } else {\n                  const fade = seg(t, FS + 0.008, FS + 0.075, E.outQuad);\n                  const gone = seg(t, FS + 0.30, FS + 0.35, E.outQuad);\n                  opacity = (inP * lerp(fade, 1, 0.18) * (1 - gone)).toFixed(3);\n                  transform = 'none';\n                }\n                return (\n                  <div\n                    key={i}\n                    ref={i === TI ? targetRef : undefined}\n                    style={{\n                      position: 'relative',\n                      height: 30,\n                      borderRadius: 15,\n                      background: bg,\n                      border: `1px solid ${borderColor}`,\n                      boxSizing: 'border-box',\n                      display: 'flex',\n                      alignItems: 'center',\n                      padding: '0 15px',\n                      boxShadow: '0 1px 4px rgba(0,0,0,.04)',\n                      overflow: 'hidden',\n                      opacity,\n                      transform,\n                    }}\n                  >\n                    <div style={{ font: `600 12px/1 ${SANS}`, color: labelColor, letterSpacing: '-.01em', whiteSpace: 'nowrap' }}>\n                      {n}\n                    </div>\n                    {/* 按压灰闪块（盖在 label 上） */}\n                    <div style={{ position: 'absolute', inset: 0, background: 'rgba(120,120,120,.5)', opacity: flashO }} />\n                  </div>\n                );\n              })}\n            </div>\n          ))}\n\n          {/* 算式行：逐词加深（浅灰占位 → 黑），整行透明度随 show 淡入 */}\n          <div\n            style={{\n              position: 'absolute',\n              left: '50%',\n              top: 150,\n              display: 'flex',\n              alignItems: 'baseline',\n              whiteSpace: 'nowrap',\n              transform: 'translateX(-50%)',\n              opacity: capShow,\n            }}\n          >\n            {CAP_WORDS.map((w, i) => {\n              const q = clamp01((capInn - i * CAP_ST) / CAP_WIN);\n              return (\n                <span\n                  key={i}\n                  style={{\n                    font: `600 14px/1.25 ${SANS}`,\n                    color: mix(q, DIM, TXT),\n                    letterSpacing: (-0.03 * (1 - q)).toFixed(4) + 'em',\n                    marginRight: i === CAP_N - 1 ? 0 : 4.5,\n                  }}\n                >\n                  {w}\n                </span>\n              );\n            })}\n          </div>\n        </div>\n      </div>\n    </DesignStage>\n  );\n};\n\n\n"
}