// ===== 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 ( {polys.map((pts, i) => ( ))} ); }; // 目标卡叠层 + 全景底 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 (
); })} {/* 右上角连击计数器:跳字 + 递增脉冲 + 递增倾斜 */}
{`×${count}`}
); };