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).
Source: video-shotcraft (Vincent Wei) ↗ · Apache-2.0 · Remotion composition (React/TSX) — preview video, no in-browser player
Variants
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
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.
- Fetch the JSON — it holds the shot recipe (timing, easing, parameter table, pitfalls) and the full TSX source.
- Clone the video-shotcraft repo; the component imports shared fixtures from demos/_fixtures and helpers from assets/lib.
- 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.jsongit 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.mp4claude 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).
// ===== 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>
);
};
- Read the recipePrompt = when to use, intention, parameter table with typical values and how each one feels, known pitfalls. Treat values as calibrated starting points.
- Get the codetool: git
git clone https://github.com/Vincentwei1021/video-shotcraft && cd video-shotcraft && npm install - Register + rendertool: remotion
npx remotion render <CompositionId> out.mp4 # component: demos/effects/impact-feedback/AnimeImpact.tsx, demos/effects/impact-feedback/HitCounter.tsx - Or re-implementPort the timing/easing from the recipe to HTML + WAAPI (gimgs motion contract) or any engine; keep the same segment windows and easing.
| param | type | default | description |
|---|---|---|---|
| — | |||
More in this category

Dark-stage spotlight reveals
Three dark-stage spotlight reveals — A wake-sleep sweep (light in = bright, light out = dark), B edge-hugging glow pan (violet light seeps along UI edges + steady rightward sweep), C corner reveal (radial light expands steadily from a corner to light the whole screen); light itself narrates in the dark.

Slam entrance moves
Three high-energy slam entrances: a whip-in perspective stop, a heavy KPI drop, and a shockwave that hits neighboring cards.

Scanline assemble fly-in
A scan line sweeps down an empty grid, flying each region's component in from off-screen — finishing as the scan ends.

Scanline annotate focus
A bright scan line sweeps down the page, framing each element it passes and labeling it with monospace annotations.