{
  "slug": "scroll-brake-moves",
  "title": "Scroll brake moves",
  "tagline": "A long scroll decelerates to a precise stop on the latest item; a variant adds a reticle lock on the same frame.",
  "description": "A long scroll decelerates to a precise stop on the latest item; a variant adds a reticle lock on the same frame.",
  "category": "data",
  "tags": [
    "shotcraft",
    "remotion",
    "data",
    "rhythm"
  ],
  "tier": "free",
  "license": "Apache-2.0",
  "spec": {
    "width": 1920,
    "height": 1080,
    "fps": 30,
    "duration": 4.72,
    "aspect": "16:9"
  },
  "kind": "remotion",
  "params_schema": {
    "type": "object",
    "properties": {}
  },
  "default_params": {},
  "preview": {
    "poster": "https://gimgs.net/motion/media/scroll-brake-moves/poster-b9b47ba72acd.jpg",
    "video": "https://gimgs.net/motion/media/scroll-brake-moves/video-1ba81111090d.mp4"
  },
  "variants": [
    {
      "key": "changelog-scroll-brake",
      "label": "changelog-scroll-brake",
      "description": "translateY scrolls ~2400px (out-exp exponential deceleration over ~50f), blur is driven by frame-to-frame displacement delta (0–6px, auto-clearing to zero); the stop-point row scales to 1.03 with a lift, shadow, and 3px outline, while everything else fades to opacity 0.38.",
      "use": "The basic version for a changelog section",
      "video": "https://gimgs.net/motion/media/scroll-brake-moves/video-1ba81111090d.mp4",
      "poster": "https://gimgs.net/motion/media/scroll-brake-moves/poster-b9b47ba72acd.jpg"
    },
    {
      "key": "brake-reticle-lock",
      "label": "brake-reticle-lock",
      "description": "The scroll has three stages: sin-in acceleration → cubic-out hard deceleration overshooting +30px → bounce-back settle; blur = v×0.12 capped at 24px; corner marks fly in from ±620/±320 off-screen with Easing.back(2.4) and clamp on, the highlight completes within 6f, and the label pops with back(2.6).",
      "use": "A highlight section where the stop point needs stronger impact",
      "video": "https://gimgs.net/motion/media/scroll-brake-moves/video-a3f96e0937e6.mp4",
      "poster": "https://gimgs.net/motion/media/scroll-brake-moves/poster-brake-reticle-lock-86ee31daf30a.jpg"
    }
  ],
  "credits": {
    "author": "video-shotcraft (Vincent Wei)",
    "url": "https://github.com/Vincentwei1021/video-shotcraft"
  },
  "page_url": "https://gimgs.net/motion/scroll-brake-moves.html",
  "json_url": "https://gimgs.net/motion/scroll-brake-moves.json",
  "template_url": "https://gimgs.net/motion/scroll-brake-moves/template.html",
  "play_url": null,
  "markdown_url": "https://gimgs.net/motion/scroll-brake-moves.md",
  "updated_at": "2026-09-24T07:40:21.063Z",
  "prompt": "## When to use\n\nChangelog/release-history/long-list sections: \"we've always been shipping, but today's is the biggest\"; use B when the stop point needs stronger impact\n\n## Intention\n\nThe Linear changelog-video grammar: a full year of update logs races past as a long scroll at high speed (blurring into a color band), decelerating exponentially to a precise stop on the latest release, which lifts off the page, highlights, and dims everything else — the density says \"we've always been shipping,\" the hard stop says \"today's is the biggest.\" B is a combined variant: on the same frame as the hard stop, four L-shaped corner marks fly in from off-screen and clamp onto the stopped item — the \"thud\" of the stop and the \"clack\" of the lock resonate on the same frame (the critical rule of the combination: the corner marks' launch frame must equal the list's stop frame; any offset degrades it into two separate tricks placed side by side).\n\n## Duration & energy\n\n- Duration: A 4–5s / B 5s\n- Energy: Opens high, settles mid (a speed-contrast type)\n\n## Shot recipe (original Chinese — timing, easing, parameter table, known pitfalls)\n\n## 意图\nLinear changelog 视频语法：一整年的更新日志作为长卷高速掠过\n（糊成色带），指数减速精准急刹停在本次发布那条上，该条抬升离面\n高亮、其余退暗——密度感讲\"一直在发货\"，急停讲\"今天这条最大\"。\nB 是组合变异：急刹帧**同帧**四个 L 角标从画外飞入咬合锁定停点\n条目——急刹的\"哐\"与咬合的\"咔\"叠在同一帧共振（组合命门：\n角标起跳帧=列表首停帧，错开退化成两招并置）。\n\n## 两式选型\n| 式 | 做法 | 适用 |\n|----|------|------|\n| A changelog-scroll-brake | translateY 扫 ~2400px（out exp 指数减速 ~50f），blur 由帧间位移差分驱动（0–6px 自动清零）；停点行 scale 1.03 抬升+阴影+3px 描边，其余 opacity 退 0.38 | changelog 段落基本款 |\n| B brake-reticle-lock | 滚动三段：sin-in 加速→cubic-out 猛减速冲过头 +30px→回弹落定；blur=v×0.12 封顶 24px；角标从 ±620/±320 画外 Easing.back(2.4) 飞入咬合，高亮 6f 内完成、标签 back(2.6) 弹出 | 停点需要更强打击感的高光段 |\n\n## 参数表\n| 参数 | 典型值 | 调节手感 |\n|------|--------|----------|\n| 减速曲线 | A 纯 out-exp；B 加\"冲过头 30px 回弹\" | 匀速滚读作跑马灯；B 的过头回弹是\"哐\"的来源 |\n| blur 驱动 | 帧间位移差分 `|p(f)-p(f-1)|`×系数 | 手排模糊关键帧必然对不上速度；差分法帧确定（模式已沉为 assets/lib/helpers/motion.ts velocityAt） |\n| 行高节奏 | 72/94/116 三档错落 | 等高行高速掠过时色带无节奏 |\n| 停位 | 目标行停画面纵向正中 | 停偏读作事故 |\n| B 共振帧 | 角标挂载起跳帧 = 列表首次停住帧（严格同帧） | 组合命门，错 2f 以上就散 |\n\n## 已知坑\n- demo 在灰阶/占位素材上调校通过——参数是调校起点非实战定稿，\n  首次实战须以真实素材回验\n- 与 timeline-travel 分工：本卡纵向列表急刹停\"最新\"，\n  那卡横向时间轴旅行看\"历史\"——同片双用时先后错开且各 ≤1 次\n- 高速段条目内容不用真实可读文本——反正糊成色带，占位灰块即可；\n  停点条目必须出版级真内容（Q10）\n- 停点后其余行退暗 0.38 是构图动作：别退到 0（上下文消失）\n- 声音：高速段低频 riser 铺垫，急刹一声 impact；B 咬合帧\n  再叠一声\"咔\"（与 impact 同帧不同声部）\n\n## 参考实现\ndemos/data/scroll-brake-moves/\n（BrakeReticleLock.tsx / ChangelogScrollBrake.tsx）\n\n## Reference implementation (Remotion)\n\n- demos/data/scroll-brake-moves/BrakeReticleLock.tsx — https://raw.githubusercontent.com/Vincentwei1021/video-shotcraft/main/demos/data/scroll-brake-moves/BrakeReticleLock.tsx\n- demos/data/scroll-brake-moves/ChangelogScrollBrake.tsx — https://raw.githubusercontent.com/Vincentwei1021/video-shotcraft/main/demos/data/scroll-brake-moves/ChangelogScrollBrake.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/data/scroll-brake-moves/BrakeReticleLock.tsx, demos/data/scroll-brake-moves/ChangelogScrollBrake.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/data/scroll-brake-moves/BrakeReticleLock.tsx",
      "url": "https://raw.githubusercontent.com/Vincentwei1021/video-shotcraft/main/demos/data/scroll-brake-moves/BrakeReticleLock.tsx"
    },
    {
      "path": "demos/data/scroll-brake-moves/ChangelogScrollBrake.tsx",
      "url": "https://raw.githubusercontent.com/Vincentwei1021/video-shotcraft/main/demos/data/scroll-brake-moves/ChangelogScrollBrake.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": 142,
    "repo": "https://github.com/Vincentwei1021/video-shotcraft",
    "files": [
      "https://raw.githubusercontent.com/Vincentwei1021/video-shotcraft/main/demos/data/scroll-brake-moves/BrakeReticleLock.tsx",
      "https://raw.githubusercontent.com/Vincentwei1021/video-shotcraft/main/demos/data/scroll-brake-moves/ChangelogScrollBrake.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/data/scroll-brake-moves/BrakeReticleLock.tsx =====\n// 组合：长卷急刹 × 准星咬合（brake-reticle-lock）\n// changelog 长列表从下往上高速掠过（加速冲刺→9f 猛减速带 30px 超调回弹），\n// 急刹帧（BRAKE=59）同帧四个 L 形角标从画外四个方向飞入，back-out 超调咬合\n// 锁定停点条目四角；条目同步高亮 + 右侧弹出小标签。\n// 组合命门：角标咬合帧与列表急刹帧必须同帧共振（都在 f=59 起跳），错开即退化。\n// 关键帧：0–12 初始静置 → 12–50 加速冲刺（速度段 blur）→ 50–59 猛减速超调 →\n// 59 急刹+角标飞入 → 59–72 咬合回弹/高亮/标签 → 75–150 真静止 75f。\n// 帧确定，无随机源。\nimport React from 'react';\nimport { useCurrentFrame, interpolate, Easing } from 'remotion';\nimport { G } from '../../_fixtures/Fixtures';\n\nconst SCROLL_START = 12;\nconst BRAKE = 59;\nconst DUR = 150;\n\nconst PITCH = 156; // 行高 120 + 间距 36\nconst ROW_H = 120;\nconst TARGET_ROW = 30;\nconst FINAL_SCROLL = TARGET_ROW * PITCH - 480; // 停点条目 top 落在屏幕 y=480\nconst LIST_X = 360;\nconst LIST_W = 1200;\n\n// 滚动位置：12–50 加速冲刺（sin-in，越滚越快）→ 50–59 猛减速冲过头 30px →\n// 59–63 回弹落定。59 帧为急刹帧（首次停住/反向）。\nconst scrollAt = (f: number): number => {\n  if (f <= SCROLL_START) return 0;\n  if (f <= 50) {\n    return interpolate(f, [SCROLL_START, 50], [0, FINAL_SCROLL - 430], {\n      easing: Easing.in(Easing.sin),\n    });\n  }\n  if (f <= BRAKE) {\n    return interpolate(f, [50, BRAKE], [FINAL_SCROLL - 430, FINAL_SCROLL + 30], {\n      easing: Easing.out(Easing.cubic),\n    });\n  }\n  return interpolate(f, [BRAKE, BRAKE + 4], [FINAL_SCROLL + 30, FINAL_SCROLL], {\n    extrapolateRight: 'clamp',\n    easing: Easing.out(Easing.quad),\n  });\n};\n\n// 一行 changelog：图标块 + 标题条 + 尾部日期条，宽度由行号确定（帧确定）\nconst Row: React.FC<{ i: number; highlight: number }> = ({ i, highlight }) => (\n  <div\n    style={{\n      position: 'absolute',\n      top: i * PITCH,\n      left: 0,\n      width: LIST_W,\n      height: ROW_H,\n      background: highlight > 0 ? '#ffffff' : G.card,\n      border: `${highlight > 0 ? 3 : 2}px solid ${highlight > 0 ? G.ink : G.border}`,\n      borderRadius: 14,\n      display: 'flex',\n      alignItems: 'center',\n      gap: 24,\n      padding: '0 28px',\n      boxSizing: 'border-box',\n      boxShadow: highlight > 0 ? `0 10px 34px rgba(0,0,0,${0.2 * highlight})` : 'none',\n    }}\n  >\n    <div style={{ width: 44, height: 44, borderRadius: 10, background: G.mid }} />\n    <div style={{ height: 14, width: `${28 + ((i * 31) % 34)}%`, background: G.bar, borderRadius: 7 }} />\n    <div style={{ height: 10, width: `${12 + ((i * 17) % 18)}%`, background: G.line, borderRadius: 5 }} />\n    <div style={{ marginLeft: 'auto', height: 12, width: 120, background: G.line, borderRadius: 6 }} />\n  </div>\n);\n\n// L 形角标：两条粗边组成的直角\nconst Corner: React.FC<{ flip: [number, number]; style: React.CSSProperties }> = ({ flip, style }) => (\n  <div style={{ position: 'absolute', width: 46, height: 46, transform: `scale(${flip[0]}, ${flip[1]})`, ...style }}>\n    <div style={{ position: 'absolute', left: 0, top: 0, width: 46, height: 8, background: G.ink, borderRadius: 3 }} />\n    <div style={{ position: 'absolute', left: 0, top: 0, width: 8, height: 46, background: G.ink, borderRadius: 3 }} />\n  </div>\n);\n\nexport const BrakeReticleLock: React.FC = () => {\n  const f = useCurrentFrame();\n  const scroll = scrollAt(f);\n\n  // 速度段 blur：由相邻帧位移决定，冲刺段全糊，刹停即清晰\n  const v = Math.abs(scroll - scrollAt(Math.max(0, f - 1)));\n  const blur = Math.min(v * 0.12, 24);\n\n  // 急刹帧起：高亮 0→1（6f），标签 63f 弹出\n  const highlight = interpolate(f, [BRAKE, BRAKE + 6], [0, 1], {\n    extrapolateLeft: 'clamp',\n    extrapolateRight: 'clamp',\n  });\n\n  // 角标：急刹帧同帧从画外四方向飞入，back-out 超调咬合（59–68f）\n  const lockT = interpolate(f, [BRAKE, BRAKE + 9], [0, 1], {\n    extrapolateLeft: 'clamp',\n    extrapolateRight: 'clamp',\n    easing: Easing.out(Easing.back(2.4)),\n  });\n  const fly = 1 - lockT; // 1=画外 0=咬合到位\n\n  // 停点条目屏幕坐标（落定后 top=480）\n  const rowTop = TARGET_ROW * PITCH - scroll;\n  const GAP = 10; // 咬合位：角外扩 10px\n  const rect = { x: LIST_X - GAP, y: rowTop - GAP, w: LIST_W + GAP * 2, h: ROW_H + GAP * 2 };\n\n  const corners: Array<{ x: number; y: number; fromX: number; fromY: number; flip: [number, number] }> = [\n    { x: rect.x - 23, y: rect.y - 23, fromX: -620, fromY: -320, flip: [1, 1] },\n    { x: rect.x + rect.w - 23, y: rect.y - 23, fromX: 620, fromY: -320, flip: [-1, 1] },\n    { x: rect.x - 23, y: rect.y + rect.h - 23, fromX: -620, fromY: 320, flip: [1, -1] },\n    { x: rect.x + rect.w - 23, y: rect.y + rect.h - 23, fromX: 620, fromY: 320, flip: [-1, -1] },\n  ];\n\n  // 小标签：63f 从条目右侧弹出（超调）\n  const tagT = interpolate(f, [BRAKE + 4, BRAKE + 12], [0, 1], {\n    extrapolateLeft: 'clamp',\n    extrapolateRight: 'clamp',\n    easing: Easing.out(Easing.back(2.6)),\n  });\n\n  return (\n    <div style={{ width: 1920, height: 1080, background: G.bg, position: 'relative', overflow: 'hidden' }}>\n      {/* 顶栏（不随滚动） */}\n      <div style={{ position: 'absolute', top: 0, left: 0, right: 0, height: 72, background: G.panel, borderBottom: `2px solid ${G.line}`, display: 'flex', alignItems: 'center', padding: '0 32px', gap: 20, boxSizing: 'border-box', zIndex: 3 }}>\n        <div style={{ height: 18, width: 220, background: G.bar, borderRadius: 9 }} />\n        <div style={{ marginLeft: 'auto', width: 36, height: 36, borderRadius: 18, background: G.mid }} />\n      </div>\n\n      {/* changelog 长列表：整体上掠，速度段 blur */}\n      <div style={{ position: 'absolute', left: LIST_X, top: 0, width: LIST_W, height: 1080, filter: blur > 0.5 ? `blur(${blur}px)` : 'none' }}>\n        <div style={{ position: 'absolute', top: -scroll, left: 0, width: LIST_W, height: 40 * PITCH }}>\n          {Array.from({ length: 38 }).map((_, i) => (\n            <Row key={i} i={i} highlight={i === TARGET_ROW ? highlight : 0} />\n          ))}\n        </div>\n      </div>\n\n      {/* 准星角标：急刹帧同帧挂载（组合命门：与刹停同帧共振） */}\n      {f >= BRAKE &&\n        corners.map((c, i) => (\n          <Corner\n            key={i}\n            flip={c.flip}\n            style={{ left: c.x + c.fromX * fly, top: c.y + c.fromY * fly, opacity: Math.min(1, lockT * 3 + 0.35) }}\n          />\n        ))}\n\n      {/* 旁弹小标签 */}\n      {f >= BRAKE + 4 && (\n        <div\n          style={{\n            position: 'absolute',\n            left: rect.x + rect.w + 28,\n            top: rect.y + rect.h / 2 - 27,\n            transform: `scale(${tagT})`,\n            transformOrigin: 'left center',\n            padding: '12px 26px',\n            borderRadius: 27,\n            background: G.ink,\n            color: '#ffffff',\n            fontFamily: 'Helvetica, Arial, sans-serif',\n            fontWeight: 800,\n            fontSize: 26,\n            letterSpacing: 0.5,\n          }}\n        >\n          v2.41\n        </div>\n      )}\n    </div>\n  );\n};\n\n\n// ===== demos/data/scroll-brake-moves/ChangelogScrollBrake.tsx =====\n// changelog-scroll-brake —— Changelog 长卷急刹\n// ~34 行灰阶条目（行高错落）从下往上高速掠过（easeOutExpo 指数减速），\n// 高速段叠 blur（速度差分驱动，糊成色带），急刹精准停位后目标行抬升\n// （scale 1.03 + 阴影加深）+ 高亮描边，其余行退暗。f=84 后全静止（56f）。\nimport React from 'react';\nimport { useCurrentFrame, interpolate, Easing } from 'remotion';\nimport { G } from '../../_fixtures/Fixtures';\n\nconst CL = { extrapolateLeft: 'clamp', extrapolateRight: 'clamp' } as const;\n\n// 时间轴\nconst SCROLL0 = 14; // 前 14f 初始静置（停在长卷顶部）\nconst SCROLL1 = 64; // 急刹停位帧\nconst LIFT0 = 68; // 目标行抬升开始\nconst LIFT1 = 82;\n\nconst COL_W = 1000;\nconst COL_X = (1920 - COL_W) / 2;\nconst GAP = 20;\nconst N = 34;\nconst TARGET = 28;\n\n// 行高错落（帧确定性：全由 i 决定）\nconst rowH = (i: number) => 72 + ((i * 29) % 3) * 22; // 72 / 94 / 116\n\n// 预计算每行 y\nconst rowY: number[] = [];\n{\n  let y = 0;\n  for (let i = 0; i < N; i++) {\n    rowY.push(y);\n    y += rowH(i) + GAP;\n  }\n}\nconst TARGET_CY = rowY[TARGET] + rowH(TARGET) / 2;\nconst END_T = 540 - TARGET_CY; // 目标行停在画面正中\nconst START_T = 80; // 起始：长卷顶部略下\n\nconst scrollAt = (f: number): number =>\n  interpolate(f, [SCROLL0, SCROLL1], [START_T, END_T], {\n    easing: Easing.out(Easing.exp),\n    ...CL,\n  });\n\nconst Row: React.FC<{ i: number; frame: number }> = ({ i, frame }) => {\n  const isTarget = i === TARGET;\n  const h = rowH(i);\n  const titleW = 30 + ((i * 37) % 45);\n\n  // 目标行抬升 + 高亮；其余行退暗\n  const t = interpolate(frame, [LIFT0, LIFT1], [0, 1], {\n    easing: Easing.out(Easing.cubic),\n    ...CL,\n  });\n  const lift = isTarget ? t : 0;\n  const dim = isTarget ? 0 : t;\n\n  return (\n    <div\n      style={{\n        position: 'absolute',\n        left: 0,\n        top: rowY[i],\n        width: COL_W,\n        height: h,\n        background: G.card,\n        border: isTarget && lift > 0 ? `3px solid rgba(47,47,47,${lift})` : `2px solid ${G.border}`,\n        borderRadius: 14,\n        display: 'flex',\n        alignItems: 'center',\n        gap: 24,\n        padding: '0 30px',\n        boxSizing: 'border-box',\n        transform: `scale(${1 + 0.03 * lift})`,\n        boxShadow:\n          lift > 0\n            ? `0 ${6 + 22 * lift}px ${16 + 44 * lift}px rgba(0,0,0,${0.08 + 0.28 * lift})`\n            : '0 2px 8px rgba(0,0,0,0.06)',\n        opacity: 1 - 0.62 * dim,\n        zIndex: isTarget ? 2 : 1,\n      }}\n    >\n      <div style={{ width: 88, height: 26, borderRadius: 13, background: isTarget ? '#4a4a48' : G.mid, flexShrink: 0 }} />\n      <div style={{ height: 16, width: `${titleW}%`, background: G.bar, borderRadius: 8 }} />\n      <div style={{ marginLeft: 'auto', height: 12, width: 110, background: G.line, borderRadius: 6, flexShrink: 0 }} />\n    </div>\n  );\n};\n\nexport const ChangelogScrollBrake: React.FC = () => {\n  const frame = useCurrentFrame();\n  const T = scrollAt(frame);\n\n  // 速度差分驱动模糊：v 达 60px/f 即满 6px blur\n  const v = Math.abs(scrollAt(frame) - scrollAt(frame - 1));\n  const blur = Math.min(v / 60, 1) * 6;\n\n  return (\n    <div style={{ width: 1920, height: 1080, background: G.bg, position: 'relative', overflow: 'hidden' }}>\n      <div\n        style={{\n          position: 'absolute',\n          left: COL_X,\n          top: 0,\n          width: COL_W,\n          height: 1080,\n          transform: `translateY(${T}px)`,\n          filter: blur > 0.15 ? `blur(${blur}px)` : undefined,\n        }}\n      >\n        {Array.from({ length: N }).map((_, i) => (\n          <Row key={i} i={i} frame={frame} />\n        ))}\n      </div>\n    </div>\n  );\n};\n\n\n"
}