import React from 'react'; import { AbsoluteFill, Easing, interpolate, useCurrentFrame } from 'remotion'; import { Card, G } from '../../_fixtures/Fixtures'; // exploded-view:整页 dashboard 带 3D 倾斜,咔地沿 Z 轴炸开——顶栏/侧栏/六卡 // 各自浮到不同深度悬停(近大而实、远略暗),层间透出投影;hold 一拍后 // 逆序咔哒合体,2f 震屏收口。 // // 节拍:0–24 静止建立(已倾斜)→ 24–59 错峰炸开(每层 14f ease-out-back) // → 悬停 → 90–123 逆序合体(每层 12f ease-in)→ 123 震屏 → 静止到 150 const EXPLODE = 24; // 炸开起始帧 const ASSEMBLE = 90; // 合体起始帧 const STAGGER = 3; // 层间错峰 const N = 8; // 可动层数(顶栏 + 侧栏 + 6 卡) const CLOSE = ASSEMBLE + (N - 1) * STAGGER + 12; // = 123,最后一层归位 // FakeDashboard variant A 的布局常量(绝对定位复刻) const SIDE_W = 220; const TOP_H = 72; const PAD = 36; const GAP = 28; const CARD_W = (1920 - SIDE_W - PAD * 2 - GAP * 2) / 3; // 524 const CARD_H = (1080 - TOP_H - PAD * 2 - GAP) / 2; // 454 type Layer = { key: string; x: number; y: number; w: number; h: number; z: number; // 炸开深度 60–320 order: number; // 错峰序 radius: number; node: React.ReactNode; }; const Sidebar: React.FC = () => (
{Array.from({ length: 7 }).map((_, i) => (
))}
); const Topbar: React.FC = () => (
); // 六卡深度:错落分布在 60–320,近的自然更大(perspective 缩放) const CARD_Z = [150, 300, 80, 230, 320, 110]; const LAYERS: Layer[] = [ { key: 'top', x: SIDE_W, y: 0, w: 1920 - SIDE_W, h: TOP_H, z: 260, order: 0, radius: 0, node: }, { key: 'side', x: 0, y: 0, w: SIDE_W, h: 1080, z: 190, order: 1, radius: 0, node: }, ...Array.from({ length: 6 }).map((_, i) => { const col = i % 3; const row = Math.floor(i / 3); return { key: `card${i}`, x: SIDE_W + PAD + col * (CARD_W + GAP), y: TOP_H + PAD + row * (CARD_H + GAP), w: CARD_W, h: CARD_H, z: CARD_Z[i], order: 2 + i, radius: 14, node: , }; }), ]; export const ExplodedView: React.FC = () => { const frame = useCurrentFrame(); // 每层进度:炸开 ease-out-back(带一点回弹的“咔”)× 合体逆序 ease-in const layerP = (order: number) => { const out = interpolate( frame, [EXPLODE + order * STAGGER, EXPLODE + order * STAGGER + 14], [0, 1], { extrapolateLeft: 'clamp', extrapolateRight: 'clamp', easing: Easing.out(Easing.back(1.7)) }, ); const back = interpolate( frame, [ASSEMBLE + (N - 1 - order) * STAGGER, ASSEMBLE + (N - 1 - order) * STAGGER + 12], [0, 1], { extrapolateLeft: 'clamp', extrapolateRight: 'clamp', easing: Easing.in(Easing.cubic) }, ); return out * (1 - back); }; // 全局散开度(驱动底板变暗) const g = interpolate(frame, [EXPLODE, EXPLODE + 24], [0, 1], { extrapolateLeft: 'clamp', extrapolateRight: 'clamp', easing: Easing.out(Easing.cubic) }) * (1 - interpolate(frame, [ASSEMBLE, CLOSE], [0, 1], { extrapolateLeft: 'clamp', extrapolateRight: 'clamp', easing: Easing.in(Easing.cubic) })); // 合体收口:2f 震屏,指数衰减 const since = frame - CLOSE; const env = since >= 0 ? 13 * Math.exp(-since / 1.3) : 0; const shakeX = env * Math.sin(since * 3.3); const shakeY = env * 0.7 * Math.sin(since * 4.7 + 1.1); return (
{/* 底板:页面背景留在 Z=0,炸开时整体压暗,衬出层间深度 */}
{/* 投到底板上的假投影:随层浮起而下移/变虚 */} {LAYERS.map((L) => { const p = Math.min(1, Math.max(0, layerP(L.order))); if (p <= 0.01) return null; return (
); })} {/* 可动层:沿 Z 浮起,近的更大更实、远的略暗 */} {LAYERS.map((L) => { const p = layerP(L.order); const pc = Math.min(1, Math.max(0, p)); const bright = 1 - (1 - L.z / 320) * 0.28 * pc; // 深度越浅(z 小=离底板近=远离镜头)越暗 return (
0.02 ? `0 ${10 + L.z * 0.1 * pc}px ${16 + L.z * 0.14 * pc}px rgba(0,0,0,${0.12 + 0.1 * pc})` : 'none', }} > {L.node}
); })}
); };