// ===== demos/camera/tension-camera-moves/BulletTimeFreezeOrbit.tsx =====
// 子弹时间冻结环绕(bullet-time-freeze-orbit)——The Matrix bullet time。
// 中央 900×560 面板内 5 根柱状图错峰生长(动画时钟 effFrame 驱动)。
// 关键帧:0–20 hold 读布景;20–45 柱子正常生长;45–105 时钟咬死(柱子完全静止),
// 相机 perspective(1600px) 下 rotateY 0→55°(45–72)→顶点悬停(72–82)→回 0(82–105),
// 同步 scale 1→1.12→1 + translateX 摆动增强绕行感;105–120 时钟恢复柱子长完;
// 118–128 数字标签浮现;128–150 全静止收尾。
import React from 'react';
import { useCurrentFrame, interpolate, Easing } from 'remotion';
import { G, TitleBlock } from '../../_fixtures/Fixtures';
const h = (n: number) => {
const s = Math.sin(n * 127.3) * 43758.5453;
return s - Math.floor(s);
};
const PANEL_W = 900;
const PANEL_H = 560;
const BAR_COUNT = 5;
export const BulletTimeFreezeOrbit: React.FC = () => {
const frame = useCurrentFrame();
// ── 子弹时间时钟:0–45 正常走,45–105 冻结,105 起恢复 ──
const effFrame =
frame < 45 ? frame : frame < 105 ? 45 : 45 + (frame - 105);
// ── 冻结区间的相机环绕(用真实 frame 驱动) ──
const rotY =
frame < 72
? interpolate(frame, [45, 72], [0, 55], {
extrapolateLeft: 'clamp',
extrapolateRight: 'clamp',
easing: Easing.inOut(Easing.cubic),
})
: frame < 82
? 55 // 顶点悬停 10f
: interpolate(frame, [82, 105], [55, 0], {
extrapolateLeft: 'clamp',
extrapolateRight: 'clamp',
easing: Easing.inOut(Easing.cubic),
});
const orbitT = rotY / 55; // 0→1→0,复用做 scale / translateX
const scale = 1 + 0.12 * orbitT;
const tx = -170 * Math.sin(orbitT * Math.PI * 0.5) - 60 * orbitT; // 绕行横摆
const ty = -24 * orbitT;
// ── 柱子:错峰生长,全部由 effFrame 驱动(冻结即静止) ──
const chartW = PANEL_W - 140;
const chartH = PANEL_H - 190;
const barW = 92;
const gap = (chartW - BAR_COUNT * barW) / (BAR_COUNT - 1);
const bars = Array.from({ length: BAR_COUNT }).map((_, i) => {
const full = chartH * (0.42 + h(i + 1) * 0.55); // 目标高度
const start = 20 + i * 4;
const end = 48 + i * 3; // 20–60f 区间内错峰
const p = interpolate(effFrame, [start, end], [0, 1], {
extrapolateLeft: 'clamp',
extrapolateRight: 'clamp',
easing: Easing.out(Easing.cubic),
});
const value = Math.round((full / chartH) * 100); // 标签与柱高一致
return { hNow: full * p, full, value, done: p >= 1 };
});
// ── 恢复段:数字标签浮现(118–128),之后全静止 ──
const labelOp = interpolate(frame, [118, 128], [0, 1], {
extrapolateLeft: 'clamp',
extrapolateRight: 'clamp',
});
// 冻结提示条:冻结期间显示 FREEZE 徽标,帮助读出手法
const freezeOp = interpolate(frame, [45, 52, 98, 105], [0, 1, 1, 0], {
extrapolateLeft: 'clamp',
extrapolateRight: 'clamp',
});
return (
{/* 冻结徽标 */}
FREEZE
{/* 3D 舞台 */}
{/* 面板标题条 */}
{/* 图表区:横向刻度线 + 柱子 */}
{[0, 1, 2, 3].map((i) => (
))}
{/* 基线 */}
{bars.map((b, i) => (
{/* 数字标签:恢复段浮现 */}
{b.value}
))}
);
};
// ===== demos/camera/tension-camera-moves/DutchRollToLevel.tsx =====
import React from 'react';
import { AbsoluteFill, Easing, interpolate, useCurrentFrame } from 'remotion';
import { FakeDashboard, Card, G } from '../../_fixtures/Fixtures';
// 斜角滚正(dutch-roll-to-level):呈现痛点时整帧带 -10° 斜角悬着(叠极缓慢
// 正弦漂移防止读作静态歪图),帧 70 解决方案的一拍整帧带单次过冲滚回水平
// (-10° → +1.2° → 0),同时警示条淡出、干净卡浮现——"世界被扶正"打在节拍上。
// 帧 0–70 斜置漂移 / 70–84 滚正冲过头 / 84–94 收回 0 / 94–140 真静止。
const ROLL = 70; // 滚正起拍
const LEVEL = 94; // 完全归位帧
export const DutchRollToLevel: React.FC = () => {
const f = useCurrentFrame();
// —— 斜置期的缓慢漂移(帧 70 前):±0.8° 长周期正弦 + 2px 纵漂 ——
const driftT = Math.min(f, ROLL);
const driftRot = Math.sin(driftT * 0.035) * 0.8;
const driftY = Math.sin(driftT * 0.05) * 2;
// 滚正期间漂移随进度淡出
const driftFade = interpolate(f, [ROLL, ROLL + 6], [1, 0], {
extrapolateLeft: 'clamp',
extrapolateRight: 'clamp',
});
// —— 滚正:-10° 用 14f 冲过 0 到 +1.2°,再 10f 收回 0(单次过冲不振荡) ——
const baseRot =
f < ROLL
? -10
: f < ROLL + 14
? interpolate(f, [ROLL, ROLL + 14], [-10, 1.2], {
easing: Easing.out(Easing.cubic),
})
: interpolate(f, [ROLL + 14, LEVEL], [1.2, 0], {
extrapolateRight: 'clamp',
easing: Easing.inOut(Easing.quad),
});
const rot = baseRot + driftRot * driftFade;
const y = driftY * driftFade;
// scale 1.15(防旋转露边)→ 滚正同步收到 1.08
const scale = interpolate(f, [ROLL, LEVEL], [1.15, 1.08], {
extrapolateLeft: 'clamp',
extrapolateRight: 'clamp',
easing: Easing.inOut(Easing.cubic),
});
// —— 警示条(痛点):斜置期悬在上方,滚正一拍淡出;干净卡反向淡入 ——
const alertOpacity = interpolate(f, [ROLL, ROLL + 12], [1, 0], {
extrapolateLeft: 'clamp',
extrapolateRight: 'clamp',
});
const cleanOpacity = interpolate(f, [ROLL + 8, ROLL + 22], [0, 1], {
extrapolateLeft: 'clamp',
extrapolateRight: 'clamp',
});
return (
{/* 痛点警示条:深色横幅压在页面上方(斜着更显歪) */}
{/* 解决方案:干净卡随滚正浮现在同一位置 */}
);
};
// ===== demos/camera/tension-camera-moves/PullBackIsolation.tsx =====
// 拉远孤立收束(pull-back-isolation)——pull-back shot。
// 相机容器 scale 2.2→0.62(0–110f,Easing.out(cubic)):开场怼在主卡
// "99.9%" 特写上,缓缓后拉露出周围 8 张兄弟卡。帧 30 起兄弟卡按离主卡
// 距离由近到远错峰熄灭(每 8f 一张,opacity→0 + brightness 压暗);
// 背景 60–110f 从 #ececea 沉入 #141414;主卡白光晕 60–100f 淡入。
// 帧 110–150 完全静止:暗场中央孤悬一张发光小卡——全片只为这一个数字。
import React from 'react';
import { useCurrentFrame, interpolate, Easing } from 'remotion';
import { G, Card } from '../../_fixtures/Fixtures';
// 8 张兄弟卡:相对主卡中心 (960,540) 的偏移 + 尺寸 + seed
const SIBS = [
{ dx: -620, dy: -330, w: 360, h: 240, seed: 3 },
{ dx: 10, dy: -390, w: 420, h: 220, seed: 4 },
{ dx: 620, dy: -320, w: 380, h: 260, seed: 5 },
{ dx: -680, dy: 20, w: 340, h: 230, seed: 6 },
{ dx: 700, dy: 40, w: 360, h: 250, seed: 7 },
{ dx: -600, dy: 360, w: 400, h: 240, seed: 8 },
{ dx: 40, dy: 400, w: 440, h: 220, seed: 9 },
{ dx: 640, dy: 350, w: 370, h: 250, seed: 10 },
].map((s) => ({ ...s, dist: Math.hypot(s.dx, s.dy) }));
// 按离主卡距离排名 → 错峰熄灭顺序(近的先灭)
const RANKED = SIBS.map((s, i) => i).sort((a, b) => SIBS[a].dist - SIBS[b].dist);
const FADE_START = RANKED.reduce((acc, idx, rank) => {
acc[idx] = 30 + rank * 8;
return acc;
}, []);
const FADE_DUR = 16;
const clamp = { extrapolateLeft: 'clamp' as const, extrapolateRight: 'clamp' as const };
export const PullBackIsolation: React.FC = () => {
const frame = useCurrentFrame();
// 相机后拉:2.2(怼脸特写)→ 0.62(大远景孤悬)
const scale = interpolate(frame, [0, 110], [2.2, 0.62], {
easing: Easing.out(Easing.cubic),
...clamp,
});
// 背景沉入黑暗:#ececea → #141414(60–110f)
const bgT = interpolate(frame, [60, 110], [0, 1], { easing: Easing.inOut(Easing.quad), ...clamp });
const bgC = Math.round(236 + (20 - 236) * bgT);
const bg = `rgb(${bgC},${bgC},${bgC})`;
// 主卡白光晕淡入(60–100f)
const glow = interpolate(frame, [60, 100], [0, 0.35], { ...clamp });
return (
{/* 相机容器:以主卡中心(画面中心)为原点整体缩放 */}
{/* 兄弟卡:错峰熄灭 */}
{SIBS.map((s, i) => {
const t0 = FADE_START[i];
const op = interpolate(frame, [t0, t0 + FADE_DUR], [1, 0], {
easing: Easing.out(Easing.quad),
...clamp,
});
const bright = interpolate(frame, [t0, t0 + FADE_DUR], [1, 0.3], { ...clamp });
return (
);
})}
{/* 主卡:520×340 居中,叠大数字 + 白光晕 */}
);
};
// ===== demos/camera/tension-camera-moves/SlowPushIn.tsx =====
// 慢推压迫(slow-push-in)——studiobinder camera movements。
// 景 A(帧 0–120):深色底 + 白色大数字 "10x",scale 用 Easing.in(Easing.quad)
// 从 1.00 匀加速推到 1.14——前 2 秒几乎不可察,后段明显可感;同时四角径向
// 暗角 opacity 0→0.5 同步加深,构成压迫感的第二来源。
// 帧 120 无任何过渡硬切景 B:满屏亮色 FakeDashboard(A) 真静止 30f——
// 暗→亮的大反差让"切"这一拍才响。
import React from 'react';
import { useCurrentFrame, interpolate, Easing } from 'remotion';
import { G, FakeDashboard } from '../../_fixtures/Fixtures';
const CUT = 120; // 硬切帧
export const SlowPushIn: React.FC = () => {
const frame = useCurrentFrame();
// ---- 景 B:帧 120 起,满屏亮面板,完全静止 ----
if (frame >= CUT) {
return ;
}
// ---- 景 A:0–120f 慢推 ----
// 匀加速推近:Easing.in(quad)——前段几乎不可察,后段可感
const scale = interpolate(frame, [0, CUT], [1.0, 1.14], {
easing: Easing.in(Easing.quad),
extrapolateLeft: 'clamp',
extrapolateRight: 'clamp',
});
// 暗角同步加深:压迫感的第二来源
const vignette = interpolate(frame, [0, CUT], [0, 0.5], {
easing: Easing.in(Easing.quad),
extrapolateLeft: 'clamp',
extrapolateRight: 'clamp',
});
return (
{/* 被推近的内容层:整体 scale */}
{/* 暗角层:四角径向渐变,随推近同步加深 */}
);
};