{
  "slug": "impact-feedback",
  "title": "Impact feedback",
  "tagline": "Two impact-feedback styles: hit-counter, combo counting (a hitch frame plus damage numbers and combo pop-text), and anime-impact, an anime impact frame (negative flash plus speed lines plus chromatic aberration).",
  "description": "Two impact-feedback styles: hit-counter, combo counting (a hitch frame plus damage numbers and combo pop-text), and anime-impact, an anime impact frame (negat",
  "category": "effects",
  "tags": [
    "shotcraft",
    "remotion",
    "effects"
  ],
  "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/impact-feedback/poster-f07bd02a2335.jpg",
    "video": "https://gimgs.net/motion/media/impact-feedback/video-90b7941ef8da.mp4"
  },
  "variants": [
    {
      "key": "hit-counter",
      "label": "hit-counter · Combo hit counter",
      "description": "Three cards slam in one after another; each hit = a 2f hitch frame + a floating damage number + a ×N combo counter that steps up each time.",
      "use": "A rapid-fire feature-point segment — tell \"there are lots of features\" as a combo; heavy gamified language",
      "video": "https://gimgs.net/motion/media/impact-feedback/video-90b7941ef8da.mp4",
      "poster": "https://gimgs.net/motion/media/impact-feedback/poster-f07bd02a2335.jpg"
    },
    {
      "key": "anime-impact",
      "label": "anime-impact · Anime impact frame",
      "description": "On a crash-zoom stop, 3 frames of full-frame negative inversion + radiating speed lines + red/cyan chromatic aberration, all pulled back on frame 4.",
      "use": "The one punch that is the film's single climax; the top of the emphasis scale",
      "video": "https://gimgs.net/motion/media/impact-feedback/video-ee85a6b83d02.mp4",
      "poster": "https://gimgs.net/motion/media/impact-feedback/poster-anime-impact-af36ab911d1f.jpg"
    }
  ],
  "credits": {
    "author": "video-shotcraft (Vincent Wei)",
    "url": "https://github.com/Vincentwei1021/video-shotcraft"
  },
  "page_url": "https://gimgs.net/motion/impact-feedback.html",
  "json_url": "https://gimgs.net/motion/impact-feedback.json",
  "template_url": "https://gimgs.net/motion/impact-feedback/template.html",
  "play_url": null,
  "markdown_url": "https://gimgs.net/motion/impact-feedback.md",
  "updated_at": "2026-09-24T07:40:03.218Z",
  "prompt": "## When to use\n\nThe \"moment of impact\" when an element lands or slams to a stop — gives a smash-in or crash-stop a game-level sense of feedback; pick a style along an intensity ladder.\n\n## Intention\n\nWhen an element slams into place with no sense of \"it landed\", the placement just reads as a PPT fly-in. Both game juice (Vlambeer's screenshake theory) and anime impact frames arrive at the same answer: the frames right at impact bite down on time, and that's how the impulse transfers to the audience. The two styles form one intensity ladder: B uses a local hitch frame plus stacked damage numbers and combo pop-text, cutting a feature list into a combo sequence — the strongest narrative feel; C is the heaviest punch — a 3-frame freeze flipped to negative with radiating speed lines, the highest, most stylized intensity level.\n\n## Duration & energy\n\n- Duration: n/a (an element-level technique that piggybacks on a landing action; each style's frame budget is in its parameter table)\n- Energy: High (an instant impact)\n\n## Shot recipe (original Chinese — timing, easing, parameter table, known pitfalls)\n\n## 意图\n元素砸到位却没有\"砸到了\"的一瞬，落位读作 PPT 飞入。游戏 juice\n（Vlambeer screenshake 理论）与动漫演出 impact frames 给了同一答案：\n命中的那几帧把时间咬住，冲量才传到观众手上。两式是一条强度阶梯：\nB 用局部顿帧叠伤害数字和 combo 跳字，把功能清单剪成一套连招，\n叙事感最强；C 是最重的一拳——撞停 3 帧翻负片炸集中线，强调级最高、\n最风格化。\n\n## 两式选型表\n| 式 | 做法 | 适用 |\n|----|------|------|\n| B hit-counter 连招计数 | 三卡接连砸入，每命中 = 顿帧 2f + 伤害数字上浮 + ×N 计数跳字逐次加码 | 功能点连打段——\"功能很多\"用连招讲；游戏化语言浓 |\n| C anime-impact 打击帧 | crash-zoom 撞停的 3f 整幅负片反色 + 放射集中线 + 红青色散，第 4f 全撤 | 全片唯一高潮的那一拳；强调级封顶 |\n\n## 参数表\n| 参数 | 典型值 | 调节手感 |\n|------|--------|----------|\n| B 节奏 | 三卡各隔 30f，命中前 10f ease-in(quad) 砸落；每命中全局冻结 2f | 间隔均匀才有\"连招\"节拍感 |\n| B 伤害数字 | 落点 scale 1.4→1 + 上浮 40–60px + 12f 渐隐 | 渐隐 >16f 拖泥带水，遮住下一击 |\n| B 计数器 | 脉冲峰值 1.3/1.45/1.6、rotate −2/−4/−6°、字号递增，`exp(−t/2.4)` 回落 | 逐次加码是命门——三次一样大读不出\"越打越狠\" |\n| C 急推撞停 | crash-zoom 6f ease-in(cubic) 推到 2.4x，卡心收敛到画面正中 | 急推参数走 crash-zoom-punch 卡 |\n| C 冲击帧 | 撞停起 3f：`invert(1) grayscale(1)` + 30 根楔形集中线（每 1–2f 换形态）+ 红/青负片副本 screen 叠加 ±8px 错位；第 4f 全撤 | 3f 是甜点——4f 起观众开始\"看清\"负片，魔术就穿帮 |\n| C 恢复震屏 | 第 4f 起 6px 指数衰减（τ≈2.2f），~12f 收干 | 幅度须过肉眼阈值（可感性判例） |\n\n## 已知坑\n- demo 在灰阶/占位素材上调校通过——参数是调校起点非实战定稿，\n  首次实战须以真实素材回验\n- B 式顿帧**必须全局**——背景滚动也要一起冻，否则读不出\"时间被\n  咬住\"，只像那张卡掉帧。demo 用统一 remap 帧 df 驱动所有层是命门；\n  唯闪白等\"冻结期间也要发生\"的效果用真实帧 f 驱动\n- 帧 remap 与 speed-ramp-freeze 同技术根，同一镜头别叠用两卡——\n  两套 remap 互相打架，节奏读作故障\n- C 式负片帧在纸墨审美的片子里非常抢戏——限高潮一次，且必须与\n  impact 音效同帧落地（拟音走 sound-design），无声的负片帧读作闪坏\n- B 式游戏化语言浓（伤害数字/combo 是明晃晃的游戏梗），产品调性\n  偏严肃时慎用\n- B 式多次顿帧后真实帧比动画时间多出累计冻结帧，排镜头预算时\n  按 `真实命中帧 = 动画命中帧 + 已冻结帧数` 折算（demo HITS_REAL）\n\n## 参考实现\ndemos/effects/impact-feedback/\n（AnimeImpact.tsx / HitCounter.tsx）\n\n## Reference implementation (Remotion)\n\n- demos/effects/impact-feedback/AnimeImpact.tsx — https://raw.githubusercontent.com/Vincentwei1021/video-shotcraft/main/demos/effects/impact-feedback/AnimeImpact.tsx\n- demos/effects/impact-feedback/HitCounter.tsx — https://raw.githubusercontent.com/Vincentwei1021/video-shotcraft/main/demos/effects/impact-feedback/HitCounter.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/effects/impact-feedback/AnimeImpact.tsx, demos/effects/impact-feedback/HitCounter.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/effects/impact-feedback/AnimeImpact.tsx",
      "url": "https://raw.githubusercontent.com/Vincentwei1021/video-shotcraft/main/demos/effects/impact-feedback/AnimeImpact.tsx"
    },
    {
      "path": "demos/effects/impact-feedback/HitCounter.tsx",
      "url": "https://raw.githubusercontent.com/Vincentwei1021/video-shotcraft/main/demos/effects/impact-feedback/HitCounter.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/effects/impact-feedback/AnimeImpact.tsx",
      "https://raw.githubusercontent.com/Vincentwei1021/video-shotcraft/main/demos/effects/impact-feedback/HitCounter.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/effects/impact-feedback/AnimeImpact.tsx =====\nimport React from 'react';\nimport { AbsoluteFill, Easing, interpolate, useCurrentFrame } from 'remotion';\nimport { FakeDashboard, Card, TitleBlock, G } from '../../_fixtures/Fixtures';\n\n// anime-impact 动漫打击帧〔组合〕：crash-zoom 急推撞停在目标卡上的那 3 帧，\n// 整幅画面反转成黑白负片 + 手绘放射集中线 + 红青通道 ±8px 色散——\n// 像素被打了一拳，第 4 帧一切撤掉恢复干净特写 + 6px 震屏衰减。\n// 节拍：0–24 建立全景 hold → 24–30 急推(6f ease-in 到 2.4x) →\n//       30–32 冲击帧(3f 负片/集中线/RGB split，每帧换形态) →\n//       33 起恢复特写 + 震屏衰减 → ~45–120 静止读卡。\n\nconst ZOOM_START = 24;\nconst ZOOM_END = 30; // 撞停帧\nconst IMPACT_LEN = 3; // 冲击帧持续 3f（30/31/32）\nconst RECOVER = ZOOM_END + IMPACT_LEN; // 33：恢复干净特写\n\n// 目标卡：盖在 3x2 网格中排第 2 格上（列 2 行 1）\nconst CARD = { x: 808, y: 108, w: 524, h: 454 };\nconst CX = CARD.x + CARD.w / 2; // 1070\nconst CY = CARD.y + CARD.h / 2; // 335\nconst SCALE_END = 2.4;\n\n// seed 正弦哈希（禁 Math.random）\nconst rnd = (i: number) => {\n  const s = Math.sin(i * 127.3) * 43758.5453;\n  return s - Math.floor(s);\n};\n\n// 手绘放射集中线：30 根从卡片周缘指向画框外的尖楔形，phase 换一次形态\nconst SpeedLines: React.FC<{ phase: number }> = ({ phase }) => {\n  const cx = 960;\n  const cy = 540;\n  const R_OUT = 1300; // 超出画框对角(~1101)\n  const polys = Array.from({ length: 30 }).map((_, i) => {\n    const k = i * 13 + phase * 101;\n    const ang = ((i + 0.5) / 30) * Math.PI * 2 + (rnd(k) - 0.5) * 0.22;\n    const r0 = 300 + rnd(k + 1) * 220; // 内端长短随机（卡片周缘一带）\n    const halfW = (7 + rnd(k + 2) * 16) / R_OUT; // 外端宽 7–23px 的楔形\n    const ax = cx + Math.cos(ang) * r0;\n    const ay = cy + Math.sin(ang) * r0;\n    const b1x = cx + Math.cos(ang - halfW) * R_OUT;\n    const b1y = cy + Math.sin(ang - halfW) * R_OUT;\n    const b2x = cx + Math.cos(ang + halfW) * R_OUT;\n    const b2y = cy + Math.sin(ang + halfW) * R_OUT;\n    return `${ax},${ay} ${b1x},${b1y} ${b2x},${b2y}`;\n  });\n  return (\n    <svg\n      viewBox=\"0 0 1920 1080\"\n      style={{ position: 'absolute', inset: 0, width: '100%', height: '100%' }}\n    >\n      {polys.map((pts, i) => (\n        <polygon key={i} points={pts} fill={i % 4 === 0 ? '#111111' : '#f5f5f5'} />\n      ))}\n    </svg>\n  );\n};\n\n// 目标卡叠层 + 全景底\nconst Scene: React.FC = () => (\n  <>\n    <FakeDashboard variant=\"A\" />\n    <div style={{ position: 'absolute', left: CARD.x, top: CARD.y }}>\n      <Card\n        w={CARD.w}\n        h={CARD.h}\n        seed={9}\n        style={{ boxShadow: '0 10px 36px rgba(0,0,0,0.18)', border: `3px solid ${G.ink}` }}\n      />\n      <div style={{ position: 'absolute', left: 24, bottom: 96 }}>\n        <TitleBlock text=\"IMPACT\" size={92} />\n      </div>\n    </div>\n  </>\n);\n\nexport const AnimeImpact: React.FC = () => {\n  const frame = useCurrentFrame();\n\n  // 急推：6f ease-in 撞到 2.4x，卡心推到画面正中\n  const p = interpolate(frame, [ZOOM_START, ZOOM_END], [0, 1], {\n    extrapolateLeft: 'clamp',\n    extrapolateRight: 'clamp',\n    easing: Easing.in(Easing.cubic),\n  });\n  const scale = 1 + p * (SCALE_END - 1);\n  const tx = (960 - CX) * p;\n  const ty = (540 - CY) * p;\n\n  const impact = frame >= ZOOM_END && frame < RECOVER;\n  const phase = impact ? frame - ZOOM_END : 0; // 每 1f 换一次集中线形态\n\n  // 撞停后震屏：6px 起步、指数衰减，~12f 收干后真静止\n  const since = frame - RECOVER;\n  const env = since >= 0 ? 6 * Math.exp(-since / 2.2) : 0;\n  const shakeX = env * Math.sin(since * 3.7);\n  const shakeY = env * 0.7 * Math.sin(since * 5.1 + 0.9);\n\n  const zoomStyle: React.CSSProperties = {\n    position: 'absolute',\n    inset: 0,\n    transform: `translate(${tx}px, ${ty}px) scale(${scale})`,\n    transformOrigin: `${CX}px ${CY}px`,\n  };\n\n  return (\n    <AbsoluteFill style={{ background: impact ? '#131315' : G.bg, overflow: 'hidden' }}>\n      <div style={{ position: 'absolute', inset: 0, transform: `translate(${shakeX}px, ${shakeY}px)` }}>\n        {/* 主层：冲击帧期间整幅负片黑白 */}\n        <div style={{ ...zoomStyle, filter: impact ? 'invert(1) grayscale(1)' : 'none' }}>\n          <Scene />\n        </div>\n        {/* RGB split：红/青双层负片副本，screen 叠底、±8px 错位（随 phase 抖动） */}\n        {impact && (\n          <>\n            <div\n              style={{\n                position: 'absolute',\n                inset: 0,\n                mixBlendMode: 'screen',\n                transform: `translate(-8px, ${phase % 2 === 0 ? 4 : -4}px)`,\n              }}\n            >\n              <div style={{ ...zoomStyle, filter: 'invert(1) grayscale(1)' }}>\n                <Scene />\n              </div>\n              <div style={{ position: 'absolute', inset: 0, background: '#ff0033', mixBlendMode: 'multiply' }} />\n            </div>\n            <div\n              style={{\n                position: 'absolute',\n                inset: 0,\n                mixBlendMode: 'screen',\n                transform: `translate(8px, ${phase % 2 === 0 ? -4 : 4}px)`,\n              }}\n            >\n              <div style={{ ...zoomStyle, filter: 'invert(1) grayscale(1)' }}>\n                <Scene />\n              </div>\n              <div style={{ position: 'absolute', inset: 0, background: '#00e5ff', mixBlendMode: 'multiply' }} />\n            </div>\n            {/* 手绘放射集中线：每帧换形态 */}\n            <SpeedLines phase={phase} />\n          </>\n        )}\n      </div>\n    </AbsoluteFill>\n  );\n};\n\n\n// ===== demos/effects/impact-feedback/HitCounter.tsx =====\nimport React from 'react';\nimport { AbsoluteFill, Easing, interpolate, useCurrentFrame } from 'remotion';\nimport { FakeDashboard, Card, TitleBlock, G } from '../../_fixtures/Fixtures';\n\n// hit-counter 连招计数：三张功能卡接连砸入槽位，每次命中 = 全局顿帧 2f\n// + 落点伤害数字上浮渐隐 + 右上角 ×N 计数跳字（脉冲/倾斜逐次加码）。\n// 组合：hitstop + damage-number-pop + combo-counter。\n\n// —— 时间结构（动画时间 t 空间）——\n// 0–19 建立 hold；卡 i 于 t = HIT-10 → HIT ease-in 砸落；HIT = 30/60/90；\n// 每命中后真实帧多出 2f 顿帧，末卡效果收完后静止 hold 到 150。\nconst HITS_T = [30, 60, 90]; // 命中时刻（重映射后的动画时间）\nconst STOP = 2; // 每次顿帧帧数\n// 真实命中帧 = 动画命中时刻 + 前面累计的顿帧\nconst HITS_REAL = HITS_T.map((h, i) => h + i * STOP);\n\nconst SLOT_W = 420;\nconst SLOT_H = 300;\nconst SLOT_Y = 430;\nconst SLOT_XS = [390, 890, 1390].map((cx) => cx - SLOT_W / 2);\n\nconst DMG_TEXT = ['+1.2k', '+2.4k', '+4.8k'];\nconst PULSE = [1.3, 1.45, 1.6]; // 计数器脉冲峰值，逐次递增\nconst TILT = [-2, -4, -6]; // 计数器倾斜，逐次递增并保持\n\nexport const HitCounter: React.FC = () => {\n  const frame = useCurrentFrame();\n\n  // —— 全局帧 remap：每个真实命中帧起冻结 2f（全画面顿帧）——\n  let t = frame;\n  for (const h of HITS_REAL) {\n    t -= Math.min(Math.max(frame - h, 0), STOP);\n  }\n\n  const count = HITS_T.filter((h) => t >= h).length; // 当前连击数\n\n  // —— 计数器（右上角 ×N）——\n  let counterScale = 1;\n  let counterRot = 0;\n  let counterSize = 84;\n  if (count > 0) {\n    const i = count - 1;\n    const since = t - HITS_T[i];\n    counterScale = 1 + (PULSE[i] - 1) * Math.exp(-since / 2.4); // 脉冲后指数回落\n    counterRot = TILT[i]; // 倾斜保持，越打越斜\n    counterSize = 84 + i * 14; // 字号逐次加大\n  }\n\n  return (\n    <AbsoluteFill style={{ background: G.bg, overflow: 'hidden' }}>\n      {/* 背景 dashboard 压暗，突出前景连招 */}\n      <div style={{ filter: 'saturate(0.9)', opacity: 0.4 }}>\n        <FakeDashboard variant=\"B\" />\n      </div>\n\n      {/* 三个槽位虚线框（全程可见，建立期先立预期） */}\n      {SLOT_XS.map((x, i) => (\n        <div\n          key={`slot-${i}`}\n          style={{\n            position: 'absolute', left: x, top: SLOT_Y,\n            width: SLOT_W, height: SLOT_H,\n            border: `3px dashed ${G.bar}`, borderRadius: 16,\n            boxSizing: 'border-box',\n          }}\n        />\n      ))}\n\n      {/* 三张功能卡：各隔 30f，命中前 10f ease-in 加速砸落 */}\n      {HITS_T.map((hit, i) => {\n        const y = interpolate(t, [hit - 10, hit], [-520, SLOT_Y], {\n          extrapolateLeft: 'clamp',\n          extrapolateRight: 'clamp',\n          easing: Easing.in(Easing.quad),\n        });\n        if (t < hit - 10) return null;\n        // 触地 3f 内轻微压扁，衬托命中\n        const since = t - hit;\n        const squash = since >= 0 ? 1 - 0.06 * Math.exp(-since / 1.5) : 1;\n        return (\n          <div\n            key={`card-${i}`}\n            style={{\n              position: 'absolute', left: SLOT_XS[i], top: y,\n              transform: `scaleY(${squash})`, transformOrigin: 'bottom center',\n            }}\n          >\n            <Card w={SLOT_W} h={SLOT_H} seed={i + 2} style={{ boxShadow: '0 16px 48px rgba(0,0,0,0.25)' }} />\n          </div>\n        );\n      })}\n\n      {/* 伤害数字：命中帧起 scale 1.4→1、上浮 60px、12f 渐隐 */}\n      {HITS_T.map((hit, i) => {\n        const s = t - hit;\n        if (s < 0 || s > 14) return null;\n        const scale = interpolate(s, [0, 5], [1.4, 1], {\n          extrapolateRight: 'clamp',\n          easing: Easing.out(Easing.quad),\n        });\n        const rise = interpolate(s, [0, 12], [0, -60], {\n          extrapolateRight: 'clamp',\n          easing: Easing.out(Easing.quad),\n        });\n        const opacity = interpolate(s, [0, 4, 12], [1, 1, 0], {\n          extrapolateRight: 'clamp',\n        });\n        return (\n          <div\n            key={`dmg-${i}`}\n            style={{\n              position: 'absolute',\n              left: SLOT_XS[i] + SLOT_W / 2, top: SLOT_Y - 40 + rise,\n              transform: `translateX(-50%) scale(${scale})`,\n              transformOrigin: 'center bottom',\n              opacity,\n            }}\n          >\n            <TitleBlock text={DMG_TEXT[i]} size={72} />\n          </div>\n        );\n      })}\n\n      {/* 右上角连击计数器：跳字 + 递增脉冲 + 递增倾斜 */}\n      <div\n        style={{\n          position: 'absolute', right: 70, top: 50,\n          transform: `scale(${counterScale}) rotate(${counterRot}deg)`,\n          transformOrigin: 'center center',\n          opacity: count === 0 ? 0.25 : 1,\n        }}\n      >\n        <div\n          style={{\n            background: G.ink, color: '#fff', borderRadius: 18,\n            padding: '10px 34px 16px',\n            fontFamily: 'Helvetica, Arial, sans-serif', fontWeight: 800,\n            fontSize: counterSize, letterSpacing: -2, lineHeight: 1.1,\n            boxShadow: '0 8px 28px rgba(0,0,0,0.3)',\n          }}\n        >\n          {`×${count}`}\n        </div>\n      </div>\n    </AbsoluteFill>\n  );\n};\n\n\n"
}