"use client"; import React, { useRef } from "react"; import { Canvas, useFrame } from "@react-three/fiber"; import { EffectComposer, Bloom, Vignette } from "@react-three/postprocessing"; import { KernelSize } from "postprocessing"; import * as THREE from "three"; import { C, WAYPOINTS } from "./theme"; import { clamp01, damp, lerp, smoothstep } from "./math"; import Brain from "./Brain"; import City from "./City"; import Routes from "./Routes"; import Network from "./Network"; import SLAClock from "./SLAClock"; type Props = { progress: React.RefObject; reduced?: boolean; isMobile?: boolean; active?: boolean; }; const posTarget = new THREE.Vector3(); const lookTarget = new THREE.Vector3(); /** * Cinematic camera that flies along the waypoint spline by scroll progress. * Adjacent waypoints are smoothstep-interpolated, then the camera exponentially * damps toward the result so even fast/flung scrolls glide instead of snapping. */ function CameraRig({ progress }: { progress: React.RefObject }) { const lookCurrent = useRef(new THREE.Vector3(0, 6, 0)); const inited = useRef(false); useFrame((state, dt) => { const p = clamp01(progress.current ?? 0); // Locate the active waypoint segment. let i = 0; for (let k = 0; k < WAYPOINTS.length - 1; k++) { if (p >= WAYPOINTS[k].at && p <= WAYPOINTS[k + 1].at) { i = k; break; } if (p > WAYPOINTS[WAYPOINTS.length - 1].at) i = WAYPOINTS.length - 2; } const a = WAYPOINTS[i]; const b = WAYPOINTS[i + 1]; const span = b.at - a.at || 1; const lt = smoothstep(0, 1, clamp01((p - a.at) / span)); posTarget.set( lerp(a.pos[0], b.pos[0], lt), lerp(a.pos[1], b.pos[1], lt), lerp(a.pos[2], b.pos[2], lt), ); lookTarget.set( lerp(a.look[0], b.look[0], lt), lerp(a.look[1], b.look[1], lt), lerp(a.look[2], b.look[2], lt), ); // Very subtle idle breathing so the shot isn't dead-static — kept tiny (~70% // smaller than before) so the camera reads as a fixed map view, not a fly-through. const t = state.clock.elapsedTime; posTarget.x += Math.sin(t * 0.16) * 0.14; posTarget.y += Math.sin(t * 0.21) * 0.07; const cam = state.camera; if (!inited.current) { cam.position.copy(posTarget); lookCurrent.current.copy(lookTarget); inited.current = true; } else { cam.position.x = damp(cam.position.x, posTarget.x, 2.6, dt); cam.position.y = damp(cam.position.y, posTarget.y, 2.6, dt); cam.position.z = damp(cam.position.z, posTarget.z, 2.6, dt); lookCurrent.current.x = damp(lookCurrent.current.x, lookTarget.x, 3, dt); lookCurrent.current.y = damp(lookCurrent.current.y, lookTarget.y, 3, dt); lookCurrent.current.z = damp(lookCurrent.current.z, lookTarget.z, 3, dt); } cam.lookAt(lookCurrent.current); }); return null; } function LogisticsBrainCanvas({ progress, reduced = false, isMobile = false, active = true }: Props) { return ( {!reduced && ( )} ); } export default React.memo(LogisticsBrainCanvas);