// paper-plane-messenger —— pitch-app 77–82s(2.5D 简化) // 点击"发送"后镜头拉远脱离窗口 A,折纸飞机从窗口飞出沿弧线飞行 // (俯仰角跟随切线),镜头伴飞穿过多层视差漂浮的灰阶道具, // 飞抵窗口 B 前落定,窗口 B 放大接管全屏。发送语义实体化成转场信使。 import React from 'react'; import { AbsoluteFill, useCurrentFrame, interpolate, Easing } from 'remotion'; import { Card, G } from '../../_fixtures/Fixtures'; const mulberry32 = (a: number) => () => { let t = (a += 0x6d2b79f5); t = Math.imul(t ^ (t >>> 15), t | 1); t ^= t + Math.imul(t ^ (t >>> 7), t | 61); return ((t ^ (t >>> 14)) >>> 0) / 4294967296; }; // ---- 世界坐标 ---- const AX = 520, AY = 560; // 窗口 A 中心 const BX = 3200, BY = 600; // 窗口 B 中心 const WIN_W = 760, WIN_H = 500; // 飞机三次贝塞尔弧线:A 的发送按钮 → 高抛 → B 门前 const P0 = { x: AX + 300, y: AY + 150 }; const P1 = { x: 1250, y: -80 }; const P2 = { x: 2500, y: 300 }; const P3 = { x: BX - 470, y: BY + 20 }; const bez = (t: number) => { const u = 1 - t; return { x: u * u * u * P0.x + 3 * u * u * t * P1.x + 3 * u * t * t * P2.x + t * t * t * P3.x, y: u * u * u * P0.y + 3 * u * u * t * P1.y + 3 * u * t * t * P2.y + t * t * t * P3.y, }; }; // ---- 时间轴(150f)---- const CLICK = 12; // 点击发送 const ZOOM_OUT = [16, 42] as const; // 镜头拉远 const FLY = [34, 104] as const; // 飞行 const TAKEOVER = [112, 146] as const; // 窗口 B 接管 // 视差道具(圆环/方块),depth: 0.45 远 / 0.75 中 / 1.3 近 type Prop = { x: number; y: number; size: number; ring: boolean; depth: number; drift: number }; const PROPS: Prop[] = (() => { const rng = mulberry32(42); const out: Prop[] = []; const depths = [0.45, 0.75, 1.3]; for (let i = 0; i < 16; i++) { const depth = depths[i % 3]; out.push({ x: 500 + rng() * 2800, y: -150 + rng() * 1350, size: depth < 0.6 ? 60 + rng() * 70 : depth < 1 ? 110 + rng() * 100 : 220 + rng() * 160, ring: rng() > 0.45, depth, drift: rng() * Math.PI * 2, }); } return out; })(); const Window: React.FC<{ cx: number; cy: number; seed: number; sendBtn?: boolean; btnPulse?: number }> = ({ cx, cy, seed, sendBtn, btnPulse = 0 }) => (
{[0, 1, 2].map((i) =>
)}
{sendBtn && (
0 ? `0 0 0 ${btnPulse * 22}px rgba(47,47,47,0.18)` : 'none' }}> Send ➤
)}
); const Plane: React.FC<{ x: number; y: number; angle: number; scale: number; opacity: number }> = ({ x, y, angle, scale, opacity }) => ( {/* 折纸飞机:三块折面,灰阶深浅示折痕 */} ); export const PaperPlaneMessenger: React.FC = () => { const frame = useCurrentFrame(); // 飞行进度(整体 ease-in-out:起飞加速、落定减速) const tFly = interpolate(frame, [FLY[0], FLY[1]], [0, 1], { extrapolateLeft: 'clamp', extrapolateRight: 'clamp', easing: Easing.bezier(0.45, 0.05, 0.25, 1), }); const pos = bez(tFly); const posNext = bez(Math.min(tFly + 0.012, 1)); const angle = Math.atan2(posNext.y - pos.y, posNext.x - pos.x) * (180 / Math.PI); // ---- 镜头 ---- const zoomOutP = interpolate(frame, [ZOOM_OUT[0], ZOOM_OUT[1]], [0, 1], { extrapolateLeft: 'clamp', extrapolateRight: 'clamp', easing: Easing.inOut(Easing.cubic), }); const takeP = interpolate(frame, [TAKEOVER[0], TAKEOVER[1]], [0, 1], { extrapolateLeft: 'clamp', extrapolateRight: 'clamp', easing: Easing.inOut(Easing.cubic), }); // 镜头中心:A → 跟飞机 → B const followW = interpolate(frame, [ZOOM_OUT[0], ZOOM_OUT[1]], [0, 1], { extrapolateLeft: 'clamp', extrapolateRight: 'clamp' }); let cx = AX + (pos.x - AX) * followW; let cy = (AY + (pos.y - AY) * followW) * (1 - takeP) + 0; // 占位,下一行合成 cy = AY + (Math.max(pos.y, 150) - AY) * followW; cx = cx * (1 - takeP) + BX * takeP; cy = cy * (1 - takeP) + BY * takeP; // 变焦:1.55(贴脸 A)→ 0.62(拉远伴飞)→ 2.6(B 接管全屏) const zBase = 1.55 + (0.62 - 1.55) * zoomOutP; const zTake = 0.62 + (3.1 - 0.62) * takeP; const z = frame < TAKEOVER[0] ? zBase : zTake; const camX = (wx: number, d: number) => 960 + (wx - cx) * z * d; const camY = (wy: number, d: number) => 540 + (wy - cy) * z * d; const btnPulse = interpolate(frame, [CLICK, CLICK + 3, CLICK + 12], [0, 1, 0], { extrapolateLeft: 'clamp', extrapolateRight: 'clamp', easing: Easing.out(Easing.quad), }); const planeVisible = frame >= FLY[0] - 2; // 飞行途中放大到 1.7 增强可读性,落定收回 1.1 const flightBoost = interpolate(tFly, [0, 0.25, 0.75, 1], [1, 1.7, 1.7, 1.1], { extrapolateLeft: 'clamp', extrapolateRight: 'clamp', }); const planeScale = interpolate(frame, [FLY[0] - 2, FLY[0] + 8], [0.3, 1], { extrapolateLeft: 'clamp', extrapolateRight: 'clamp', easing: Easing.out(Easing.back(1.6)), }) * flightBoost; // 窗口 B 接管时飞机淡出,不留残影 const planeOpacity = interpolate(takeP, [0, 0.35], [1, 0], { extrapolateLeft: 'clamp', extrapolateRight: 'clamp' }); return ( {/* 视差道具:远层 / 中层(世界层之下) */} {PROPS.filter((p) => p.depth < 1).map((p, i) => { const wob = Math.sin(frame * 0.035 + p.drift) * 14; const x = camX(p.x, p.depth) + wob; const y = camY(p.y, p.depth) + Math.cos(frame * 0.03 + p.drift) * 10; const s = p.size * z * p.depth; const fade = interpolate(takeP, [0.3, 0.9], [1, 0], { extrapolateLeft: 'clamp', extrapolateRight: 'clamp' }); return (
); })} {/* 世界层(depth=1):窗口 A / B + 飞机 */}
{planeVisible && planeOpacity > 0.01 && }
{/* 近景道具(世界层之上,焦外大件) */} {PROPS.filter((p) => p.depth >= 1).map((p, i) => { const wob = Math.sin(frame * 0.04 + p.drift) * 20; const x = camX(p.x, p.depth) + wob; const y = camY(p.y, p.depth) + Math.cos(frame * 0.033 + p.drift) * 16; const s = p.size * z * p.depth; const fade = interpolate(takeP, [0.2, 0.7], [1, 0], { extrapolateLeft: 'clamp', extrapolateRight: 'clamp' }); return (
); })} ); };