// ===== 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 居中,叠大数字 + 白光晕 */}
99.9%
); }; // ===== 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 */}
10x
FASTER THAN BASELINE
{/* 暗角层:四角径向渐变,随推近同步加深 */}
); };