{
  "slug": "vertical-word-roll-blur-cycle",
  "title": "Vertical word-roll cycle",
  "tagline": "The sentence's last word cycles on a vertical roller, 3 swaps at 0.55s each, fast then very slow with a slight overshoot; neighboring rows carry vertical blur and grayscale by distance, the centered word snapping from gray to accent color as it lands.",
  "description": "The sentence's last word cycles on a vertical roller, 3 swaps at 0.55s each, fast then very slow with a slight overshoot; neighboring rows carry vertical blur",
  "category": "typography",
  "tags": [
    "shotcraft",
    "remotion",
    "typography"
  ],
  "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/vertical-word-roll-blur-cycle/poster-bfb6d2f5234f.jpg",
    "video": "https://gimgs.net/motion/media/vertical-word-roll-blur-cycle/video-791835a1214b.mp4"
  },
  "variants": [
    {
      "key": "vertical-word-roll-blur-cycle",
      "label": "vertical-word-roll-blur-cycle",
      "description": "The sentence's last word cycles on a vertical roller, 3 swaps at 0.55s each (70% outQuint + 30% outBack, fast-then-very-slow with a slight overshoot), neighboring rows carrying vertical blur and grayscale by distance, the centered word snapping from gray to accent color as it lands",
      "use": "",
      "video": "https://gimgs.net/motion/media/vertical-word-roll-blur-cycle/video-791835a1214b.mp4",
      "poster": "https://gimgs.net/motion/media/vertical-word-roll-blur-cycle/poster-bfb6d2f5234f.jpg"
    }
  ],
  "credits": {
    "author": "video-shotcraft (Vincent Wei)",
    "url": "https://github.com/Vincentwei1021/video-shotcraft"
  },
  "page_url": "https://gimgs.net/motion/vertical-word-roll-blur-cycle.html",
  "json_url": "https://gimgs.net/motion/vertical-word-roll-blur-cycle.json",
  "template_url": "https://gimgs.net/motion/vertical-word-roll-blur-cycle/template.html",
  "play_url": null,
  "markdown_url": "https://gimgs.net/motion/vertical-word-roll-blur-cycle.md",
  "updated_at": "2026-09-24T07:40:33.991Z",
  "prompt": "## When to use\n\nA one-line pitch like \"Built for ___\" followed by an enumerated audience/object; a clean beat for light-background brand films\n\n## Intention\n\nThe best solution for swapping the sentence's last word is a roller, not a cross-fade: a roller has its own built-in **direction** (flipping upward), telling the viewer \"there's another one coming.\" This card's two identity marks are the neighboring rows' **vertical blur** (roller depth of field, simulating a mechanical drum's defocus) and the landing word's **color shift** (gray to accent color, \"this one is the answer\"). The sentence stem \"Built for\" never moves for the entire shot — the eye stays locked on the roller; if the stem wobbled, the sense of enumeration would fall apart.\n\n## Duration & energy\n\n- Duration: ~5.0s (150f@30fps: hold → 3 word swaps → group fades out at the tail)\n- Energy: Medium (a steady three-beat, no peak)\n\n## Shot recipe (original Chinese — timing, easing, parameter table, known pitfalls)\n\n## 意图\n句尾换词的最优解是滚轮而不是淡入淡出：滚轮自带**方向**（往上翻页），\n观众知道\"还有下一个\"。这卡的两个身份记号是相邻行的**垂直 blur**\n（滚轮景深，模拟机械转筒的失焦）和落定时的**染色**（灰→强调色，\n\"这个才是答案\"）。句干 `Built for` 全程纹丝不动——眼睛钉在滚轮上，\n句干一晃列举感就散。\n\n## 动效核心\n- 位置量 `p` 由 3 段窗累加：`STEPS = [0.16, 0.36, 0.56]`，每段窗 0.11\n  （≈16f，约 0.55s），段间留 0.09 静置给观众读词\n- 每段缓动是**混合曲线**：`0.7·E.outQuint(u) + 0.3·E.outBack(u)`——\n  outQuint 给\"前快后极慢\"的机械减速，outBack 那三成给落定的微过冲回落\n- reel 定位 `translateY(ROW - p·ROW)`，ROW=44px，mask 高 `ROW·3`=132px\n  （露三行）——中心行恒定是 mask 第二行\n- 距离驱动的三通道，`d = |i - p|`：\n  - blur：`d<1` 时 `3d`，`d≥1` 时 `3 + 2·min(d-1,1)`（最远 5px）\n  - opacity：`d<1` 时 `1 - 0.65d`，否则 `max(0.1, 0.35 - 0.23(d-1))`\n  - 颜色：`mixHex(ACCENT_DIM, ACCENT, clamp01(1 - 2.4d))` —— **d>0.42 就完全\n    是暗色**，所以只有真正落定的那一刻才是强调色，滚动中途全程灰\n- 收尾 `seg(t, 0.9, 0.985)` 整组（句干 + 滚轮）淡出，不留残影给下一镜\n\n## 参数表\n| 参数 | 典型值 | 调节手感 |\n|------|--------|----------|\n| 词表 | 4 词（Apps→Teams→Data→Everyone），3 次换词 | 末词是结论（最宽那个词），前三个是例子；>5 词观众开始数，<3 词读不出\"列举\" |\n| 拍点 | `[0.16, 0.36, 0.56]`，窗 0.11（≈16f） | 段间 0.09（≈13f）静置是给读词的，读不完的拍子等于没换；窗 >0.18 新旧同屏太久 |\n| 缓动配方 | `0.7·outQuint + 0.3·outBack` | outQuint 是机械减速的来源；outBack 那三成给落定\"咔\"一下，纯 outQuint 读作电子屏滑动 |\n| 行高/视窗 | ROW 44px、mask `44×3`px、词宽 190px | 露三行才有滚筒感（露一行读作淡入淡出）；宽度写死 190 是为不挤句干，词更长要同步加 |\n| 相邻行 blur | 邻行 3px，最远 5px，**仅垂直向感知** | blur 是\"滚筒景深\"的身份证，删了退化成词条平移；>8px 邻行糊成灰条看不出是字 |\n| 落定染色 | `1 - 2.4d` 映射 ACCENT_DIM→ACCENT | 系数 2.4 让染色只发生在落定瞬间；<1.5 滚动中途就上色，答案感散掉 |\n| 字号字重 | 句干与滚轮同为 30px/800、`letter-spacing:-.5px` | 必须完全一致——滚轮词比句干大就读作两个元素，不是一句话 |\n| 收尾淡出 | `seg(t, 0.9, 0.985)` 整组 | 末词落定后约 40f 静置再淡出；不 hold 等于白列举 |\n\n## 已知坑\n- demo 在灰阶/占位素材上调校通过——参数是调校起点非实战定稿，\n  首次实战须以真实素材回验\n- **mask 宽度写死 190px**：换词表时最长词（demo `Everyone`）必须能装进去，\n  否则被裁字；反之词都很短则右侧留大片空白，句子重心偏左——换词表必须\n  重量一次宽度\n- 词长差别不宜过大：mask 宽度是固定的（本卡不做宽度自适应，那是\n  pill-chip-slot-cycle-handled 的活），词长差 >2 倍时短词在框里显得孤立\n- 句干必须钉死：demo 用 `flex + gap:14px` 是因为 mask 宽度恒定不会推挤\n  句干——一旦改成宽度自适应的滚轮，就必须换成绝对定位锚左端，\n  否则句干随词宽左右甩\n- `p` 是三段窗**累加**出来的，所以三段窗不能重叠：重叠会让 p 在一帧内\n  跨越一整行，滚轮\"跳格\"\n- 与 pill-chip-slot-cycle-handled 分工：那卡是深色胶囊 + 宽度自适应挤开\n  （词是徽章），本卡是裸词滚轮（词是句子成分）——同片只留一个\n- 声音：每次落定是一个节奏点，配轻机械/开关音；三连发要交替双样本\n  防机枪感，配法纪律见 sound-design.md\n\n## 参考实现\ndemos/typography/vertical-word-roll-blur-cycle/\n（VerticalWordRollBlurCycle.tsx）\n\n## Reference implementation (Remotion)\n\n- demos/typography/vertical-word-roll-blur-cycle/VerticalWordRollBlurCycle.tsx — https://raw.githubusercontent.com/Vincentwei1021/video-shotcraft/main/demos/typography/vertical-word-roll-blur-cycle/VerticalWordRollBlurCycle.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/vertical-word-roll-blur-cycle/VerticalWordRollBlurCycle.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/vertical-word-roll-blur-cycle/VerticalWordRollBlurCycle.tsx",
      "url": "https://raw.githubusercontent.com/Vincentwei1021/video-shotcraft/main/demos/typography/vertical-word-roll-blur-cycle/VerticalWordRollBlurCycle.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/typography/vertical-word-roll-blur-cycle/VerticalWordRollBlurCycle.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/vertical-word-roll-blur-cycle/VerticalWordRollBlurCycle.tsx =====\n// vertical-word-roll-blur-cycle — Word Roll 竖向词条滚轮（motion-lab 定稿转原生 Remotion）\n// 标语后半词用竖向滚轮循环 Apps→Teams→Data→Everyone：中心词清晰上色，相邻行浅灰带\n// 垂直 blur（滚轮景深），每步 outQuint 前快后慢带轻微过冲，落定瞬间中心词从灰染成强调色。\n// 设计坐标 480×270（DesignStage 等比放大），参数表数值以此坐标系标定。\nimport React from 'react';\nimport { DesignStage, E, seg, useT } from '../../_fixtures/Motion';\n\nexport const VERTICAL_WORD_ROLL_BLUR_CYCLE_DURATION = 150; // 5000ms @30fps\n\nconst ROW = 44;\nconst WORDS = ['Apps', 'Teams', 'Data', 'Everyone'];\n// 3 次换词，每次 0.11（≈0.55s）\nconst STEPS = [0.16, 0.36, 0.56];\n\nconst ACCENT = '#4B4BF5';\nconst ACCENT_DIM = '#B9B9BE';\nconst clamp01 = (v: number) => Math.max(0, Math.min(1, v));\n// 十六进制颜色按 k 混合（与原 effect.js 的 mixHex 等价）\nconst mixHex = (a: string, b: string, k: number) => {\n  k = clamp01(k);\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  const c = pa.map((v, i) => Math.round(v + (pb[i] - v) * k));\n  return `rgb(${c[0]},${c[1]},${c[2]})`;\n};\n\nexport const VerticalWordRollBlurCycle: React.FC = () => {\n  const t = useT();\n\n  // 滚轮进度 p：每步 0.7·outQuint + 0.3·outBack（前快后极慢 + 轻微过冲回落）\n  let p = 0;\n  for (const s of STEPS) {\n    const u = seg(t, s, s + 0.11);\n    p += 0.7 * E.outQuint(u) + 0.3 * E.outBack(u);\n  }\n\n  return (\n    <DesignStage bg=\"#F7F7FA\">\n      <div\n        style={{\n          position: 'absolute',\n          inset: 0,\n          display: 'flex',\n          alignItems: 'center',\n          justifyContent: 'center',\n          fontFamily: '-apple-system,system-ui,sans-serif',\n        }}\n      >\n        <div\n          style={{\n            display: 'flex',\n            alignItems: 'center',\n            gap: 14,\n            // 结束整组淡出\n            opacity: 1 - seg(t, 0.9, 0.985) * 0.999,\n          }}\n        >\n          <div\n            style={{\n              fontSize: 30,\n              fontWeight: 800,\n              color: '#0B0B0C',\n              letterSpacing: -0.5,\n            }}\n          >\n            Built for\n          </div>\n          {/* 三行高的遮罩窗口，滚轮列在其中滑动，中心行 = 第二行 */}\n          <div style={{ position: 'relative', height: ROW * 3, width: 190, overflow: 'hidden' }}>\n            <div\n              style={{\n                position: 'absolute',\n                left: 0,\n                top: 0,\n                transform: `translateY(${ROW - p * ROW}px)`,\n              }}\n            >\n              {WORDS.map((w, i) => {\n                const d = Math.abs(i - p);\n                // 相邻行方向 blur：距中心 1 行内线性 0→3px，更远 3→5px\n                const blur = d < 1 ? 3 * d : 3 + 2 * Math.min(d - 1, 1);\n                const op = d < 1 ? 1 - 0.65 * d : Math.max(0.1, 0.35 - 0.23 * (d - 1));\n                return (\n                  <div\n                    key={w}\n                    style={{\n                      height: ROW,\n                      display: 'flex',\n                      alignItems: 'center',\n                      fontSize: 30,\n                      fontWeight: 800,\n                      letterSpacing: -0.5,\n                      filter: `blur(${blur.toFixed(2)}px)`,\n                      opacity: op,\n                      // 落定染色：中心词灰→强调色（d 越小越彩）\n                      color: mixHex(ACCENT_DIM, ACCENT, clamp01(1 - d * 2.4)),\n                    }}\n                  >\n                    {w}\n                  </div>\n                );\n              })}\n            </div>\n          </div>\n        </div>\n      </div>\n    </DesignStage>\n  );\n};\n\n\n"
}