{
  "slug": "pill-slot-cycle",
  "title": "Pill slot cycle",
  "tagline": "A fixed sentence stem stays locked while a pill badge at the end slot-machine-rolls a new word roughly every 0.7s.",
  "description": "A fixed sentence stem stays locked while a pill badge at the end slot-machine-rolls a new word roughly every 0.7s.",
  "category": "typography",
  "tags": [
    "shotcraft",
    "remotion",
    "typography"
  ],
  "tier": "free",
  "license": "Apache-2.0",
  "spec": {
    "width": 1920,
    "height": 1080,
    "fps": 30,
    "duration": 5.89,
    "aspect": "16:9"
  },
  "kind": "remotion",
  "params_schema": {
    "type": "object",
    "properties": {}
  },
  "default_params": {},
  "preview": {
    "poster": "https://gimgs.net/motion/media/pill-slot-cycle/poster-c1cc02740e90.jpg",
    "video": "https://gimgs.net/motion/media/pill-slot-cycle/video-472e18ed0f4e.mp4"
  },
  "variants": [
    {
      "key": "pill-slot-cycle",
      "label": "pill-slot-cycle",
      "description": "A word slot cycles mid-sentence — the fixed stem stays locked while a pill badge at the end of the sentence slot-rolls one notch roughly every 0.7s (the old word flies up and fades out with acceleration, the new one slides in from below with blur), cycling through N feature words before settling into the complete sentence.",
      "use": "",
      "video": "https://gimgs.net/motion/media/pill-slot-cycle/video-472e18ed0f4e.mp4",
      "poster": "https://gimgs.net/motion/media/pill-slot-cycle/poster-c1cc02740e90.jpg"
    }
  ],
  "credits": {
    "author": "video-shotcraft (Vincent Wei)",
    "url": "https://github.com/Vincentwei1021/video-shotcraft"
  },
  "page_url": "https://gimgs.net/motion/pill-slot-cycle.html",
  "json_url": "https://gimgs.net/motion/pill-slot-cycle.json",
  "template_url": "https://gimgs.net/motion/pill-slot-cycle/template.html",
  "play_url": null,
  "markdown_url": "https://gimgs.net/motion/pill-slot-cycle.md",
  "updated_at": "2026-09-24T07:40:14.722Z",
  "prompt": "## When to use\n\nThe most elegant solution for \"feature listing\" copy (faster than a bullet list, more semantic than scramble decoding); sections with a one-line value prop plus multiple verb phrases\n\n## Intention\n\nListing six features would normally need six lines, or six beats of one-by-one fade-ins that fill the whole screen. Word-slot cycling packs them into **a single slot in a single sentence**: the stem \"One AI tool to ___\" is the promise, each pill rolling through the slot is a piece of evidence, and when the final pill flies off and \"do it all.\" lands, the evidence is complete and the conclusion is sealed. The structure carries a built-in three-act shape: promise → enumeration → close. The critical rule is that the stem **never moves an inch** — the viewer's eyes stay locked on the slot the whole time, and any wobble in the stem breaks the sense of enumeration.\n\n## Duration & energy\n\n- Duration: Entrance 12f + 21f per beat × word count + close 14f + hold; 6 words ~5.8s (demo 175f)\n- Energy: Medium (a steady metronome, no peak)\n\n## Shot recipe (original Chinese — timing, easing, parameter table, known pitfalls)\n\n## 意图\n罗列六个功能，列表要六行、逐条淡入要六拍还占满屏。词槽轮换把它们\n塞进**一句话的一个槽位**里：句干 \"One AI tool to ___\" 是承诺，槽里\n滚过的每个 pill 是证据，最后 pill 飞走、\"do it all.\" 落位——证据\n列举完毕，结论盖章。结构自带三幕：承诺→列举→收束。命门是句干\n**纹丝不动**：观众的眼睛全程钉在槽位上，句干一晃列举感就散。\n\n## 动效核心\n- 句干绝对定位左端锚死——**不能用 flex 居中**，pill 宽度变化会带着\n  句干重排（首轮踩过的坑，已是判例）\n- 每拍 21f（~0.7s），前 8f 完成换位、后 13f 静置——静置段是给观众\n  读词的，读不完的拍子等于没换\n- 入场：translateY 120→0 + blur 14→4→0，Easing.out(cubic)；\n  离场：translateY 0→-130 + blur 0→10，Easing.in(cubic) 加速飞出\n  ——新来的减速落定、旧的加速逃走，方向感是\"向上翻页\"\n- 槽内用隐形占位 pill（visibility hidden）撑宽度，新旧两枚绝对定位\n  叠放——槽宽随当前词自然变化但不抖\n- 收束拍：末 pill 上飞（7f ease-in），\"do it all.\" 从下 90px 带\n  Easing.back(1.4) 过冲落位——全片唯一一次过冲，留给结论\n\n## 参数表\n| 参数 | 典型值 | 调节手感 |\n|------|--------|----------|\n| 拍长 | 21f/词，5–7 词 | <16f 读不完短语；>28f 节拍器感散掉；8+ 词观众开始数数 |\n| 换位窗 | 拍内前 8f | 拉长到 12f+ 新旧同屏太久，读作两个 pill 打架 |\n| 飞行距离 | 入 +120 / 出 -130px | <80px 读作原地闪换；入出不对称（出略远）让离场更决绝 |\n| 运动模糊 | 入 14→0 / 出 0→10px blur | 去掉 blur 换位读作硬弹；>20px 中间帧糊成色块 |\n| 收束过冲 | Easing.back(1.4)，14f | >2 结论落位像卡通；无过冲结论与列举没有档次差 |\n| 收尾 hold | 完整句静置 ≥45f | 这句话是全段目的，不 hold 等于白列举 |\n\n## 已知坑\n- demo 在灰阶/占位素材上调校通过——参数是调校起点非实战定稿，\n  首次实战须以真实素材回验\n- 与 odometer-digit-roll / split-flap-title 分工：那两卡是字符/数位级\n  机械滚动（无语义），本卡是整词级 pill 轮换且嵌在活句子里——文案是\n  \"句干+动词短语\"结构才用本卡，纯数字/纯标题词别硬套\n- 与 typewriter-moves 分工：打字机是\"一个字一个字造句\"，本卡是\n  \"句子造好了换零件\"——同片可共存但别在同一句文案上连用\n- 与 type-rhythm-sync 分工：那是既有文字随音轨变属性（不换内容），\n  本卡换内容——绑音轨时本卡拍点对鼓点即可，别再叠字重脉冲\n- pill 短语长度要接近（demo 最长 \"Draft an agenda\"）——长短差 >2 倍\n  槽宽跳变太猛，句子重心左右甩\n- 收束句与句干字号字重必须完全一致（demo 都是 96px/800）——\n  \"do it all.\" 若比句干大，读作新标题而非句子补完\n\n## 参考实现\ndemos/typography/pill-slot-cycle/\n（PillSlotCycle.tsx）\n原片出处：notion-ai 4.5–8.5s\n\n## Reference implementation (Remotion)\n\n- demos/typography/pill-slot-cycle/PillSlotCycle.tsx — https://raw.githubusercontent.com/Vincentwei1021/video-shotcraft/main/demos/typography/pill-slot-cycle/PillSlotCycle.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/typography/pill-slot-cycle/PillSlotCycle.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/typography/pill-slot-cycle/PillSlotCycle.tsx",
      "url": "https://raw.githubusercontent.com/Vincentwei1021/video-shotcraft/main/demos/typography/pill-slot-cycle/PillSlotCycle.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": 177,
    "repo": "https://github.com/Vincentwei1021/video-shotcraft",
    "files": [
      "https://raw.githubusercontent.com/Vincentwei1021/video-shotcraft/main/demos/typography/pill-slot-cycle/PillSlotCycle.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/typography/pill-slot-cycle/PillSlotCycle.tsx =====\n// pill-slot-cycle —— 句中词槽轮换：固定句干 + 句尾 pill 老虎机式滚一格\n// 源：notion-ai 4.5–8.5s。旧 pill 上飞淡出、新 pill 从下带运动模糊滑入，\n// 连换 6 次后 pill 消失、句子落成 \"One AI tool to do it all.\" 收束。\nimport React from 'react';\nimport { AbsoluteFill, useCurrentFrame, interpolate, Easing } from 'remotion';\nimport { G } from '../../_fixtures/Fixtures';\n\nconst FONT = 'Helvetica, Arial, sans-serif';\n\nconst PILLS = [\n  { label: 'Ask a question', icon: '?' },\n  { label: 'Find in Drive', icon: '▲' },\n  { label: 'Find in Slack', icon: '#' },\n  { label: 'Summarize', icon: '≡' },\n  { label: 'Improve writing', icon: '✎' },\n  { label: 'Draft an agenda', icon: '☰' },\n];\n\nconst BEAT = 21; // ~0.7s @30fps\nconst INTRO = 12; // 句干入场\nconst CYCLES = PILLS.length;\n\nconst Pill: React.FC<{ label: string; icon: string; style?: React.CSSProperties }> = ({\n  label,\n  icon,\n  style,\n}) => (\n  <div\n    style={{\n      display: 'inline-flex',\n      alignItems: 'center',\n      gap: 16,\n      padding: '14px 36px 14px 24px',\n      borderRadius: 999,\n      background: '#fff',\n      border: `3px solid ${G.border}`,\n      boxShadow: '0 6px 18px rgba(0,0,0,0.10)',\n      fontFamily: FONT,\n      fontWeight: 700,\n      fontSize: 64,\n      color: G.ink,\n      whiteSpace: 'nowrap',\n      ...style,\n    }}\n  >\n    <span\n      style={{\n        width: 58,\n        height: 58,\n        borderRadius: 14,\n        background: G.bg,\n        display: 'inline-flex',\n        alignItems: 'center',\n        justifyContent: 'center',\n        fontSize: 38,\n        color: G.mid,\n        flexShrink: 0,\n      }}\n    >\n      {icon}\n    </span>\n    {label}\n  </div>\n);\n\nexport const PillSlotCycle: React.FC = () => {\n  const frame = useCurrentFrame();\n\n  // 句干入场：ease-out 上浮淡入\n  const stemT = interpolate(frame, [0, INTRO], [0, 1], {\n    extrapolateRight: 'clamp',\n    easing: Easing.out(Easing.cubic),\n  });\n\n  const cycleStart = INTRO;\n  const cycleEnd = cycleStart + CYCLES * BEAT;\n\n  // 当前处于第几个 pill 拍\n  const rel = frame - cycleStart;\n  const idx = Math.min(Math.floor(rel / BEAT), CYCLES - 1);\n  const beatFrame = rel - idx * BEAT;\n\n  const SWAP = 8; // 每拍前 8f 完成换位\n\n  // 收束段：pill 上飞消失，\"do it all.\" 从下滑入落位\n  const isFinale = frame >= cycleEnd;\n  const finT = interpolate(frame, [cycleEnd, cycleEnd + 14], [0, 1], {\n    extrapolateLeft: 'clamp',\n    extrapolateRight: 'clamp',\n    easing: Easing.out(Easing.back(1.4)),\n  });\n\n  // 槽内容渲染\n  let slot: React.ReactNode = null;\n  if (!isFinale && rel >= 0) {\n    const incoming = PILLS[idx];\n    const outgoing = idx > 0 ? PILLS[idx - 1] : null;\n\n    // 新 pill：从下 +120px 带 blur 滑入，ease-out\n    const inT = interpolate(beatFrame, [0, SWAP], [0, 1], {\n      extrapolateRight: 'clamp',\n      easing: Easing.out(Easing.cubic),\n    });\n    const inY = interpolate(inT, [0, 1], [120, 0]);\n    const inBlur = interpolate(inT, [0, 0.7, 1], [14, 4, 0]);\n\n    // 旧 pill：向上 -120px 加速飞出淡掉\n    const outT = interpolate(beatFrame, [0, SWAP - 1], [0, 1], {\n      extrapolateRight: 'clamp',\n      easing: Easing.in(Easing.cubic),\n    });\n    const outY = interpolate(outT, [0, 1], [0, -130]);\n\n    slot = (\n      <div style={{ position: 'relative', display: 'inline-block' }}>\n        {/* 撑起槽宽度的隐形占位（当前 pill） */}\n        <Pill label={incoming.label} icon={incoming.icon} style={{ visibility: 'hidden' }} />\n        {outgoing && outT < 1 && (\n          <div\n            style={{\n              position: 'absolute',\n              left: 0,\n              top: 0,\n              transform: `translateY(${outY}px)`,\n              opacity: 1 - outT,\n              filter: `blur(${outT * 10}px)`,\n            }}\n          >\n            <Pill label={outgoing.label} icon={outgoing.icon} />\n          </div>\n        )}\n        <div\n          style={{\n            position: 'absolute',\n            left: 0,\n            top: 0,\n            transform: `translateY(${inY}px)`,\n            opacity: idx === 0 ? inT : Math.min(1, inT * 1.6),\n            filter: `blur(${inBlur}px)`,\n          }}\n        >\n          <Pill label={incoming.label} icon={incoming.icon} />\n        </div>\n      </div>\n    );\n  } else if (isFinale) {\n    // 最后一个 pill 上飞离场（前 8f），然后 \"do it all.\" 落位\n    const lastOutT = interpolate(frame, [cycleEnd, cycleEnd + 7], [0, 1], {\n      extrapolateLeft: 'clamp',\n      extrapolateRight: 'clamp',\n      easing: Easing.in(Easing.cubic),\n    });\n    slot = (\n      <div style={{ position: 'relative', display: 'inline-block' }}>\n        <span\n          style={{\n            fontFamily: FONT,\n            fontWeight: 800,\n            fontSize: 96,\n            color: G.ink,\n            letterSpacing: -1,\n            display: 'inline-block',\n            opacity: finT,\n            transform: `translateY(${(1 - finT) * 90}px)`,\n            filter: `blur(${(1 - finT) * 8}px)`,\n            whiteSpace: 'nowrap',\n          }}\n        >\n          do it all.\n        </span>\n        {lastOutT < 1 && (\n          <div\n            style={{\n              position: 'absolute',\n              left: 0,\n              top: -8,\n              transform: `translateY(${-130 * lastOutT}px)`,\n              opacity: 1 - lastOutT,\n              filter: `blur(${lastOutT * 10}px)`,\n            }}\n          >\n            <Pill label={PILLS[CYCLES - 1].label} icon={PILLS[CYCLES - 1].icon} />\n          </div>\n        )}\n      </div>\n    );\n  }\n\n  return (\n    <AbsoluteFill style={{ background: G.bg }}>\n      {/* 句干固定不动：整行左端锚死，不随 pill 宽度居中重排 */}\n      <div\n        style={{\n          position: 'absolute',\n          left: 300,\n          top: 540,\n          transform: `translateY(calc(-50% + ${(1 - stemT) * 50}px))`,\n          display: 'flex',\n          alignItems: 'center',\n          gap: 36,\n          opacity: stemT,\n        }}\n      >\n        <span\n          style={{\n            fontFamily: FONT,\n            fontWeight: 800,\n            fontSize: 96,\n            color: G.ink,\n            letterSpacing: -1,\n            whiteSpace: 'nowrap',\n          }}\n        >\n          One AI tool to\n        </span>\n        {slot}\n      </div>\n    </AbsoluteFill>\n  );\n};\n\n\n"
}