{
  "slug": "speed-ramp-freeze",
  "title": "Speed ramp and freeze-annotate",
  "tagline": "Two frame-remap pacing moves — speed-ramp (fast → 0.2x gaze → fast) and freeze-annotate (flowing → freeze with a circled callout → unfreeze).",
  "description": "Two frame-remap pacing moves — speed-ramp (fast → 0.2x gaze → fast) and freeze-annotate (flowing → freeze with a circled callout → unfreeze).",
  "category": "rhythm",
  "tags": [
    "shotcraft",
    "remotion",
    "rhythm"
  ],
  "tier": "free",
  "license": "Apache-2.0",
  "spec": {
    "width": 1920,
    "height": 1080,
    "fps": 30,
    "duration": 4.54,
    "aspect": "16:9"
  },
  "kind": "remotion",
  "params_schema": {
    "type": "object",
    "properties": {}
  },
  "default_params": {},
  "preview": {
    "poster": "https://gimgs.net/motion/media/speed-ramp-freeze/poster-d6dfb5af2ddf.jpg",
    "video": "https://gimgs.net/motion/media/speed-ramp-freeze/video-d961ba1dca0a.mp4"
  },
  "variants": [
    {
      "key": "speed-ramp",
      "label": "speed-ramp",
      "description": "Nonlinear frame-number remap speed change — fast sweep suddenly drops to 0.2x to linger on the highlight, then accelerates away",
      "use": "",
      "video": "https://gimgs.net/motion/media/speed-ramp-freeze/video-d961ba1dca0a.mp4",
      "poster": "https://gimgs.net/motion/media/speed-ramp-freeze/poster-d6dfb5af2ddf.jpg"
    },
    {
      "key": "freeze-annotate",
      "label": "freeze-annotate",
      "description": "The card stream freezes mid-flow on the full frame, a circled callout pops in to call out the point, then it unfreezes and keeps flowing",
      "use": "",
      "video": "https://gimgs.net/motion/media/speed-ramp-freeze/video-79585b409076.mp4",
      "poster": "https://gimgs.net/motion/media/speed-ramp-freeze/poster-freeze-annotate-f43b5eafbe5d.jpg"
    }
  ],
  "credits": {
    "author": "video-shotcraft (Vincent Wei)",
    "url": "https://github.com/Vincentwei1021/video-shotcraft"
  },
  "page_url": "https://gimgs.net/motion/speed-ramp-freeze.html",
  "json_url": "https://gimgs.net/motion/speed-ramp-freeze.json",
  "template_url": "https://gimgs.net/motion/speed-ramp-freeze/template.html",
  "play_url": null,
  "markdown_url": "https://gimgs.net/motion/speed-ramp-freeze.md",
  "updated_at": "2026-09-24T07:40:24.074Z",
  "prompt": "## When to use\n\nFor slowing/stopping on one highlight in a card stream or long pan; use freeze-annotate in tutorial/explainer contexts\n\n## Intention\n\nA constant-speed stream reads as a slideshow (R2); an all-fast stream leaves viewers unable to catch the highlight (R3). Speed-ramp creates a \"sprint–gaze–sprint\" inside one motion; freeze-annotate goes further — it simply stops, marker-circles the highlight, then moves on. Both share the same technical root (frame-number remap) but differ in meaning: speed-ramp is \"a second glance while passing by,\" freeze is \"stopping class to underline the point.\"\n\n## Duration & energy\n\n- Duration: Speed-ramp: 4–5s total (slow window ≥40f); freeze-annotate: 4–5s total (freeze hold ≥45f)\n- Energy: Medium-high (the speed contrast itself is the energy beat)\n\n## Shot recipe (original Chinese — timing, easing, parameter table, known pitfalls)\n\n## 意图\n匀速流读作 PPT（R2），全程快流观众抓不住重点（R3）。变速在一条\n运动里制造\"冲刺-凝视-冲刺\"；定格标注更进一步——干脆停下来，\n马克笔圈出重点再走。两款同一技术根（帧号 remap），语义不同：\n变速是\"路过时多看一眼\"，定格是\"停课划重点\"。\n\n## 动效核心\n- 源动画匀速，输出帧对源帧做非线性 remap：\n  `src = interpolate(frame, [0,40,85,135], [0,88,97,207])`（斜率即速率）\n- 变速款：快段斜率 ≥2、慢窗斜率 ≈0.2（≥10 倍反差才可感）；\n  **blur 联动速率开关**——快段包 CameraMotionBlur、慢窗裸渲，\n  \"快糊-慢清\"反差是手法成立的一半\n- 定格款：定格段斜率 =0（瞬时切换，无缓入——定格要干脆）；\n  定格期强调色马克笔 SVG 椭圆 8f 描边（stroke-dashoffset）圈住目标 +\n  箭头 6f 点题，feTurbulence（scale≈7）给手绘抖动；解冻段斜率 >1\n  补偿时长，圈注 8f 淡出\n- 目标卡用高清纹理并对位屏中（凝视/圈注对象必须锐利、居中）\n\n## 参数表\n| 参数 | 典型值 | 调节手感 |\n|------|--------|----------|\n| 慢窗/定格时长 | ≥40f / ≥45f | 短了凝视不成立（R3 宁慢勿快） |\n| 快慢斜率反差 | ≥10 倍 | 反差不足读不出\"慢下来了\" |\n| 圈注描边 | 8f 划完 + 抖动 scale 7 | 太慢像加载动画；无抖动像机械标注 |\n\n## 已知坑\n- 变速段 SFX 钉帧跟**输出帧**不跟源帧（画面动作在输出时间轴上发生）；\n  拟音槽位：定格款配马克笔沙沙声（S4），本卡转正时未配、实战补\n- 定格标注一支 30s 片 ≤2 次（同 D 式克制原则）\n- 参数经占位素材调校转正，非实战定稿，首次实战后回验\n\n## 参考实现\ndemos/rhythm/speed-ramp-freeze/\n（FreezeAnnotateReal.tsx / SpeedRampReal.tsx）\n\n## Reference implementation (Remotion)\n\n- demos/rhythm/speed-ramp-freeze/FreezeAnnotateReal.tsx — https://raw.githubusercontent.com/Vincentwei1021/video-shotcraft/main/demos/rhythm/speed-ramp-freeze/FreezeAnnotateReal.tsx\n- demos/rhythm/speed-ramp-freeze/SpeedRampReal.tsx — https://raw.githubusercontent.com/Vincentwei1021/video-shotcraft/main/demos/rhythm/speed-ramp-freeze/SpeedRampReal.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/speed-ramp-freeze/FreezeAnnotateReal.tsx, demos/rhythm/speed-ramp-freeze/SpeedRampReal.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/speed-ramp-freeze/FreezeAnnotateReal.tsx",
      "url": "https://raw.githubusercontent.com/Vincentwei1021/video-shotcraft/main/demos/rhythm/speed-ramp-freeze/FreezeAnnotateReal.tsx"
    },
    {
      "path": "demos/rhythm/speed-ramp-freeze/SpeedRampReal.tsx",
      "url": "https://raw.githubusercontent.com/Vincentwei1021/video-shotcraft/main/demos/rhythm/speed-ramp-freeze/SpeedRampReal.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": 136,
    "repo": "https://github.com/Vincentwei1021/video-shotcraft",
    "files": [
      "https://raw.githubusercontent.com/Vincentwei1021/video-shotcraft/main/demos/rhythm/speed-ramp-freeze/FreezeAnnotateReal.tsx",
      "https://raw.githubusercontent.com/Vincentwei1021/video-shotcraft/main/demos/rhythm/speed-ramp-freeze/SpeedRampReal.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/speed-ramp-freeze/FreezeAnnotateReal.tsx =====\n// freeze-annotate 定格标注（轮 G）——真实卡片流运动中定格，\n// 马克笔琥珀圈注（feTurbulence 手绘抖动）圈住目标卡 + 箭头点题，解冻继续。\n// remap：0–45 流动 → 45–100 定格（斜率0）→ 100–135 解冻（1.4x 补偿）。\nimport { useId } from 'react';\nimport { AbsoluteFill, Img, interpolate, staticFile, useCurrentFrame } from 'remotion';\nimport layout from '../../_textures/live-layout.json';\n\nexport const FREEZEANNOTATE_DUR = 135;\n\nconst CARD_W = 460;\nconst GAP = 60;\nconst RAIL = layout.projects.cards.slice(0, 9);\nconst TARGET_I = 5;\nconst AMBER = '#b45309';\n\nexport const FreezeAnnotateReal: React.FC = () => {\n  const frame = useCurrentFrame();\n  // 滤镜 ID 按实例生成，多实例同场不串引（useId 的 «:» 在 url() 里非法，需清洗）\n  const roughId = `rough-${useId().replace(/[^a-zA-Z0-9]/g, '')}`;\n  const src = interpolate(frame, [0, 45, 100, 135], [0, 45, 45, 94], {\n    extrapolateLeft: 'clamp', extrapolateRight: 'clamp',\n  });\n  const dx = src * 22;\n  const draw = interpolate(frame, [52, 60], [0, 1], { extrapolateLeft: 'clamp', extrapolateRight: 'clamp' });\n  const arrowDraw = interpolate(frame, [60, 66], [0, 1], { extrapolateLeft: 'clamp', extrapolateRight: 'clamp' });\n  const fade = interpolate(frame, [96, 104], [1, 0], { extrapolateLeft: 'clamp', extrapolateRight: 'clamp' });\n  // 导轨起点 -880：定格时（dx=990）目标卡 k=5 中心 = -880+2600-990+230 = 960 屏中\n  const X0 = -880;\n  const targetX = X0 + TARGET_I * (CARD_W + GAP) - dx + CARD_W / 2;\n  const C = 1750; // 椭圆周长近似\n  return (\n    <AbsoluteFill style={{ backgroundColor: '#f9f6f1', overflow: 'hidden' }}>\n      <div style={{ position: 'absolute', top: 370, transform: `translateX(${-dx + X0}px)` }}>\n        {Array.from({ length: 14 }).map((_, k) => {\n          const c = RAIL[k % RAIL.length];\n          const isTarget = k === TARGET_I;\n          return (\n            <Img\n              key={k}\n              src={staticFile(`textures/live/${isTarget ? 'card4-hires.png' : c.file}`)}\n              style={{\n                position: 'absolute', left: k * (CARD_W + GAP), top: 0,\n                width: CARD_W, borderRadius: 12,\n                boxShadow: '0 4px 16px rgba(31,28,23,0.10)',\n              }}\n            />\n          );\n        })}\n      </div>\n      <svg width={1920} height={1080} style={{ position: 'absolute', inset: 0, pointerEvents: 'none', opacity: fade }}>\n        <defs>\n          <filter id={roughId}>\n            <feTurbulence type=\"fractalNoise\" baseFrequency=\"0.02\" numOctaves=\"2\" seed=\"7\" result=\"n\" />\n            <feDisplacementMap in=\"SourceGraphic\" in2=\"n\" scale=\"7\" />\n          </filter>\n        </defs>\n        <ellipse\n          cx={targetX} cy={560} rx={290} ry={230}\n          fill=\"none\" stroke={AMBER} strokeWidth={8} strokeLinecap=\"round\"\n          strokeDasharray={C} strokeDashoffset={C * (1 - draw)}\n          transform={`rotate(-6 ${targetX} 560)`}\n          filter={`url(#${roughId})`}\n        />\n        <path\n          d={`M ${targetX + 330} 190 Q ${targetX + 220} 240 ${targetX + 140} 320`}\n          fill=\"none\" stroke={AMBER} strokeWidth={8} strokeLinecap=\"round\"\n          strokeDasharray={420} strokeDashoffset={420 * (1 - arrowDraw)}\n          filter={`url(#${roughId})`}\n        />\n      </svg>\n    </AbsoluteFill>\n  );\n};\n\n\n// ===== demos/rhythm/speed-ramp-freeze/SpeedRampReal.tsx =====\n// speed-ramp 变速（轮 C）——真实卡片流帧号 remap：快(斜率2.2) →\n// 0.2x 慢速展示窗 → 快。慢速窗中目标卡（card4-hires）清晰滑过屏中。\n// blur 联动速率：快段包 blur、慢段不包，反差即\"凝视感\"。\nimport { AbsoluteFill, Img, interpolate, staticFile, useCurrentFrame } from 'remotion';\nimport { CameraMotionBlur } from '@remotion/motion-blur';\nimport layout from '../../_textures/live-layout.json';\n\nexport const SPEEDRAMP_DUR = 135;\n\nconst CARD_W = 460;\nconst GAP = 60;\nconst RAIL = layout.projects.cards.slice(0, 9);\nconst TARGET_I = 5;\n\nconst Scene: React.FC = () => {\n  const frame = useCurrentFrame();\n  // remap：0–40f 走 88 源帧（快），40–85f 走 9 源帧（0.2x），85–135f 走 110（快）\n  const src = interpolate(frame, [0, 40, 85, 135], [0, 88, 97, 207], {\n    extrapolateLeft: 'clamp', extrapolateRight: 'clamp',\n  });\n  const dx = src * 28;\n  return (\n    <AbsoluteFill style={{ backgroundColor: '#f9f6f1', overflow: 'hidden' }}>\n      {/* 偏移 717：让慢速窗中点（src≈92.4，dx≈2587）时目标卡 k=5 中心落屏中 */}\n      <div style={{ position: 'absolute', top: 360, transform: `translateX(${-dx + 717}px)` }}>\n        {Array.from({ length: 16 }).map((_, k) => {\n          const c = RAIL[k % RAIL.length];\n          const isTarget = k === TARGET_I;\n          return (\n            <Img\n              key={k}\n              src={staticFile(`textures/live/${isTarget ? 'card4-hires.png' : c.file}`)}\n              style={{\n                position: 'absolute', left: k * (CARD_W + GAP), top: isTarget ? -16 : 0,\n                width: CARD_W, borderRadius: 12,\n                boxShadow: isTarget\n                  ? '0 10px 32px rgba(31,28,23,0.18)'\n                  : '0 4px 16px rgba(31,28,23,0.10)',\n              }}\n            />\n          );\n        })}\n      </div>\n    </AbsoluteFill>\n  );\n};\n\nexport const SpeedRampReal: React.FC = () => {\n  const frame = useCurrentFrame();\n  const fast = frame < 42 || frame > 83;\n  return fast ? (\n    <CameraMotionBlur shutterAngle={200} samples={20}>\n      <Scene />\n    </CameraMotionBlur>\n  ) : (\n    <Scene />\n  );\n};\n\n\n"
}