Motion · Ánh sáng & nhấn
Phản hồi va chạm
Hai kiểu phản hồi va chạm: hit-counter — đếm combo (khựng khung hình + số sát thương + chữ combo bật lên), và anime-impact — khung hình đánh trúng kiểu anime (đảo âm bản + tia hội tụ + tán sắc màu).
Nguồn: video-shotcraft (Vincent Wei) ↗ · Apache-2.0 · Composition Remotion (React/TSX) — có video preview, không có player trong trình duyệt
Biến thể
Ba thẻ lần lượt đập vào; mỗi lần trúng = khựng khung hình 2f + số sát thương nổi lên + chữ đếm ×N tăng dần theo từng lần.
Đoạn liệt kê tính năng dồn dập — kể "nhiều tính năng" bằng combo; ngôn ngữ game hóa đậm
Khi crash-zoom va dừng, 3 khung hình đảo âm bản toàn khung + tia hội tụ tỏa ra + tán sắc đỏ-lục lam, đến khung hình thứ 4 rút hết.
Cú đấm duy nhất là cao trào của cả phim; đỉnh của thang nhấn mạnh
Khi nào dùng
Khoảnh khắc "trúng đích" khi phần tử đáp xuống hoặc va chạm dừng lại — cho cú đập vào/va dừng cảm giác phản hồi cấp độ game; chọn kiểu theo thang cường độ.
Ý đồ
Phần tử đập vào đúng vị trí mà không có khoảnh khắc "đã trúng" thì việc đặt vào chỉ đọc như bay vào kiểu PPT. Cả lý thuyết juice trong game (screenshake của Vlambeer) lẫn khung hình impact trong diễn xuất anime đều cho cùng một đáp án: những khung hình ngay lúc va chạm cắn chặt thời gian lại, xung lực nhờ đó mới truyền tới tay khán giả. Hai kiểu tạo thành một thang cường độ: B dùng khựng khung hình cục bộ kèm số sát thương chồng lên và chữ combo bật lên, cắt danh sách tính năng thành một chuỗi combo, cảm giác kể chuyện mạnh nhất; C là cú đấm nặng nhất — đóng băng 3 khung hình đảo thành âm bản kèm tia hội tụ tỏa ra, mức cường độ cao nhất, phong cách hóa nhất.
Thời lượng & năng lượng
- Thời lượng: n/a (kỹ thuật cấp phần tử, bám vào động tác đáp xuống; số khung hình của từng kiểu xem bảng tham số)
- Năng lượng: Cao (va chạm tức thời)
Công thức cảnh quay (bản gốc tiếng Trung — timing, easing, bảng tham số, lỗi hay gặp)
意图
元素砸到位却没有"砸到了"的一瞬,落位读作 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)
Mã nguồn tham chiếu (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
Component import fixture chung từ demos/_fixtures và helper từ assets/lib của repo; xem SKILL.md trong repo để biết quy trình render.
Mọi thứ trên trang này đều có bản máy đọc. Đưa agent link JSON, hoặc thêm MCP server gimgs rồi bảo nó tìm motion template.
- Tải JSON — trong đó có công thức cảnh quay (timing, easing, bảng tham số, lỗi hay gặp) và toàn bộ source TSX.
- Clone repo video-shotcraft; component import fixture chung từ demos/_fixtures và helper từ assets/lib.
- Đăng ký component thành <Composition> trong project Remotion rồi render bằng `npx remotion render`, hoặc dựng lại công thức bằng engine của bạn.
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 "Phản hồi va chạm" (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 |
|---|---|---|---|
| — | |||
Cùng danh mục

Đèn rọi hiện hình trên nền tối
Ba kiểu đèn rọi hiện hình trên nền tối — A quét thức-ngủ (đèn tới thì sáng, đèn đi thì tối), B quét ngang rìa viền (ánh tím bám mép UI thấm vào + đèn quét phải đều), C hiện hình đều từ góc (đèn hình tròn nở đều từ góc sáng cả màn hình); ánh sáng chính là lời kể trên nền đen.

Đập vào cảnh năng lượng cao
Ba kiểu đập vào cảnh năng lượng cao: dừng phối cảnh kiểu Kanada, đập rơi tỷ số, hoặc lan chấn động sang thẻ lân cận.

Vệt sáng quét lắp ráp bay vào
Vệt sáng quét từ trên xuống lưới nền tối trống, tới đâu thành phần vùng đó bay vào khớp vị trí — quét xong là lắp ráp xong.

Vệt sáng quét chú thích tiêu điểm
Một vệt sáng quét từ trên xuống dưới trang, khoanh khung ngắm kiểu máy ảnh vào từng vùng nó đi qua rồi gắn chú thích chữ mono.