{
  "slug": "sakuga-timing-shift",
  "title": "Sakuga timing shift",
  "tagline": "On-3s cuts to on-1s — an element moves in choppy 3-frame steps, then snaps to buttery frame-by-frame speed at the climax.",
  "description": "On-3s cuts to on-1s — an element moves in choppy 3-frame steps, then snaps to buttery frame-by-frame speed at the climax.",
  "category": "rhythm",
  "tags": [
    "shotcraft",
    "remotion",
    "rhythm",
    "ui-entrance"
  ],
  "tier": "free",
  "license": "Apache-2.0",
  "spec": {
    "width": 1920,
    "height": 1080,
    "fps": 30,
    "duration": 5.06,
    "aspect": "16:9"
  },
  "kind": "remotion",
  "params_schema": {
    "type": "object",
    "properties": {}
  },
  "default_params": {},
  "preview": {
    "poster": "https://gimgs.net/motion/media/sakuga-timing-shift/poster-ab20d4f9297b.jpg",
    "video": "https://gimgs.net/motion/media/sakuga-timing-shift/video-0d10d5d1246e.mp4"
  },
  "variants": [
    {
      "key": "sakuga-timing-shift",
      "label": "sakuga-timing-shift",
      "description": "On-3s to on-1s — an element first moves in choppy flip-book steps of 3 frames each, then at the climax instant switches to buttery frame-by-frame sprinting; the quantized frame-rate jump itself is the spectacle.",
      "use": "",
      "video": "https://gimgs.net/motion/media/sakuga-timing-shift/video-0d10d5d1246e.mp4",
      "poster": "https://gimgs.net/motion/media/sakuga-timing-shift/poster-ab20d4f9297b.jpg"
    }
  ],
  "credits": {
    "author": "video-shotcraft (Vincent Wei)",
    "url": "https://github.com/Vincentwei1021/video-shotcraft"
  },
  "page_url": "https://gimgs.net/motion/sakuga-timing-shift.html",
  "json_url": "https://gimgs.net/motion/sakuga-timing-shift.json",
  "template_url": "https://gimgs.net/motion/sakuga-timing-shift/template.html",
  "play_url": null,
  "markdown_url": "https://gimgs.net/motion/sakuga-timing-shift.md",
  "updated_at": "2026-09-24T07:40:18.939Z",
  "prompt": "## When to use\n\nEmphatic movement for a single element (a card entrance, a metric sprinting to the finish); sections that need a \"handmade feel → climax burst\" contrast\n\n## Intention\n\nThe library already has speed-ramp for continuous speed changes and hit-counter for a localized hitch. This card is different — **it changes the frame rate itself, not the speed**: during the choppy section, the driving frame is q=floor(f/3)*3, so the element only moves once every 3 frames (a 10fps flip-book feel); at the switch point it starts driving off the raw f frame by frame for a buttery sprint. The chunky threes and the smooth ones cut together in the same shot — this is the \"shooting on 3s / on 1s\" performance technique from Japanese animation. The choppy section isn't cheap, it's handmade-feeling, and the switch point itself is the climax marker.\n\n## Duration & energy\n\n- Duration: ~5s\n- Energy: Medium-high\n\n## Shot recipe (original Chinese — timing, easing, parameter table, known pitfalls)\n\n## 意图\n库内时间轴操纵已有 speed-ramp 连续变速与 hit-counter 的局部顿帧。\n本卡是另一种——**改的不是速度是帧率本身**：\n顿挫段驱动帧 q=floor(f/3)*3，元素每 3 帧才动一步（10fps 手翻书），\n切换帧起改用原始 f 逐帧驱动，丝滑冲刺。一拍三的钝与一拍一的滑\n同屏对切，日式作画的拍数演出（shooting on 3s / on 1s）。顿挫段\n不是低级感是手工感，切换点即高潮标记。\n\n## 动效核心\n- 顿挫段：q = Math.floor(f/3)*3 当驱动帧，位置线性 X 120→1380，\n  每步 ≈79px；姿势 sin(q) rotate ±5°，一步一个样\n- f48 切换：改原始 f 连续驱动，Easing.out(poly(4)) 高初速冲刺\n  折返中央 + 过冲 36px + 3f scaleX 0.9 回弹急停\n- 冲刺段运动拉伸由位置差分速度驱动（scaleX 峰值 ≈1.35 /\n  scaleY 0.88），残影 2 层**条件挂载**——速度低即摘除，非 opacity 0\n- 角标 \"on 3s\"/\"on 1s\" 随段切换，切换瞬间 pop 1.35×；角标\n  line-boil 用 h() 伪随机、f=108 后冻结\n- f108–150 真静止 42f，全画面无任何动画\n\n## 参数表\n| 参数 | 典型值 | 调节手感 |\n|------|--------|----------|\n| 拍数 | 顿挫段 floor(f/3)（一拍三） | /2 顿感不足、/4 以上读作播放器卡死 |\n| 每步位移 | ≈79px（总程 ÷ 步数倒推） | <30px 读作卡顿掉帧不是顿挫——命门 |\n| 步间姿势 | sin(q) rotate ±5° | 无姿势变化的纯位移顿挫像 bug 不像手翻书 |\n| 切换帧 | f48（顿挫:冲刺 ≈ 3:2） | 切换必须一帧完成，任何补间都毁掉\"换拍\"感 |\n| 冲刺缓动 | Easing.out(poly(4)) + 过冲 36px + 3f 回弹 | 初速要猛，与顿挫段拉满反差 |\n| 收尾 | 真静止 ≥42f，角标沸腾同步冻结 | 沸腾残留毁真静止（feTurbulence 摘罩判例同族） |\n\n## 已知坑\n- demo 在灰阶/占位素材上调校通过——参数是调校起点非实战定稿，\n  首次实战须以真实素材回验\n- 同一镜头单一时间操纵者判例：与 speed-ramp / hit-counter 顿帧\n  互斥，一个镜头只能有一个人动时间轴\n- 顿挫段每步位移 ≥30px 才读得出\"顿\"——微步顿挫读作渲染掉帧，\n  这是本卡命门；步幅不够就缩短总时长换大步幅\n- 切换点必须有标注（角标 \"on 3s\"→\"on 1s\" 或一声打点，\n  sound-design §4.5）——无标注观众只觉得\"变快了\"，读不出\"换拍\"\n\n## 参考实现\ndemos/rhythm/sakuga-timing-shift/\n（SakugaTimingShift.tsx）\n\n## Reference implementation (Remotion)\n\n- demos/rhythm/sakuga-timing-shift/SakugaTimingShift.tsx — https://raw.githubusercontent.com/Vincentwei1021/video-shotcraft/main/demos/rhythm/sakuga-timing-shift/SakugaTimingShift.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/sakuga-timing-shift/SakugaTimingShift.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/sakuga-timing-shift/SakugaTimingShift.tsx",
      "url": "https://raw.githubusercontent.com/Vincentwei1021/video-shotcraft/main/demos/rhythm/sakuga-timing-shift/SakugaTimingShift.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": 152,
    "repo": "https://github.com/Vincentwei1021/video-shotcraft",
    "files": [
      "https://raw.githubusercontent.com/Vincentwei1021/video-shotcraft/main/demos/rhythm/sakuga-timing-shift/SakugaTimingShift.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/sakuga-timing-shift/SakugaTimingShift.tsx =====\n// sakuga-timing-shift —— 一拍三转一拍一（作画打拍切换）\n// 0–48f：驱动帧 q = floor(f/3)*3，卡从左侧顿挫横移到右侧（10fps 手翻书感），\n// 每步 ≈74px + rotate 摆动 = 一拍三的钝感；48f 切换点后改用原始 f 连续驱动，\n// 48–75f 丝滑冲刺折返中央（out-poly(4) 高初速 + 运动拉伸 scaleX + 残影），\n// 过冲 36px 后 3f 回弹落位。左上角标 \"on 3s\"/\"on 1s\" 随段切换并带 line-boil\n// （boil 在 f=108 后冻结）。收尾真静止 ≥40f。\nimport React from 'react';\nimport { useCurrentFrame, interpolate, Easing } from 'remotion';\nimport { G, Card, TitleBlock } from '../../_fixtures/Fixtures';\n\nconst W = 1920;\nconst CARD_W = 420;\nconst CARD_H = 260;\nconst CARD_Y = (1080 - CARD_H) / 2; // 410\n\nconst X_LEFT = 120;\nconst X_RIGHT = 1380; // 卡左缘，右侧停点\nconst X_CENTER = (W - CARD_W) / 2; // 750，中央落位\nconst OVERSHOOT = 36;\n\nconst SWITCH = 48; // 打拍切换帧\nconst ARRIVE = 70; // 冲刺到过冲点\nconst SETTLE = 75; // 回弹落位完成\n\n// 段一：一拍三。位置函数线性，但只在 q = floor(f/3)*3 上取值。\nconst pos1 = (t: number): number =>\n  interpolate(t, [0, SWITCH], [X_LEFT, X_RIGHT], {\n    extrapolateLeft: 'clamp',\n    extrapolateRight: 'clamp',\n  });\n\n// 段二：一拍一。out-poly(4) 高初速冲刺 → 过冲 → 3f 回弹。\nconst pos2 = (t: number): number =>\n  interpolate(t, [SWITCH, ARRIVE, SETTLE], [X_RIGHT, X_CENTER - OVERSHOOT, X_CENTER], {\n    easing: Easing.out(Easing.poly(4)),\n    extrapolateLeft: 'clamp',\n    extrapolateRight: 'clamp',\n  });\n\nexport const SakugaTimingShift: React.FC = () => {\n  const f = useCurrentFrame();\n  const onThrees = f < SWITCH;\n\n  // ---- 卡片位置 ----\n  const q = Math.floor(f / 3) * 3; // 一拍三驱动帧\n  const x = onThrees ? pos1(q) : pos2(f);\n\n  // 顿挫段 rotate 摆动（随 q 冻结，一步一个姿势）\n  const rot = onThrees\n    ? Math.sin(q * 0.7) * 5\n    : interpolate(f, [SWITCH, SWITCH + 8], [Math.sin(SWITCH * 0.7) * 5, 0], {\n        extrapolateLeft: 'clamp',\n        extrapolateRight: 'clamp',\n      });\n\n  // 冲刺段速度（位置差分，帧时间解耦）→ 运动拉伸\n  const v = onThrees ? 0 : Math.abs(pos2(f) - pos2(f - 1));\n  const sFac = Math.min(v / 55, 1);\n  const stretchX = 1 + 0.35 * sFac; // 峰值 ≈1.35，横向拉丝\n  const stretchY = 1 - 0.12 * sFac;\n\n  // 落位急停回弹：72–78f scaleX 压扁再回 1\n  const sqX = interpolate(f, [SETTLE - 3, SETTLE, SETTLE + 3], [1, 0.9, 1], {\n    easing: Easing.out(Easing.cubic),\n    extrapolateLeft: 'clamp',\n    extrapolateRight: 'clamp',\n  });\n  const sqY = interpolate(f, [SETTLE - 3, SETTLE, SETTLE + 3], [1, 1.07, 1], {\n    easing: Easing.out(Easing.cubic),\n    extrapolateLeft: 'clamp',\n    extrapolateRight: 'clamp',\n  });\n\n  // 冲刺残影：只在段二速度高时挂载（条件挂载，不留 opacity 0 的壳）\n  const ghosts =\n    !onThrees && f > SWITCH + 1 && f < ARRIVE + 2 && sFac > 0.15\n      ? [\n          { xg: pos2(f - 2), op: 0.28 * sFac },\n          { xg: pos2(f - 4), op: 0.13 * sFac },\n        ]\n      : [];\n\n  // ---- 角标 \"on 3s\" / \"on 1s\"，line-boil，f=108 后冻结 ----\n  const h = (n: number) => {\n    const s = Math.sin(n * 127.3) * 43758.5453;\n    return s - Math.floor(s);\n  };\n  const qb = Math.min(Math.floor(f / 4) * 4, 108); // boil 驱动帧，108 后冻结\n  const bx = (h(qb + 1) - 0.5) * 7;\n  const by = (h(qb + 2) - 0.5) * 7;\n  const brot = (h(qb + 3) - 0.5) * 3;\n  // 切换瞬间角标弹一下\n  const pop = interpolate(f, [SWITCH, SWITCH + 3, SWITCH + 9], [1, 1.35, 1], {\n    easing: Easing.out(Easing.cubic),\n    extrapolateLeft: 'clamp',\n    extrapolateRight: 'clamp',\n  });\n\n  const titleOp = interpolate(f, [0, 10], [0, 1], {\n    extrapolateLeft: 'clamp',\n    extrapolateRight: 'clamp',\n  });\n\n  return (\n    <div style={{ width: 1920, height: 1080, background: G.bg, position: 'relative', overflow: 'hidden' }}>\n      <div style={{ position: 'absolute', top: 90, width: '100%', textAlign: 'center', opacity: titleOp }}>\n        <TitleBlock text=\"SAKUGA TIMING SHIFT\" size={64} />\n      </div>\n\n      {/* 轨道基线 + 中央落位虚线槽 */}\n      <div\n        style={{\n          position: 'absolute',\n          left: 100,\n          right: 100,\n          top: CARD_Y + CARD_H + 24,\n          height: 3,\n          background: G.line,\n          borderRadius: 2,\n        }}\n      />\n      <div\n        style={{\n          position: 'absolute',\n          left: X_CENTER,\n          top: CARD_Y,\n          width: CARD_W,\n          height: CARD_H,\n          border: `3px dashed ${G.bar}`,\n          borderRadius: 14,\n          boxSizing: 'border-box',\n        }}\n      />\n\n      {/* 冲刺残影 */}\n      {ghosts.map((g, i) => (\n        <div\n          key={`ghost-${i}`}\n          style={{ position: 'absolute', left: 0, top: CARD_Y, opacity: g.op, transform: `translateX(${g.xg}px)` }}\n        >\n          <Card w={CARD_W} h={CARD_H} seed={4} />\n        </div>\n      ))}\n\n      {/* 主卡 */}\n      <div\n        style={{\n          position: 'absolute',\n          left: 0,\n          top: CARD_Y,\n          transform: `translateX(${x}px) rotate(${rot}deg) scaleX(${stretchX * sqX}) scaleY(${stretchY * sqY})`,\n          transformOrigin: '50% 50%',\n        }}\n      >\n        <Card w={CARD_W} h={CARD_H} seed={4} />\n      </div>\n\n      {/* 角标：on 3s / on 1s */}\n      <div\n        style={{\n          position: 'absolute',\n          left: 120,\n          top: 160,\n          transform: `translate(${bx}px, ${by}px) rotate(${brot}deg) scale(${pop})`,\n          transformOrigin: '0% 50%',\n          fontFamily: 'Courier New, monospace',\n          fontWeight: 700,\n          fontSize: 84,\n          color: G.ink,\n          borderBottom: `6px solid ${G.ink}`,\n          paddingBottom: 6,\n        }}\n      >\n        {onThrees ? 'on 3s' : 'on 1s'}\n      </div>\n    </div>\n  );\n};\n\n\n"
}