{
  "slug": "beat-step-list-theme-cycle",
  "title": "Beat Step Theme Cycle",
  "tagline": "A three-channel metronome — a dark-scene adjective list shifts up one line per beat, a fixed center pill \"catches\" the next word and recolors, and the whole background flips color on the same beat.",
  "description": "A three-channel metronome — a dark-scene adjective list shifts up one line per beat, a fixed center pill \"catches\" the next word and recolors, and the whole b",
  "category": "rhythm",
  "tags": [
    "shotcraft",
    "remotion",
    "rhythm",
    "typography"
  ],
  "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/beat-step-list-theme-cycle/poster-55882624b208.jpg",
    "video": "https://gimgs.net/motion/media/beat-step-list-theme-cycle/video-06d75d23c1ca.mp4"
  },
  "variants": [
    {
      "key": "beat-step-list-theme-cycle",
      "label": "beat-step-list-theme-cycle",
      "description": "A three-channel metronome — a dark-scene adjective list shifts up one line per beat, a fixed center pill \"catches\" the next word and recolors, and the whole background flips color on the same beat.",
      "use": "",
      "video": "https://gimgs.net/motion/media/beat-step-list-theme-cycle/video-06d75d23c1ca.mp4",
      "poster": "https://gimgs.net/motion/media/beat-step-list-theme-cycle/poster-55882624b208.jpg"
    }
  ],
  "credits": {
    "author": "video-shotcraft (Vincent Wei)",
    "url": "https://github.com/Vincentwei1021/video-shotcraft"
  },
  "page_url": "https://gimgs.net/motion/beat-step-list-theme-cycle.html",
  "json_url": "https://gimgs.net/motion/beat-step-list-theme-cycle.json",
  "template_url": "https://gimgs.net/motion/beat-step-list-theme-cycle/template.html",
  "play_url": null,
  "markdown_url": "https://gimgs.net/motion/beat-step-list-theme-cycle.md",
  "updated_at": "2026-09-24T07:39:44.337Z",
  "prompt": "## When to use\n\n\"Same product, many moods/multi-theme showcase\" segments (modern/playful/expressive-style adjective bursts); the densest-rhythm segment in the film; syncing to a music beat.\n\n## Intention\n\nSwitching theme color once is a feature demo; switching every 0.6s, three times in a row, with \"selected line + pill color + whole background\" all jumping on the same frame — that's a metronome. It builds the densest rhythm in the film from the cheapest possible variable swap. Two things are critical: first, all three channels must lock to the same beat point — if any one channel lags half a beat, \"jumping together\" degrades into three independent animations; second, the boundary between jump and slide — the jump only occupies the first 6f of the beat, the remaining 12f is fully still; the beat must \"jump,\" not \"slide,\" or a uniformly scrolling list is just a list.\n\n## Duration & energy\n\n- Duration: Lead-in 30f + 18f per beat × beat count + closing hold; 3 beats ≈ 3.5s (demo 110f).\n- Energy: High (three channels jump together every 0.6s — density-driven high energy).\n\n## Shot recipe (original Chinese — timing, easing, parameter table, known pitfalls)\n\n## 意图\n换一次主题色是功能演示；0.6 秒换一次、连换三次、每次\"选中行+胶囊色\n+全场底色\"三件事同帧齐跳——就成了节拍器。用最便宜的变量切换做出\n全片最高的节奏密度。命门有二：一是**三通道必须锁同一拍点**，任何\n一通道慢半拍，\"齐跳\"退化成三个各自为政的动画；二是**跳与滑的分界**\n——跳变只占拍头 6f，其余 12f 完全静置，拍点要\"跳\"不要\"滑\"，\n匀速滚动的列表就只是列表。\n\n## 动效核心\n- 胶囊固定在视口中央行**不动**，列表 translateY 逐拍步进——视觉上\n  读作\"胶囊跳到下一行接住新词\"，实际动的是世界不是胶囊\n- 拍长 18f（0.6s @30fps ≈ 100BPM），跳变窗仅拍头 6f：\n  `snap(t) = 1-(1-x)^3.2`（陡 ease-out），其余帧静置\n- 三通道同源驱动：同一个 tInBeat 同时喂列表位移、胶囊/底色\n  rgb 逐通道 cross-mix、胶囊 squash——一个时钟，不许各挂各的曲线\n- 主题表逐拍换对（demo：灰白/深棕→绿→紫→红/深藏青），胶囊色与\n  底色是配好的深浅对，不是独立随机色\n- 胶囊落位 squash：scale 1.12→0.97→1 随拍头 6f 走完\n- 选中词反白、非选中词 34% 白；上下各 300px 底色渐变羽化收视口\n  ——羽化色必须跟 bgColor 实时同步，写死颜色换场时穿帮\n- 开拍前 30f 静置铺垫——先让观众看清\"这是个列表\"，再开始打拍\n\n## 参数表\n| 参数 | 典型值 | 调节手感 |\n|------|--------|----------|\n| 拍长 | 18f（BEAT_LEN，改它即改 BPM 对音轨） | <12f 观众来不及读词只见闪色；>26f 拍感散成逐个展示 |\n| 跳变窗 | 拍头 6f，ease-out 指数 3.2 | 窗 >10f 或缓曲线，\"跳\"变\"滑\"，节拍器塌成滚动列表 |\n| 拍数 | 3–4 拍 | 5+ 拍观众适应了节奏开始走神；1–2 拍立不起\"节拍器\"人设 |\n| squash | 1.12→0.97→1 | 去掉它胶囊像贴图换色；>1.2 弹跳抢了换色的戏 |\n| 底色跨度 | 相邻主题底色须同明度不同色相（都深色） | 明度跳变（深→浅）会读作闪屏而非换肤 |\n| 铺垫/收尾 | 前 30f 静置；末拍后 hold ≥30f | 没铺垫第一拍读作 bug；末拍即切没有\"停在最终色\"的确认 |\n\n## 已知坑\n- demo 在灰阶/占位素材上调校通过——参数是调校起点非实战定稿，\n  首次实战须以真实素材回验\n- 与 theme-switch-moves 分工：那卡是单次换肤的仪式（扫场/涟漪，换一次\n  讲一次故事），本卡是连续多次换肤当节拍用——讲\"深色模式功能\"用那卡，\n  讲\"多气质/多主题体量\"用本卡\n- 与 cel-flash-stomp 同为高密度拍点段，同片二选一或隔远——都是\n  \"每 0.6s 一记\"的手法，紧邻观众会拍疲劳\n- **声音强依赖**：拍点对齐鼓点（换 BEAT_LEN 对 BPM），无声版三通道\n  齐跳读作界面抽风\n- 词表长度 ≥ 拍数+2——列表底部要留未选中的词垫着，最后一拍就停在\n  列表末行会露出\"下面没了\"\n- 反白判定跟 beat 整数走、颜色跟 tInBeat 小数走（demo 即如此）——\n  两者都跟小数会出现半白半灰的中间态词\n\n## 参考实现\ndemos/rhythm/beat-step-list-theme-cycle/\n（BeatStepListThemeCycle.tsx）\n原片出处：bear-app 22.3–24.6s\n\n## Reference implementation (Remotion)\n\n- demos/rhythm/beat-step-list-theme-cycle/BeatStepListThemeCycle.tsx — https://raw.githubusercontent.com/Vincentwei1021/video-shotcraft/main/demos/rhythm/beat-step-list-theme-cycle/BeatStepListThemeCycle.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/rhythm/beat-step-list-theme-cycle/BeatStepListThemeCycle.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/rhythm/beat-step-list-theme-cycle/BeatStepListThemeCycle.tsx",
      "url": "https://raw.githubusercontent.com/Vincentwei1021/video-shotcraft/main/demos/rhythm/beat-step-list-theme-cycle/BeatStepListThemeCycle.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/rhythm/beat-step-list-theme-cycle/BeatStepListThemeCycle.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/rhythm/beat-step-list-theme-cycle/BeatStepListThemeCycle.tsx =====\n// beat-step-list-theme-cycle（bear 22.3–24.6s）\n// 深色场形容词列表逐拍步进：每拍列表上移一行 + 选中胶囊跳到下一行并换色\n// （绿→紫→红），整场底色同步换（深棕→深紫→深藏青）。\n// 一拍之内\"行、色、场\"三通道同步跳变，~0.6s 一拍。\nimport React from 'react';\nimport { AbsoluteFill, interpolate, useCurrentFrame } from 'remotion';\n\nconst FONT = 'Helvetica, Arial, sans-serif';\nconst ROW_H = 150;\nconst WORDS = ['modern', 'playful', 'expressive', 'seamless', 'intuitive'];\n\n// 拍点：~0.6s 一拍 = 18 帧\nconst BEAT_LEN = 18;\nconst FIRST_BEAT = 30; // 前 1s 静置铺垫\nconst N_BEATS = 3;\n\n// 每拍的（胶囊色 / 场底色）——起始态 + 三拍\nconst THEMES = [\n  { pill: '#d8d8d4', bg: '#241a12', ink: '#2a2018' }, // modern：灰白胶囊 / 深棕场\n  { pill: '#4fae62', bg: '#1e2416', ink: '#173015' }, // playful：绿\n  { pill: '#8e6fd8', bg: '#221a33', ink: '#2a2144' }, // expressive：紫\n  { pill: '#d64d55', bg: '#141c2e', ink: '#1a2440' }, // seamless：红 / 深藏青\n];\n\n// 拍内跳变：5 帧内完成，ease-out + 微过冲\nconst snap = (t: number) => interpolate(t, [0, 1], [0, 1], {\n  easing: (x) => 1 - Math.pow(1 - x, 3.2),\n  extrapolateLeft: 'clamp', extrapolateRight: 'clamp',\n});\n\nexport const BeatStepListThemeCycle: React.FC = () => {\n  const frame = useCurrentFrame();\n\n  // 当前拍序号与拍内进度（跳变只占拍头 6 帧）\n  const raw = (frame - FIRST_BEAT) / BEAT_LEN;\n  const beat = Math.min(N_BEATS, Math.max(0, Math.floor(raw) + 1)); // 已触发的拍数\n  const beatStartFrame = FIRST_BEAT + (beat - 1) * BEAT_LEN;\n  const tInBeat = beat === 0 ? 1 : snap((frame - beatStartFrame) / 6);\n\n  // 连续步进量：整数拍 + 拍头 6 帧内的插值\n  const step = beat === 0 ? 0 : (beat - 1) + tInBeat;\n\n  // 三通道 —— 1) 列表上移一行\n  const listY = -step * ROW_H;\n\n  // 2) 胶囊换色（拍头硬跳，带一点 cross-fade）\n  const themePrev = THEMES[Math.max(0, beat - 1)];\n  const themeNow = THEMES[beat];\n  const mixT = beat === 0 ? 1 : tInBeat;\n  const mix = (a: string, b: string, t: number) => {\n    const pa = [1, 3, 5].map((i) => parseInt(a.slice(i, i + 2), 16));\n    const pb = [1, 3, 5].map((i) => parseInt(b.slice(i, i + 2), 16));\n    return `rgb(${pa.map((v, i) => Math.round(v + (pb[i] - v) * t)).join(',')})`;\n  };\n  const pillColor = mix(themePrev.pill, themeNow.pill, mixT);\n  const bgColor = mix(themePrev.bg, themeNow.bg, mixT);\n\n  // 胶囊每拍落位时 squash 弹一下\n  const pop = beat === 0 ? 1 : interpolate(tInBeat, [0, 0.6, 1], [1.12, 0.97, 1]);\n\n  // 选中行文字反白判定：胶囊固定在视口中央行，选中词 = WORDS[beat]\n  const selectedIdx = beat;\n\n  return (\n    <AbsoluteFill style={{ background: bgColor, fontFamily: FONT, justifyContent: 'center' }}>\n      {/* 中央固定胶囊（列表在其下滚动，视觉上\"胶囊跳到下一行\"） */}\n      <div style={{\n        position: 'absolute', left: '50%', top: 540 - ROW_H / 2 + 10,\n        width: 900, height: ROW_H - 20, transform: `translateX(-50%) scale(${pop})`,\n        background: pillColor, borderRadius: 999,\n        boxShadow: '0 14px 40px rgba(0,0,0,0.35)',\n      }} />\n      {/* 词列表 */}\n      <div style={{\n        position: 'absolute', left: 0, right: 0, top: 540 - ROW_H / 2,\n        transform: `translateY(${listY}px)`,\n      }}>\n        {WORDS.map((w, i) => {\n          const isSel = i === selectedIdx;\n          return (\n            <div key={w} style={{\n              height: ROW_H, display: 'flex', alignItems: 'center', justifyContent: 'center',\n            }}>\n              <span style={{\n                fontSize: 92, fontWeight: 800, letterSpacing: -1.5,\n                color: isSel ? (beat === 0 ? '#2a2018' : '#ffffff') : 'rgba(255,255,255,0.34)',\n                position: 'relative', zIndex: 2,\n              }}>{w}</span>\n            </div>\n          );\n        })}\n      </div>\n      {/* 视口上下羽化 */}\n      <div style={{ position: 'absolute', left: 0, right: 0, top: 0, height: 300, background: `linear-gradient(${bgColor}, transparent)`, zIndex: 3 }} />\n      <div style={{ position: 'absolute', left: 0, right: 0, bottom: 0, height: 300, background: `linear-gradient(transparent, ${bgColor})`, zIndex: 3 }} />\n    </AbsoluteFill>\n  );\n};\n\n\n"
}