Motion · Light & emphasis

Impact feedback

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).

1920×108016:930 fps5.06s · 152 framesremotionLicense: Apache-2.0#shotcraft#remotion#effects

Source: video-shotcraft (Vincent Wei) ↗ · Apache-2.0 · Remotion composition (React/TSX) — preview video, no in-browser player

Variants

hit-counterhit-counter · Combo hit counter
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.
A rapid-fire feature-point segment — tell "there are lots of features" as a combo; heavy gamified language
anime-impactanime-impact · Anime impact frame
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.
The one punch that is the film's single climax; the top of the emphasis scale

When to use

The "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.

Intention

When 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.

Duration & energy

  • Duration: n/a (an element-level technique that piggybacks on a landing action; each style's frame budget is in its parameter table)
  • Energy: High (an instant impact)

Shot recipe (original Chinese — timing, easing, parameter table, known pitfalls)

意图

元素砸到位却没有"砸到了"的一瞬,落位读作 PPT 飞入。游戏 juice (Vlambeer screenshake 理论)与动漫演出 impact frames 给了同一答案: 命中的那几帧把时间咬住,冲量才传到观众手上。两式是一条强度阶梯: B 用局部顿帧叠伤害数字和 combo 跳字,把功能清单剪成一套连招, 叙事感最强;C 是最重的一拳——撞停 3 帧翻负片炸集中线,强调级最高、 最风格化。

两式选型表

| 式 | 做法 | 适用 | |----|------|------| | B hit-counter 连招计数 | 三卡接连砸入,每命中 = 顿帧 2f + 伤害数字上浮 + ×N 计数跳字逐次加码 | 功能点连打段——"功能很多"用连招讲;游戏化语言浓 | | C anime-impact 打击帧 | crash-zoom 撞停的 3f 整幅负片反色 + 放射集中线 + 红青色散,第 4f 全撤 | 全片唯一高潮的那一拳;强调级封顶 |

参数表

| 参数 | 典型值 | 调节手感 | |------|--------|----------| | B 节奏 | 三卡各隔 30f,命中前 10f ease-in(quad) 砸落;每命中全局冻结 2f | 间隔均匀才有"连招"节拍感 | | B 伤害数字 | 落点 scale 1.4→1 + 上浮 40–60px + 12f 渐隐 | 渐隐 >16f 拖泥带水,遮住下一击 | | B 计数器 | 脉冲峰值 1.3/1.45/1.6、rotate −2/−4/−6°、字号递增,exp(−t/2.4) 回落 | 逐次加码是命门——三次一样大读不出"越打越狠" | | C 急推撞停 | crash-zoom 6f ease-in(cubic) 推到 2.4x,卡心收敛到画面正中 | 急推参数走 crash-zoom-punch 卡 | | C 冲击帧 | 撞停起 3f:invert(1) grayscale(1) + 30 根楔形集中线(每 1–2f 换形态)+ 红/青负片副本 screen 叠加 ±8px 错位;第 4f 全撤 | 3f 是甜点——4f 起观众开始"看清"负片,魔术就穿帮 | | C 恢复震屏 | 第 4f 起 6px 指数衰减(τ≈2.2f),~12f 收干 | 幅度须过肉眼阈值(可感性判例) |

已知坑

  • demo 在灰阶/占位素材上调校通过——参数是调校起点非实战定稿,

首次实战须以真实素材回验

  • B 式顿帧必须全局——背景滚动也要一起冻,否则读不出"时间被

咬住",只像那张卡掉帧。demo 用统一 remap 帧 df 驱动所有层是命门; 唯闪白等"冻结期间也要发生"的效果用真实帧 f 驱动

  • 帧 remap 与 speed-ramp-freeze 同技术根,同一镜头别叠用两卡——

两套 remap 互相打架,节奏读作故障

  • C 式负片帧在纸墨审美的片子里非常抢戏——限高潮一次,且必须与

impact 音效同帧落地(拟音走 sound-design),无声的负片帧读作闪坏

  • B 式游戏化语言浓(伤害数字/combo 是明晃晃的游戏梗),产品调性

偏严肃时慎用

  • B 式多次顿帧后真实帧比动画时间多出累计冻结帧,排镜头预算时

按 真实命中帧 = 动画命中帧 + 已冻结帧数 折算(demo HITS_REAL)

参考实现

demos/effects/impact-feedback/ (AnimeImpact.tsx / HitCounter.tsx)

Reference implementation (Remotion)

  • demos/effects/impact-feedback/AnimeImpact.tsx — https://raw.githubusercontent.com/Vincentwei1021/video-shotcraft/main/demos/effects/impact-feedback/AnimeImpact.tsx
  • demos/effects/impact-feedback/HitCounter.tsx — https://raw.githubusercontent.com/Vincentwei1021/video-shotcraft/main/demos/effects/impact-feedback/HitCounter.tsx

The component imports shared fixtures from demos/_fixtures and helpers from assets/lib in the repo; SKILL.md there documents the render workflow.

Everything on this page is also machine-readable. Point your agent at the JSON, or add the gimgs MCP server and ask it to search motion templates.

  1. Fetch the JSON — it holds the shot recipe (timing, easing, parameter table, pitfalls) and the full TSX source.
  2. Clone the video-shotcraft repo; the component imports shared fixtures from demos/_fixtures and helpers from assets/lib.
  3. Register the component as a <Composition> in a Remotion project and render with `npx remotion render`, or re-implement the recipe in your own engine.
curl -s https://gimgs.net/motion/impact-feedback.json
git clone https://github.com/Vincentwei1021/video-shotcraft
# component: demos/effects/impact-feedback/AnimeImpact.tsx, demos/effects/impact-feedback/HitCounter.tsx
npx remotion render <CompositionId> out.mp4
claude mcp add --transport http gimgs https://gimgs.net/mcp
# then: search_motion_templates / get_motion_template("impact-feedback")

Prompt

Use the video-shotcraft shot recipe "Impact feedback" (Remotion composition, Apache-2.0) published on gimgs.net.
Recipe JSON (prompt = timing/easing/parameter table/pitfalls, files = TSX source): https://gimgs.net/motion/impact-feedback.json
Raw TSX: https://gimgs.net/motion/impact-feedback/template.html
Repository: https://github.com/Vincentwei1021/video-shotcraft  (component: demos/effects/impact-feedback/AnimeImpact.tsx, demos/effects/impact-feedback/HitCounter.tsx; imports demos/_fixtures and assets/lib)
Spec: 1920x1080, 30 fps, 5.06s

1. Read the JSON and follow its "prompt" section (when to use, intention, parameter table, known pitfalls).
2. Adapt it for: <describe what you want>.
3. Either clone the repo, register the component as a <Composition> in a Remotion project and run npx remotion render <CompositionId> out.mp4 — or re-implement the recipe in your own engine (HTML/WAAPI works: keep the same timing and easing).

JSON · Markdown · llms.txt

9,854 chars · Apache-2.0Open raw template ↗
// ===== demos/effects/impact-feedback/AnimeImpact.tsx =====
import React from 'react';
import { AbsoluteFill, Easing, interpolate, useCurrentFrame } from 'remotion';
import { FakeDashboard, Card, TitleBlock, G } from '../../_fixtures/Fixtures';

// anime-impact 动漫打击帧〔组合〕:crash-zoom 急推撞停在目标卡上的那 3 帧,
// 整幅画面反转成黑白负片 + 手绘放射集中线 + 红青通道 ±8px 色散——
// 像素被打了一拳,第 4 帧一切撤掉恢复干净特写 + 6px 震屏衰减。
// 节拍:0–24 建立全景 hold → 24–30 急推(6f ease-in 到 2.4x) →
//       30–32 冲击帧(3f 负片/集中线/RGB split,每帧换形态) →
//       33 起恢复特写 + 震屏衰减 → ~45–120 静止读卡。

const ZOOM_START = 24;
const ZOOM_END = 30; // 撞停帧
const IMPACT_LEN = 3; // 冲击帧持续 3f(30/31/32)
const RECOVER = ZOOM_END + IMPACT_LEN; // 33:恢复干净特写

// 目标卡:盖在 3x2 网格中排第 2 格上(列 2 行 1)
const CARD = { x: 808, y: 108, w: 524, h: 454 };
const CX = CARD.x + CARD.w / 2; // 1070
const CY = CARD.y + CARD.h / 2; // 335
const SCALE_END = 2.4;

// seed 正弦哈希(禁 Math.random)
const rnd = (i: number) => {
  const s = Math.sin(i * 127.3) * 43758.5453;
  return s - Math.floor(s);
};

// 手绘放射集中线:30 根从卡片周缘指向画框外的尖楔形,phase 换一次形态
const SpeedLines: React.FC<{ phase: number }> = ({ phase }) => {
  const cx = 960;
  const cy = 540;
  const R_OUT = 1300; // 超出画框对角(~1101)
  const polys = Array.from({ length: 30 }).map((_, i) => {
    const k = i * 13 + phase * 101;
    const ang = ((i + 0.5) / 30) * Math.PI * 2 + (rnd(k) - 0.5) * 0.22;
    const r0 = 300 + rnd(k + 1) * 220; // 内端长短随机(卡片周缘一带)
    const halfW = (7 + rnd(k + 2) * 16) / R_OUT; // 外端宽 7–23px 的楔形
    const ax = cx + Math.cos(ang) * r0;
    const ay = cy + Math.sin(ang) * r0;
    const b1x = cx + Math.cos(ang - halfW) * R_OUT;
    const b1y = cy + Math.sin(ang - halfW) * R_OUT;
    const b2x = cx + Math.cos(ang + halfW) * R_OUT;
    const b2y = cy + Math.sin(ang + halfW) * R_OUT;
    return `${ax},${ay} ${b1x},${b1y} ${b2x},${b2y}`;
  });
  return (
    <svg
      viewBox="0 0 1920 1080"
      style={{ position: 'absolute', inset: 0, width: '100%', height: '100%' }}
    >
      {polys.map((pts, i) => (
        <polygon key={i} points={pts} fill={i % 4 === 0 ? '#111111' : '#f5f5f5'} />
      ))}
    </svg>
  );
};

// 目标卡叠层 + 全景底
const Scene: React.FC = () => (
  <>
    <FakeDashboard variant="A" />
    <div style={{ position: 'absolute', left: CARD.x, top: CARD.y }}>
      <Card
        w={CARD.w}
        h={CARD.h}
        seed={9}
        style={{ boxShadow: '0 10px 36px rgba(0,0,0,0.18)', border: `3px solid ${G.ink}` }}
      />
      <div style={{ position: 'absolute', left: 24, bottom: 96 }}>
        <TitleBlock text="IMPACT" size={92} />
      </div>
    </div>
  </>
);

export const AnimeImpact: React.FC = () => {
  const frame = useCurrentFrame();

  // 急推:6f ease-in 撞到 2.4x,卡心推到画面正中
  const p = interpolate(frame, [ZOOM_START, ZOOM_END], [0, 1], {
    extrapolateLeft: 'clamp',
    extrapolateRight: 'clamp',
    easing: Easing.in(Easing.cubic),
  });
  const scale = 1 + p * (SCALE_END - 1);
  const tx = (960 - CX) * p;
  const ty = (540 - CY) * p;

  const impact = frame >= ZOOM_END && frame < RECOVER;
  const phase = impact ? frame - ZOOM_END : 0; // 每 1f 换一次集中线形态

  // 撞停后震屏:6px 起步、指数衰减,~12f 收干后真静止
  const since = frame - RECOVER;
  const env = since >= 0 ? 6 * Math.exp(-since / 2.2) : 0;
  const shakeX = env * Math.sin(since * 3.7);
  const shakeY = env * 0.7 * Math.sin(since * 5.1 + 0.9);

  const zoomStyle: React.CSSProperties = {
    position: 'absolute',
    inset: 0,
    transform: `translate(${tx}px, ${ty}px) scale(${scale})`,
    transformOrigin: `${CX}px ${CY}px`,
  };

  return (
    <AbsoluteFill style={{ background: impact ? '#131315' : G.bg, overflow: 'hidden' }}>
      <div style={{ position: 'absolute', inset: 0, transform: `translate(${shakeX}px, ${shakeY}px)` }}>
        {/* 主层:冲击帧期间整幅负片黑白 */}
        <div style={{ ...zoomStyle, filter: impact ? 'invert(1) grayscale(1)' : 'none' }}>
          <Scene />
        </div>
        {/* RGB split:红/青双层负片副本,screen 叠底、±8px 错位(随 phase 抖动) */}
        {impact && (
          <>
            <div
              style={{
                position: 'absolute',
                inset: 0,
                mixBlendMode: 'screen',
                transform: `translate(-8px, ${phase % 2 === 0 ? 4 : -4}px)`,
              }}
            >
              <div style={{ ...zoomStyle, filter: 'invert(1) grayscale(1)' }}>
                <Scene />
              </div>
              <div style={{ position: 'absolute', inset: 0, background: '#ff0033', mixBlendMode: 'multiply' }} />
            </div>
            <div
              style={{
                position: 'absolute',
                inset: 0,
                mixBlendMode: 'screen',
                transform: `translate(8px, ${phase % 2 === 0 ? -4 : 4}px)`,
              }}
            >
              <div style={{ ...zoomStyle, filter: 'invert(1) grayscale(1)' }}>
                <Scene />
              </div>
              <div style={{ position: 'absolute', inset: 0, background: '#00e5ff', mixBlendMode: 'multiply' }} />
            </div>
            {/* 手绘放射集中线:每帧换形态 */}
            <SpeedLines phase={phase} />
          </>
        )}
      </div>
    </AbsoluteFill>
  );
};


// ===== demos/effects/impact-feedback/HitCounter.tsx =====
import React from 'react';
import { AbsoluteFill, Easing, interpolate, useCurrentFrame } from 'remotion';
import { FakeDashboard, Card, TitleBlock, G } from '../../_fixtures/Fixtures';

// hit-counter 连招计数:三张功能卡接连砸入槽位,每次命中 = 全局顿帧 2f
// + 落点伤害数字上浮渐隐 + 右上角 ×N 计数跳字(脉冲/倾斜逐次加码)。
// 组合:hitstop + damage-number-pop + combo-counter。

// —— 时间结构(动画时间 t 空间)——
// 0–19 建立 hold;卡 i 于 t = HIT-10 → HIT ease-in 砸落;HIT = 30/60/90;
// 每命中后真实帧多出 2f 顿帧,末卡效果收完后静止 hold 到 150。
const HITS_T = [30, 60, 90]; // 命中时刻(重映射后的动画时间)
const STOP = 2; // 每次顿帧帧数
// 真实命中帧 = 动画命中时刻 + 前面累计的顿帧
const HITS_REAL = HITS_T.map((h, i) => h + i * STOP);

const SLOT_W = 420;
const SLOT_H = 300;
const SLOT_Y = 430;
const SLOT_XS = [390, 890, 1390].map((cx) => cx - SLOT_W / 2);

const DMG_TEXT = ['+1.2k', '+2.4k', '+4.8k'];
const PULSE = [1.3, 1.45, 1.6]; // 计数器脉冲峰值,逐次递增
const TILT = [-2, -4, -6]; // 计数器倾斜,逐次递增并保持

export const HitCounter: React.FC = () => {
  const frame = useCurrentFrame();

  // —— 全局帧 remap:每个真实命中帧起冻结 2f(全画面顿帧)——
  let t = frame;
  for (const h of HITS_REAL) {
    t -= Math.min(Math.max(frame - h, 0), STOP);
  }

  const count = HITS_T.filter((h) => t >= h).length; // 当前连击数

  // —— 计数器(右上角 ×N)——
  let counterScale = 1;
  let counterRot = 0;
  let counterSize = 84;
  if (count > 0) {
    const i = count - 1;
    const since = t - HITS_T[i];
    counterScale = 1 + (PULSE[i] - 1) * Math.exp(-since / 2.4); // 脉冲后指数回落
    counterRot = TILT[i]; // 倾斜保持,越打越斜
    counterSize = 84 + i * 14; // 字号逐次加大
  }

  return (
    <AbsoluteFill style={{ background: G.bg, overflow: 'hidden' }}>
      {/* 背景 dashboard 压暗,突出前景连招 */}
      <div style={{ filter: 'saturate(0.9)', opacity: 0.4 }}>
        <FakeDashboard variant="B" />
      </div>

      {/* 三个槽位虚线框(全程可见,建立期先立预期) */}
      {SLOT_XS.map((x, i) => (
        <div
          key={`slot-${i}`}
          style={{
            position: 'absolute', left: x, top: SLOT_Y,
            width: SLOT_W, height: SLOT_H,
            border: `3px dashed ${G.bar}`, borderRadius: 16,
            boxSizing: 'border-box',
          }}
        />
      ))}

      {/* 三张功能卡:各隔 30f,命中前 10f ease-in 加速砸落 */}
      {HITS_T.map((hit, i) => {
        const y = interpolate(t, [hit - 10, hit], [-520, SLOT_Y], {
          extrapolateLeft: 'clamp',
          extrapolateRight: 'clamp',
          easing: Easing.in(Easing.quad),
        });
        if (t < hit - 10) return null;
        // 触地 3f 内轻微压扁,衬托命中
        const since = t - hit;
        const squash = since >= 0 ? 1 - 0.06 * Math.exp(-since / 1.5) : 1;
        return (
          <div
            key={`card-${i}`}
            style={{
              position: 'absolute', left: SLOT_XS[i], top: y,
              transform: `scaleY(${squash})`, transformOrigin: 'bottom center',
            }}
          >
            <Card w={SLOT_W} h={SLOT_H} seed={i + 2} style={{ boxShadow: '0 16px 48px rgba(0,0,0,0.25)' }} />
          </div>
        );
      })}

      {/* 伤害数字:命中帧起 scale 1.4→1、上浮 60px、12f 渐隐 */}
      {HITS_T.map((hit, i) => {
        const s = t - hit;
        if (s < 0 || s > 14) return null;
        const scale = interpolate(s, [0, 5], [1.4, 1], {
          extrapolateRight: 'clamp',
          easing: Easing.out(Easing.quad),
        });
        const rise = interpolate(s, [0, 12], [0, -60], {
          extrapolateRight: 'clamp',
          easing: Easing.out(Easing.quad),
        });
        const opacity = interpolate(s, [0, 4, 12], [1, 1, 0], {
          extrapolateRight: 'clamp',
        });
        return (
          <div
            key={`dmg-${i}`}
            style={{
              position: 'absolute',
              left: SLOT_XS[i] + SLOT_W / 2, top: SLOT_Y - 40 + rise,
              transform: `translateX(-50%) scale(${scale})`,
              transformOrigin: 'center bottom',
              opacity,
            }}
          >
            <TitleBlock text={DMG_TEXT[i]} size={72} />
          </div>
        );
      })}

      {/* 右上角连击计数器:跳字 + 递增脉冲 + 递增倾斜 */}
      <div
        style={{
          position: 'absolute', right: 70, top: 50,
          transform: `scale(${counterScale}) rotate(${counterRot}deg)`,
          transformOrigin: 'center center',
          opacity: count === 0 ? 0.25 : 1,
        }}
      >
        <div
          style={{
            background: G.ink, color: '#fff', borderRadius: 18,
            padding: '10px 34px 16px',
            fontFamily: 'Helvetica, Arial, sans-serif', fontWeight: 800,
            fontSize: counterSize, letterSpacing: -2, lineHeight: 1.1,
            boxShadow: '0 8px 28px rgba(0,0,0,0.3)',
          }}
        >
          {`×${count}`}
        </div>
      </div>
    </AbsoluteFill>
  );
};


  1. Read the recipe
    Prompt = when to use, intention, parameter table with typical values and how each one feels, known pitfalls. Treat values as calibrated starting points.
  2. Get the code
    tool: git
    git clone https://github.com/Vincentwei1021/video-shotcraft && cd video-shotcraft && npm install
  3. Register + render
    tool: remotion
    npx remotion render <CompositionId> out.mp4 # component: demos/effects/impact-feedback/AnimeImpact.tsx, demos/effects/impact-feedback/HitCounter.tsx
  4. Or re-implement
    Port the timing/easing from the recipe to HTML + WAAPI (gimgs motion contract) or any engine; keep the same segment windows and easing.
paramtypedefaultdescription
—

More in this category