// ===== demos/effects/slam-entrance-moves/ImpactBurstKit.tsx =====
// 落点冲击套件(impact-burst-kit)——shockwave-ring + particle-burst 组合变异。
// 主卡砸落的落点帧同时触发:冲击波环扩散 + 14 粒子放射迸发 + 震屏,
// 且冲击波前沿扫到左右邻卡的那一帧(按半径-距离算准=落点后 3f)邻卡被
// 向外推开再 spring 弹回——"波及邻居"即两条词汇焊接成立的证据。
// 关键帧:0–14 两侧卡驻场、主卡 scale1.8/y-120 悬停 → 14–20 主卡 6f 加速砸落 →
// 20 落点帧:环 80→900px(14f out-cubic, op .75→0) + 14 粒子飞散 160–340px(22f)
// + 4f 震屏 6px 衰减 + 主卡 6f 压扁回弹 → 23 环前沿过邻卡(中心距 460px):
// 邻卡外推 30px + rotate ±3° 阻尼振荡弹回(40f 内钳到 0) → 63–140 全静止(77f)。
import React from 'react';
import { useCurrentFrame, interpolate, Easing } from 'remotion';
import { G, Card, TitleBlock } from '../../_fixtures/Fixtures';
// 伪随机(帧确定)
const h = (n: number): number => {
const s = Math.sin(n * 127.3) * 43758.5453;
return s - Math.floor(s);
};
const CW = 400;
const CH = 280;
const GAP = 60;
const X_L = (1920 - (CW * 3 + GAP * 2)) / 2; // 300
const Y = (1080 - CH) / 2; // 400
const CX = 960; // 主卡中心
const CY = Y + CH / 2; // 540
const IMPACT = 20; // 落点帧
// 冲击波:80→900px 14f out-cubic。前沿到达邻卡中心距 460px 的帧:
// (460-80)/820=0.463 → 1-(1-p)^3 → p≈0.19 → t≈2.6f → 取落点后 3f = 帧 23
const HIT_NEIGHBOR = IMPACT + 3;
// 14 个粒子:方/圆混合,角度带向上偏置,飞散 160–340px 减速缩小消失
const PARTICLES = Array.from({ length: 14 }).map((_, i) => ({
angle: -Math.PI / 2 + (h(i + 1) - 0.5) * Math.PI * 1.7, // 上半球为主
dist: 160 + h(i + 40) * 180,
size: 8 + h(i + 80) * 10,
square: i % 2 === 0,
}));
// 邻卡被推开的阻尼振荡包络:t=0 瞬时到 1,之后余弦衰减弹回,40f 后钳 0 保真静止
const pushEnv = (f: number): number => {
const t = f - HIT_NEIGHBOR;
if (t < 0 || t >= 40) return 0;
return Math.cos(t * 0.5) * Math.exp(-t / 8);
};
export const ImpactBurstKit: React.FC = () => {
const frame = useCurrentFrame();
// ── 主卡砸落:14–20 帧 scale 1.8→1 / y -120→0,加速进场
const dropP = interpolate(frame, [14, IMPACT], [0, 1], {
extrapolateLeft: 'clamp',
extrapolateRight: 'clamp',
easing: Easing.in(Easing.cubic),
});
const mainScale = interpolate(dropP, [0, 1], [1.8, 1]);
const mainDy = interpolate(dropP, [0, 1], [-120, 0]);
// 落点后 6f 压扁回弹(squash & stretch)
const sq = interpolate(frame, [IMPACT, IMPACT + 3, IMPACT + 6], [0, 1, 0], {
extrapolateLeft: 'clamp',
extrapolateRight: 'clamp',
easing: Easing.out(Easing.quad),
});
const mainSx = mainScale * (1 + 0.07 * sq);
const mainSy = mainScale * (1 - 0.1 * sq);
// ── ① 冲击波环:半径 80→900,14f,opacity 0.75→0
const ringP = interpolate(frame, [IMPACT, IMPACT + 14], [0, 1], {
extrapolateLeft: 'clamp',
extrapolateRight: 'clamp',
easing: Easing.out(Easing.cubic),
});
const ringR = interpolate(ringP, [0, 1], [80, 900]);
const ringOp = frame >= IMPACT && frame < IMPACT + 14 ? 0.75 * (1 - ringP) : 0;
// ── ② 粒子:落点起 22f,减速飞散 + 缩小 + 淡出
const pT = interpolate(frame, [IMPACT, IMPACT + 22], [0, 1], {
extrapolateLeft: 'clamp',
extrapolateRight: 'clamp',
easing: Easing.out(Easing.cubic),
});
const particlesAlive = frame >= IMPACT && frame < IMPACT + 22;
// ── ③ 邻卡外推 30px + rotate ±3°,阻尼弹回
const env = pushEnv(frame);
const pushX = 30 * env;
const pushRot = 3 * env;
// ── ④ 震屏:落点起 4f,6px 衰减(h 伪随机方向,帧确定)
let shakeX = 0;
let shakeY = 0;
if (frame >= IMPACT && frame < IMPACT + 4) {
const amp = 6 * (1 - (frame - IMPACT) / 4);
shakeX = (h(frame * 3.7) - 0.5) * 2 * amp;
shakeY = (h(frame * 7.1 + 13) - 0.5) * 2 * amp;
}
return (
{/* 左邻卡:被冲击波推开再弹回 */}
{/* 右邻卡 */}
{/* 主卡:砸落 + 落点压扁回弹 */}
{/* ② 粒子迸发(画在卡之上) */}
{particlesAlive &&
PARTICLES.map((p, i) => {
const px = CX + Math.cos(p.angle) * p.dist * pT;
const py = CY + Math.sin(p.angle) * p.dist * pT;
const s = p.size * (1 - pT);
if (s < 0.5) return null;
return (
);
})}
{/* ① 冲击波环(最上层扫过邻卡) */}
{ringOp > 0 && (
)}
);
};
// ===== demos/effects/slam-entrance-moves/KanadaPerspectiveSnap.tsx =====
// 金田透视急停(kanada-perspective-snap)——金田伊功式夸张透视入场。
// 一张卡片以鱼眼级夸张透视姿态高速甩入画面中心:容器 perspective 300→1500px
// (短焦→长焦,透视畸变随之收敛),卡片 rotate3d(0.5,1,0.1) 58°→0 +
// scale 1.7→1 + translateX -700→0,近角冲出画面感。落定瞬间"啪"地弹平:
// rotateY 过冲 +5° 再 4f 回 0;同时 6px 震屏 2f 衰减,拉长斜影收为正常投影。
// 甩入期整卡叠 blur(2px) 增速感,落定即摘(保证收尾逐帧全同)。
// 关键帧:0–18 透视甩入(out cubic)→ 14–18 rotateY 过冲至 +5° →
// 18–22 回弹归 0 + 震屏衰减 + 阴影收正 → 22–130 全静止(≥45f)。
import React from 'react';
import { useCurrentFrame, interpolate, Easing } from 'remotion';
import { G, Card, TitleBlock } from '../../_fixtures/Fixtures';
// 确定性伪随机(震屏抖动用)
const h = (n: number): number => {
const s = Math.sin(n * 127.3) * 43758.5453;
return s - Math.floor(s);
};
const CLAMP = { extrapolateLeft: 'clamp', extrapolateRight: 'clamp' } as const;
const CARD_W = 520;
const CARD_H = 340;
const CX = (1920 - CARD_W) / 2; // 700
const CY = (1080 - CARD_H) / 2; // 370
export const KanadaPerspectiveSnap: React.FC = () => {
const frame = useCurrentFrame();
// 0–18f 甩入主通道(out cubic:先猛后缓,急停感)
const p = interpolate(frame, [0, 18], [0, 1], { ...CLAMP, easing: Easing.out(Easing.cubic) });
const persp = interpolate(p, [0, 1], [300, 1500]); // 短焦鱼眼→长焦收平
const angle3d = interpolate(p, [0, 1], [58, 0]); // rotate3d(0.5,1,0.1)
const scale = interpolate(p, [0, 1], [1.7, 1]);
const tx = interpolate(p, [0, 1], [-700, 0]);
// rotateY 过冲通道:14–18f 冲到 +5°,18–22f "啪"地回 0
const rotY =
frame < 18
? interpolate(frame, [14, 18], [0, 5], CLAMP)
: interpolate(frame, [18, 22], [5, 0], { ...CLAMP, easing: Easing.out(Easing.cubic) });
// 落定震屏:18f 起 6px,2f 内衰减到 0(21f 后恒为 0,保证真静止)
const shakeAmp = frame >= 18 ? interpolate(frame, [18, 21], [6, 0], CLAMP) : 0;
const shakeX = shakeAmp * (h(frame * 7 + 1) * 2 - 1);
const shakeY = shakeAmp * (h(frame * 13 + 2) * 2 - 1);
// 阴影:飞行期拉长斜影(大偏移大模糊)→ 落定收为正常投影(18–22f 收拢)
const shOff = frame < 18 ? interpolate(p, [0, 1], [1, 0.35]) : interpolate(frame, [18, 22], [0.35, 0], { ...CLAMP, easing: Easing.out(Easing.quad) });
const shX = interpolate(shOff, [0, 1], [0, 70]);
const shY = interpolate(shOff, [0, 1], [8, 52]);
const shBlur = interpolate(shOff, [0, 1], [14, 36]);
const shAlpha = interpolate(shOff, [0, 1], [0.16, 0.3]);
// 甩速 blur:透视扭曲期叠 2px,落定(18f)即摘,收尾无逐帧滤镜
const blur = frame < 18 ? 2 : 0;
return (
{/* 落点槽位(虚线框,给"甩到哪"一个参照) */}
{/* 透视容器:perspective 随落定从鱼眼收敛到长焦 */}
);
};
// ===== demos/effects/slam-entrance-moves/ScoreSlam.tsx =====
// 比分砸落入场(score-slam)——ESPN 比分弹窗 slam。
// KPI 卡从 scale 2.5 / rotate 5° / y-80 高空加速砸落屏心,落点帧同时触发
// 三件套:冲击波描边圆环扩散消散、8 个尘点小方块 seeded 抛物线飞散、
// 整画面震屏指数衰减。砸落带 3% 压缩过冲回弹落定。
// 关键帧:0–8 环境 hold → 8–14 砸落(Easing.in(quad),scale 2.5→0.97,
// rotate 5→0,y -80→0)→ 14 落点帧触发环/尘/震 → 14–22 过冲回弹 0.97→1 →
// 14–28 圆环 60→720px 直径 op 0.7→0 → 14–30 尘点飞散 → 14–19 震屏 18px 衰减
// → 30–135 全静止(≥45f,无逐帧噪声层)。
import React from 'react';
import { useCurrentFrame, interpolate, Easing } from 'remotion';
import { G, FakeDashboard, TitleBlock } from '../../_fixtures/Fixtures';
// 库规伪随机
const h = (n: number) => {
const s = Math.sin(n * 127.3) * 43758.5453;
return s - Math.floor(s);
};
const IMPACT = 14; // 落点帧
const CX = 960;
const CY = 540;
const CARD_W = 460;
const CARD_H = 260;
export const ScoreSlam: React.FC = () => {
const frame = useCurrentFrame();
// —— 卡片砸落:8–14 加速下砸到 0.97(压缩过冲),14–22 回弹到 1 ——
const slamScale =
frame < IMPACT
? interpolate(frame, [8, IMPACT], [2.5, 0.97], {
extrapolateLeft: 'clamp',
extrapolateRight: 'clamp',
easing: Easing.in(Easing.quad),
})
: interpolate(frame, [IMPACT, 22], [0.97, 1], {
extrapolateLeft: 'clamp',
extrapolateRight: 'clamp',
easing: Easing.out(Easing.cubic),
});
const slamRot = interpolate(frame, [8, IMPACT], [5, 0], {
extrapolateLeft: 'clamp',
extrapolateRight: 'clamp',
easing: Easing.in(Easing.quad),
});
const slamY = interpolate(frame, [8, IMPACT], [-80, 0], {
extrapolateLeft: 'clamp',
extrapolateRight: 'clamp',
easing: Easing.in(Easing.quad),
});
const cardOp = interpolate(frame, [8, 11], [0, 1], {
extrapolateLeft: 'clamp',
extrapolateRight: 'clamp',
});
// —— 震屏:落点帧起 5f,18px 指数衰减,seeded 方向,19f 后严格归零 ——
let shakeX = 0;
let shakeY = 0;
if (frame >= IMPACT && frame < IMPACT + 5) {
const t = frame - IMPACT;
const amp = 18 * Math.exp(-t * 0.9);
shakeX = amp * (h(frame * 7 + 1) * 2 - 1);
shakeY = amp * (h(frame * 13 + 2) * 2 - 1);
}
// —— 冲击波圆环:14f 内直径 60→720,opacity 0.7→0 ——
const ringT = interpolate(frame, [IMPACT, IMPACT + 14], [0, 1], {
extrapolateLeft: 'clamp',
extrapolateRight: 'clamp',
easing: Easing.out(Easing.cubic),
});
const ringD = interpolate(ringT, [0, 1], [60, 860]);
// 不透明度走线性帧时间(与扩散的 out-cubic 解耦),扩到最大前都保持可见
const ringTLin = interpolate(frame, [IMPACT, IMPACT + 14], [0, 1], {
extrapolateLeft: 'clamp',
extrapolateRight: 'clamp',
});
const ringOp = interpolate(ringTLin, [0, 0.65, 1], [0.75, 0.55, 0]);
const ringOn = frame >= IMPACT && frame < IMPACT + 14;
// —— 8 个尘点:seeded 角度抛物线飞散 120–260px,16f 减速缩小消失 ——
const dustT = interpolate(frame, [IMPACT, IMPACT + 16], [0, 1], {
extrapolateLeft: 'clamp',
extrapolateRight: 'clamp',
easing: Easing.out(Easing.cubic),
});
const dustTLin = interpolate(frame, [IMPACT, IMPACT + 16], [0, 1], {
extrapolateLeft: 'clamp',
extrapolateRight: 'clamp',
});
const dustOn = frame >= IMPACT && frame < IMPACT + 16;
return (
{/* 震屏容器:整画面一起抖 */}
{/* 虚化 dashboard 环境底 */}
{/* ② 尘点飞散 */}
{dustOn &&
Array.from({ length: 8 }).map((_, i) => {
const ang = (i / 8) * Math.PI * 2 + (h(i + 3) - 0.5) * 0.7;
const dist = 160 + h(i + 11) * 160; // 160–320px
const size = 18 + h(i + 23) * 12; // 18–30px
const dx = Math.cos(ang) * dist * dustT;
// 抛物线:先随角度飞出,再叠加重力下坠
const dy = Math.sin(ang) * dist * dustT + 90 * dustT * dustT;
const s = size * (1 - 0.75 * dustTLin);
const op = interpolate(dustTLin, [0, 0.75, 1], [0.9, 0.7, 0]);
return (
);
})}
{/* ① 冲击波圆环 */}
{ringOn && (
)}
{/* KPI 卡本体 */}
{frame >= 8 && (
)}
);
};