// ===== 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}
);
})}
);
};