// ===== demos/camera/space-camera-moves/DroneDiveLanding.tsx ===== import React from 'react'; import { AbsoluteFill, Easing, interpolate, useCurrentFrame } from 'remotion'; import { CameraMotionBlur } from '@remotion/motion-blur'; import { FakeDashboard, G } from '../../_fixtures/Fixtures'; // drone-dive-landing:上帝视角俯视整页平躺的 dashboard(近垂直俯角、缩小居中), // 相机猛扎下来——俯角抬平、页面放大立正,最后一段气垫式长尾减速, // 稳稳停在 hero 卡正前方特写。FPV 无人机俯冲降落的运镜翻译。 // // hero 卡 = FakeDashboard A 网格左上格: // 侧栏 220 + padding 36 = x 256 起,列宽 (1628-56)/3 = 524; // 顶栏 72 + padding 36 = y 108 起,行高 (936-28)/2 = 454。 // 卡中心 (256+262, 108+227) = (518, 335),全程 transform-origin 钉在这里。 const HERO = { cx: 518, cy: 335 }; const DIVE_START = 20; // 开头 hold 20f:上帝视角建立 const DIVE_END = 45; // 主俯冲段 25f,ease-in(cubic) 越冲越快 const LAND_END = 65; // 气垫段 20f,ease-out(quint) 长尾减速,之后真静止 const DIVE_SHARE = 0.82; // 俯冲段吃掉 82% 行程,剩 18% 留给气垫 const Scene: React.FC = () => { const frame = useCurrentFrame(); // 两段速度曲线拼一条行程 p∈[0,1]:先猛加速扎下,切换帧速度骤降 = 气垫顶住的体感 const pDive = interpolate(frame, [DIVE_START, DIVE_END], [0, DIVE_SHARE], { extrapolateLeft: 'clamp', extrapolateRight: 'clamp', easing: Easing.in(Easing.cubic), }); const pLand = interpolate(frame, [DIVE_END, LAND_END], [0, 1 - DIVE_SHARE], { extrapolateLeft: 'clamp', extrapolateRight: 'clamp', easing: Easing.out(Easing.poly(5)), }); const p = frame < DIVE_END ? pDive : DIVE_SHARE + pLand; // 三轴联动,全部由同一条 p 驱动(同一台"相机"的一次连续机动) const rotX = interpolate(p, [0, 1], [72, 0]); // 俯角抬平 const scale = interpolate(p, [0, 1], [0.42, 1.35]); // 缩小全景 → hero 特写 // 平移:起点让整页平躺在画面中央偏上;终点 hero 卡中心正对画面中心 const tx = interpolate(p, [0, 1], [256, 960 - HERO.cx]); const ty = interpolate(p, [0, 1], [150, 540 - HERO.cy]); // 地面投影:俯视时页面悬空、下方拖一团椭圆软影;落地立正后影子收干 const shadowOp = interpolate(p, [0, 0.8], [0.32, 0], { extrapolateLeft: 'clamp', extrapolateRight: 'clamp', }); const shadowW = 1300 * (0.6 + scale * 0.4); return ( {/* 地面软影(高度感线索) */}
{/* 相机 = perspective 容器;页面绕 hero 卡中心做 rotateX + scale + translate */}
); }; export const DroneDiveLanding: React.FC = () => ( ); // ===== demos/camera/space-camera-moves/ExplodedView.tsx ===== 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}
); })}
); };