// ===== 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 (
);
};
// 目标卡叠层 + 全景底
const Scene: React.FC = () => (
<>
>
);
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 (
{/* 主层:冲击帧期间整幅负片黑白 */}
{/* RGB split:红/青双层负片副本,screen 叠底、±8px 错位(随 phase 抖动) */}
{impact && (
<>
{/* 手绘放射集中线:每帧换形态 */}
>
)}
);
};
// ===== 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 (
{/* 背景 dashboard 压暗,突出前景连招 */}
{/* 三个槽位虚线框(全程可见,建立期先立预期) */}
{SLOT_XS.map((x, i) => (
))}
{/* 三张功能卡:各隔 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 (
);
})}
{/* 伤害数字:命中帧起 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 (
);
})}
{/* 右上角连击计数器:跳字 + 递增脉冲 + 递增倾斜 */}
);
};