// keycap-smash-cut|键帽引信+猛切句号(keycap-press-trigger × smash-cut)
// 0–25f 浅底中央 3D 键帽(⌘⏎)悬浮呼吸 → 25–28f 砰按下(3f 下沉 14px +
// 底座 12→3px 压扁 + 键面变暗)+ 底部亮环(扩散 out-cubic / 消散线性解耦)
// → 按下即引爆轰鸣段 28–58f:5 张卡 + 1 面板从四面八方高速飞入冲镜
// (ease-in 全程加速 + scale 1.5→3 冲脸 + 速度门控 blur),背景整体
// ease-in 推近 1→1.3 + 滚动 1.5°——切点前一刻动势最猛 → 58f 一帧猛切:
// FakeDashboard variant="A" 整齐静止全景,键帽已缩成小元素稳稳嵌在
// 顶栏角落(引信呼应),死寂 82f(>50f)。总 140f。
import React from 'react';
import { useCurrentFrame, interpolate, Easing } from 'remotion';
import { G, Card, FakeDashboard } from '../../_fixtures/Fixtures';
const T = {
press: 25, // 按下起始(呼吸周期 25f,此帧位移恰好归零)
pressEnd: 28, // 3f 压到底,同帧引爆轰鸣段
ringGrowEnd: 46, // 亮环扩散结束(out-cubic)
ringFadeEnd: 52, // 亮环消散结束(线性,帧时间与扩散解耦)
cut: 58, // 猛切帧:>=58 全静止
total: 140, // 死寂 82f
};
// —— 键帽(复用 keycap 规格:多层 box-shadow 造厚度)——
const Keycap: React.FC<{ size: number; thick: number; bright: number; label?: string }> = ({
size, thick, bright, label = '⌘⏎',
}) => (
);
// —— 轰鸣段飞入物:5 张卡 + 1 面板,四面八方冲镜 ——
type Fly = {
from: [number, number];
to: [number, number];
rot: [number, number];
seed: number;
w: number;
h: number;
panel?: boolean;
};
const FLIES: Fly[] = [
{ from: [-1400, -140], to: [1400, 80], rot: [-6, 5], seed: 1, w: 440, h: 290 },
{ from: [1400, 200], to: [-1400, -120], rot: [7, -4], seed: 2, w: 400, h: 260 },
{ from: [-260, -900], to: [180, 900], rot: [-3, 8], seed: 3, w: 460, h: 300 },
{ from: [-1300, 820], to: [1300, -760], rot: [5, -7], seed: 4, w: 420, h: 280 },
{ from: [1350, -800], to: [-1350, 840], rot: [-8, 4], seed: 5, w: 480, h: 310 },
// 第 6 位:面板(更大更重,斜穿)
{ from: [1500, 700], to: [-1500, -600], rot: [4, -6], seed: 6, w: 700, h: 430, panel: true },
];
// 卡 i 的第 k 轮:start = 28 + i*3 + k*16,时长 15 / 11(第二轮更快=整体加速)。
// i=3 k=1 的 53–64f 在切点 58f 被硬切截断——绝不减速迎接切点。
const passWindow = (i: number, k: number): [number, number] => {
const start = T.pressEnd + i * 3 + k * 16;
const dur = k === 0 ? 15 : 11;
return [start, start + dur];
};
// 假面板飞入物:命令面板式灰阶块
const FlyPanel: React.FC<{ w: number; h: number }> = ({ w, h }) => (
{Array.from({ length: 3 }).map((_, i) => (
))}
);
const FlyItem: React.FC<{ fly: Fly; i: number; frame: number }> = ({ fly, i, frame }) => {
let active: [number, number] | null = null;
for (let k = 0; k < 2; k++) {
const [s, e] = passWindow(i, k);
if (frame >= s && frame < e) { active = [s, e]; break; }
}
if (!active) return null;
const [s, e] = active;
// ease-in:全程加速,越近终点越快
const p = interpolate(frame, [s, e], [0, 1], {
extrapolateLeft: 'clamp', extrapolateRight: 'clamp',
easing: Easing.in(Easing.quad),
});
const x = fly.from[0] + (fly.to[0] - fly.from[0]) * p;
const y = fly.from[1] + (fly.to[1] - fly.from[1]) * p;
const rot = fly.rot[0] + (fly.rot[1] - fly.rot[0]) * p;
const scale = 1.5 + 1.5 * p; // 冲脸
const blur = 1 + 4 * p; // 速度门控方向糊感(ease-in 下速度 ∝ p)
return (
{fly.panel
?
: }
);
};
export const KeycapSmashCut: React.FC = () => {
const frame = useCurrentFrame();
// —— 死寂段:58f 起 variant A 整齐静止全景 + 键帽小元素嵌顶栏角落,
// 无任何随帧变化的属性 ——
if (frame >= T.cut) {
return (
{/* 键帽句号:稳稳嵌在顶栏(标题条右侧空档),呼应引信 */}
);
}
// —— 引信段键帽运动 ——
// 呼吸:0–25f 一个完整正弦周期(±5px),f25 位移归零接按下
const breathY = frame < T.press ? -5 * Math.sin((2 * Math.PI * frame) / T.press) : 0;
// 按下:3f 下沉 14px,之后保持压死
const pressY = interpolate(frame, [T.press, T.pressEnd], [0, 14], {
extrapolateLeft: 'clamp', extrapolateRight: 'clamp',
});
// 底座厚度 12→3px 压扁(灵魂),轰鸣段保持压扁
const thick = interpolate(frame, [T.press, T.pressEnd], [12, 3], {
extrapolateLeft: 'clamp', extrapolateRight: 'clamp',
});
const bright = interpolate(frame, [T.press, T.pressEnd], [1, 0.9], {
extrapolateLeft: 'clamp', extrapolateRight: 'clamp',
});
// —— 亮环:扩散 out-cubic 90→640px,消散线性,帧时间解耦,摘罩=条件挂载 ——
const ringOn = frame >= T.press && frame < T.ringFadeEnd;
const ringD = interpolate(frame, [T.press, T.ringGrowEnd], [90, 640], {
extrapolateLeft: 'clamp', extrapolateRight: 'clamp',
easing: Easing.out(Easing.cubic),
});
const ringA = interpolate(frame, [T.press, T.ringFadeEnd], [0.9, 0], {
extrapolateLeft: 'clamp', extrapolateRight: 'clamp',
});
const ringBorder = interpolate(frame, [T.press, T.ringGrowEnd], [14, 3], {
extrapolateLeft: 'clamp', extrapolateRight: 'clamp',
easing: Easing.out(Easing.cubic),
});
// —— 轰鸣段背景动势:整体 ease-in 推近 + 滚动,切点前仍在加速 ——
const bgScale = interpolate(frame, [T.pressEnd, T.cut], [1, 1.3], {
extrapolateLeft: 'clamp', extrapolateRight: 'clamp',
easing: Easing.in(Easing.quad),
});
const bgRot = interpolate(frame, [T.pressEnd, T.cut], [0, 1.5], {
extrapolateLeft: 'clamp', extrapolateRight: 'clamp',
easing: Easing.in(Easing.quad),
});
return (
{/* 背景层(浅底 + 键帽 + 亮环),轰鸣段整体加速推近 */}
{/* 轰鸣段飞入物 */}
{frame >= T.pressEnd && FLIES.map((fly, i) => (
))}
);
};