update miletruth page

This commit is contained in:
2026-05-30 17:24:26 +05:30
parent 88722329d5
commit 8d74f7063e
23 changed files with 5373 additions and 158 deletions

View File

@@ -0,0 +1,158 @@
"use client";
import React, { useMemo, useRef } from "react";
import { useFrame } from "@react-three/fiber";
import * as THREE from "three";
import { COLORS } from "./constants";
import { seeded, smoothstep } from "./math";
type Props = {
progress: React.RefObject<number>;
reduced?: boolean;
};
const CORE_Y = 3.4;
type CalcBeamProps = {
offset: THREE.Vector3;
index: number;
progress: React.RefObject<number>;
color: string;
};
function CalculationBeam({ offset, index, progress, color }: CalcBeamProps) {
const lineRef = useRef<THREE.LineSegments>(null);
const lineMatRef = useRef<THREE.LineBasicMaterial>(null);
const packetRefs = useRef<(THREE.Mesh | null)[]>([]);
const packetCount = 3;
const offsets = useMemo(() => Array.from({ length: packetCount }, (_, k) => k / packetCount), []);
useFrame((state) => {
const p = progress.current ?? 0;
const t = state.clock.elapsedTime;
// Beams are active during AI Scan and Route Generation phases (progress 0.22 -> 0.76)
const activeFactor = smoothstep(0.20, 0.40, p) * (1 - smoothstep(0.72, 0.85, p));
const isVisible = activeFactor > 0.01;
if (lineRef.current) lineRef.current.visible = isVisible;
if (lineMatRef.current && isVisible) {
lineMatRef.current.opacity = activeFactor * (0.15 + Math.sin(t * 12 + index) * 0.08);
}
packetRefs.current.forEach((mesh, k) => {
if (!mesh) return;
mesh.visible = isVisible;
if (!isVisible) return;
const speed = 0.8 + seeded(index * 5 + k) * 0.4;
const u = (offsets[k] + t * (speed / 3)) % 1;
// Linearly interpolate from center [0,0,0] to hub offset
mesh.position.copy(offset).multiplyScalar(u);
const mat = mesh.material as THREE.MeshBasicMaterial;
mat.opacity = activeFactor * Math.sin(u * Math.PI) * 0.8;
const s = 0.04 + Math.sin(t * 8 + k) * 0.015;
mesh.scale.setScalar(s / 0.06);
});
});
// Pre-generated geometry for the static connecting line
const lineGeo = useMemo(() => {
const geo = new THREE.BufferGeometry();
const pts = new Float32Array([0, 0, 0, offset.x, offset.y, offset.z]);
geo.setAttribute("position", new THREE.BufferAttribute(pts, 3));
return geo;
}, [offset]);
return (
<group>
{/* Connector line segment */}
<lineSegments ref={lineRef} geometry={lineGeo}>
<lineBasicMaterial
ref={lineMatRef}
color={color}
transparent
opacity={0}
depthWrite={false}
blending={THREE.AdditiveBlending}
/>
</lineSegments>
{/* Streams of flowing energy data packets */}
{Array.from({ length: packetCount }).map((_, k) => (
<mesh
key={k}
ref={(el) => {
packetRefs.current[k] = el;
}}
visible={false}
>
<sphereGeometry args={[0.06, 8, 8]} />
<meshBasicMaterial
color={color}
transparent
opacity={0}
depthWrite={false}
blending={THREE.AdditiveBlending}
/>
</mesh>
))}
</group>
);
}
const CalculationBeamMemo = React.memo(CalculationBeam);
/**
* The Doormile AI core: a clean floating holographic orb with energy rings,
* a particle shell and neural links. It powers up during the AI-scan phase,
* fires an expanding radar scan, then a route-optimization burst.
*/
function AICore({ progress, reduced = false }: Props) {
const root = useRef<THREE.Group>(null);
const hubOffsets = useMemo(() => {
return Array.from({ length: 5 }, (_, c) => {
const baseAngle = (c / 5) * Math.PI * 2 + 0.3;
const radius = 7 + seeded(c * 7 + 1) * 3;
const cx = Math.cos(baseAngle) * radius;
const cz = Math.sin(baseAngle) * radius;
return new THREE.Vector3(cx, 0.1 - CORE_Y, cz);
});
}, []);
useFrame((state) => {
const t = state.clock.elapsedTime;
if (root.current) {
root.current.position.y = CORE_Y + Math.sin(t * 0.8) * 0.12;
}
});
return (
<group ref={root} position={[0, CORE_Y, 0]}>
{/* Sleek, subtle dispatch beacon directly above warehouse tower */}
<mesh>
<sphereGeometry args={[0.06, 12, 12]} />
<meshBasicMaterial color={COLORS.green} blending={THREE.AdditiveBlending} />
</mesh>
{/* Route Calculation/Analysis Beams */}
{hubOffsets.map((offset, i) => (
<CalculationBeamMemo
key={`calc-beam-${i}`}
offset={offset}
index={i}
progress={progress}
color={i % 2 === 0 ? COLORS.cyan : COLORS.green}
/>
))}
</group>
);
}
export default React.memo(AICore);

View File

@@ -0,0 +1,191 @@
"use client";
import React, { useMemo, useRef } from "react";
import { useFrame } from "@react-three/fiber";
import * as THREE from "three";
import { COLORS } from "./constants";
import { damp, lerp, seeded, smoothstep } from "./math";
type Props = {
progress: React.RefObject<number>;
count?: number;
reduced?: boolean;
};
const CYAN = new THREE.Color(COLORS.cyan);
const RED = new THREE.Color(COLORS.red);
const GREEN = new THREE.Color(COLORS.green);
// Generate deterministic cluster centers matching routes.ts
const clusterCount = 5;
const clusterCenters = Array.from({ length: clusterCount }, (_, c) => {
const baseAngle = (c / clusterCount) * Math.PI * 2 + 0.3;
const radius = 7 + seeded(c * 7 + 1) * 3;
const cx = Math.cos(baseAngle) * radius;
const cz = Math.sin(baseAngle) * radius;
return new THREE.Vector3(cx, 0.1, cz);
});
function HologramCity({ progress, reduced = false }: Props) {
const mainWarehouseRef = useRef<THREE.Group>(null);
const radarRefs = useRef<(THREE.Group | null)[]>([]);
const corridorMats = useRef<(THREE.LineBasicMaterial | null)[]>([]);
const zoneMats = useRef<(THREE.MeshBasicMaterial | null)[]>([]);
const eased = useRef(0);
// Pre-generate corridor geometries (Warehouse [0, 0, 0] to Cluster Centers)
const corridorGeometries = useMemo(() => {
return clusterCenters.map((center) => {
const pts = [
new THREE.Vector3(0, 0.15, 0),
new THREE.Vector3(center.x * 0.5, 0.6, center.z * 0.5), // arch up slightly in the middle
new THREE.Vector3(center.x, 0.15, center.z),
];
return new THREE.CatmullRomCurve3(pts).getPoints(24);
});
}, []);
useFrame((state, dt) => {
const p = progress.current ?? 0;
eased.current = damp(eased.current, p, 3, dt);
const e = eased.current;
const t = state.clock.elapsedTime;
// Animate Main Warehouse central beacon rotation
if (mainWarehouseRef.current) {
mainWarehouseRef.current.position.y = Math.sin(t * 1.2) * 0.04;
}
// Rotate Radar dishes on top of the Regional Hubs
radarRefs.current.forEach((radar, idx) => {
if (radar) {
radar.rotation.y = t * (0.8 + idx * 0.2);
}
});
// Animate corridor line opacity and color:
// Under unoptimized states (chaos/scan) they are faint or red.
// As optimization settles, they light up with active cyan/green.
corridorMats.current.forEach((mat, idx) => {
if (mat) {
const activeColor = idx % 2 === 0 ? CYAN : GREEN;
const colorFactor = smoothstep(0.45, 0.8, e);
mat.color.copy(RED).lerp(activeColor, colorFactor);
mat.opacity = lerp(0.12, 0.7, colorFactor) * (0.8 + Math.sin(t * 4 - idx * 0.5) * 0.2);
}
});
// Animate ground delivery zone circles
zoneMats.current.forEach((mat, idx) => {
if (mat) {
const activeColor = idx % 2 === 0 ? CYAN : GREEN;
const colorFactor = smoothstep(0.5, 0.82, e);
mat.color.copy(RED).lerp(activeColor, colorFactor);
mat.opacity = lerp(0.06, 0.22, colorFactor) * (0.85 + Math.sin(t * 3 - idx) * 0.15);
}
});
});
return (
<group>
{/* Sleek Dark Cyber Ground Grid */}
<gridHelper
args={[60, 48, COLORS.cyan, COLORS.cyan]}
position={[0, -0.01, 0]}
>
<lineBasicMaterial
attach="material"
color={COLORS.cyan}
transparent
opacity={0.05}
/>
</gridHelper>
{/* Primary Transport Corridors (Warehouse to Cluster Hubs) */}
{corridorGeometries.map((points, i) => (
<line key={`corr${i}`}>
<bufferGeometry>
<float32BufferAttribute
attach="attributes-position"
args={[new Float32Array(points.flatMap(p => [p.x, p.y, p.z])), 3]}
/>
</bufferGeometry>
<lineBasicMaterial
ref={(el) => {
corridorMats.current[i] = el;
}}
transparent
opacity={0.15}
depthWrite={false}
/>
</line>
))}
{/* CENTRAL LOGISTICS ASSET: Main Warehouse Hub */}
<group ref={mainWarehouseRef} position={[0, 0, 0]}>
{/* Core foundation structure */}
<mesh position={[0, 0.35, 0]}>
<boxGeometry args={[2.2, 0.7, 1.8]} />
<meshBasicMaterial color={COLORS.ink} />
</mesh>
<mesh position={[0, 0.35, 0]}>
<boxGeometry args={[2.2, 0.7, 1.8]} />
<meshBasicMaterial color={COLORS.cyan} wireframe transparent opacity={0.65} />
</mesh>
{/* Loading Docks */}
{[-0.6, 0, 0.6].map((offset, i) => (
<mesh key={i} position={[offset, 0.18, 0.91]}>
<boxGeometry args={[0.3, 0.35, 0.06]} />
<meshBasicMaterial color={COLORS.cyan} transparent opacity={0.8} />
</mesh>
))}
{/* Upper Level / Control Deck */}
<mesh position={[0, 0.85, 0]}>
<boxGeometry args={[1.2, 0.3, 1.0]} />
<meshBasicMaterial color={COLORS.cyan} transparent opacity={0.15} />
</mesh>
<mesh position={[0, 0.85, 0]}>
<boxGeometry args={[1.2, 0.3, 1.0]} />
<meshBasicMaterial color={COLORS.cyan} wireframe transparent opacity={0.9} />
</mesh>
{/* Rooftop Solar Panels */}
{[-0.4, 0.4].map((offset, i) => (
<mesh key={i} position={[offset, 0.71, -0.4]} rotation={[0.15, 0, 0]}>
<boxGeometry args={[0.5, 0.03, 0.6]} />
<meshBasicMaterial color="#1e293b" />
</mesh>
))}
{/* Central Communications Array / Tower */}
<group position={[0, 1.0, 0]}>
{/* Main rod */}
<mesh position={[0, 0.4, 0]}>
<cylinderGeometry args={[0.03, 0.03, 0.8, 8]} />
<meshBasicMaterial color={COLORS.cyan} />
</mesh>
{/* Glowing Beacon */}
<mesh position={[0, 0.8, 0]}>
<sphereGeometry args={[0.12, 16, 16]} />
<meshBasicMaterial color="#ffffff" />
</mesh>
</group>
{/* Pulsing ground base glow */}
<mesh rotation={[-Math.PI / 2, 0, 0]} position={[0, 0.02, 0]}>
<ringGeometry args={[1.8, 2.2, 32]} />
<meshBasicMaterial
color={COLORS.cyan}
transparent
opacity={0.3}
blending={THREE.AdditiveBlending}
/>
</mesh>
</group>
</group>
);
}
export default React.memo(HologramCity);

View File

@@ -0,0 +1,152 @@
"use client";
import React from "react";
import { motion, useTransform, type MotionValue } from "framer-motion";
import { COLORS, KPIS, Kpi, rgba } from "./constants";
type Props = {
/** shared scroll progress (0 → 1) */
scroll: MotionValue<number>;
};
const COUNT_START = 0.7;
const COUNT_END = 0.97;
function Metric({ kpi, scroll, index }: { kpi: Kpi; scroll: MotionValue<number>; index: number }) {
const [jitter, setJitter] = React.useState(0);
React.useEffect(() => {
const interval = setInterval(() => {
// Tiny micro-fluctuation between -0.4% and +0.4%
const val = (Math.random() - 0.5) * 0.008;
setJitter(val);
}, 800 + Math.random() * 600); // slightly staggered updates
return () => clearInterval(interval);
}, []);
// Single reactive string — updates without re-rendering React.
const value = useTransform(scroll, (v) => {
const t = Math.min(1, Math.max(0, (v - COUNT_START) / (COUNT_END - COUNT_START)));
let n = kpi.before + (kpi.after - kpi.before) * t;
// Apply high-frequency live operational fluctuations on settled state
if (t > 0.95) {
n = n * (1 + jitter);
}
const afterSide = t > 0.5;
const prefix = afterSide ? kpi.prefixAfter ?? "" : kpi.prefixBefore ?? "";
if (kpi.key === "delayed" && kpi.after === 0 && t > 0.95) {
return `0`;
}
return `${prefix}${Math.round(n)}${kpi.suffix ?? ""}`;
});
const label = useTransform(scroll, (v) =>
v > (COUNT_START + COUNT_END) / 2 ? kpi.labelAfter : kpi.labelBefore,
);
const fromColor = kpi.key === "orders" ? COLORS.cyan : COLORS.red;
const toColor = COLORS.green;
const accent = useTransform(
scroll,
[COUNT_START, COUNT_END],
[rgba(fromColor, 1), rgba(toColor, 1)],
);
const glow = useTransform(
scroll,
[COUNT_START, COUNT_END],
[rgba(fromColor, 0.28), rgba(toColor, 0.32)],
);
const boxShadow = useTransform(glow, (g) => `0 10px 40px -12px ${g}, inset 0 1px 0 rgba(255,255,255,0.06)`);
const borderColor = useTransform(
scroll,
[COUNT_START, COUNT_END],
[rgba(fromColor, 0.4), rgba(toColor, 0.45)],
);
// Improvement arrow appears once optimized.
const arrowOpacity = useTransform(scroll, [COUNT_END - 0.12, COUNT_END], [0, 1]);
// Bar fills as the counter morphs from before → after.
const fillScale = useTransform(scroll, [COUNT_START, COUNT_END], [0.12, 1]);
const trendColor = kpi.goodWhenLower ? COLORS.green : (kpi.key === "orders" ? COLORS.cyan : COLORS.green);
const sparklinePath = React.useMemo(() => {
switch (kpi.key) {
case "distance":
case "vehicles":
return "M 0,3 C 16,3 32,17 64,17"; // Downward trend
case "orders":
return "M 0,17 C 16,6 32,19 64,8"; // Stable high wavy trend
case "delayed":
case "cost":
return "M 0,1 C 16,1 28,19 64,19"; // Sharp drop
default:
return "M 0,10 L 64,10";
}
}, [kpi.key]);
return (
<motion.div
className="dm-opt-metric"
style={{ boxShadow, borderColor, animationDelay: `${0.06 * index}s` }}
>
<div className="dm-opt-metric__top">
<motion.span className="dm-opt-metric__label">{label}</motion.span>
<motion.span className="dm-opt-metric__arrow" style={{ opacity: arrowOpacity, color: COLORS.green }}>
{kpi.goodWhenLower ? "▼" : "▲"}
</motion.span>
</div>
<motion.div className="dm-opt-metric__value" style={{ color: accent }}>
{value}
</motion.div>
{/* Sparkline trend graphic */}
<div className="dm-opt-metric__sparkline" style={{ position: "absolute", bottom: 8, right: 12, opacity: 0.32, width: 64, height: 20, pointerEvents: "none" }}>
<svg width="100%" height="100%" viewBox="0 0 64 20" fill="none">
<defs>
<linearGradient id={`grad-${kpi.key}`} x1="0" y1="0" x2="0" y2="20" gradientUnits="userSpaceOnUse">
<stop offset="0%" stopColor={trendColor} stopOpacity="0.22" />
<stop offset="100%" stopColor={trendColor} stopOpacity="0.0" />
</linearGradient>
</defs>
<path
d={sparklinePath}
stroke={trendColor}
strokeWidth="1.2"
strokeLinecap="round"
strokeLinejoin="round"
/>
<path
d={`${sparklinePath} L 64,20 L 0,20 Z`}
fill={`url(#grad-${kpi.key})`}
/>
</svg>
</div>
<div className="dm-opt-metric__bar">
<motion.div
className="dm-opt-metric__fill"
style={{ background: accent, scaleX: fillScale }}
/>
</div>
</motion.div>
);
}
const MetricCard = React.memo(Metric);
function MetricsPanel({ scroll }: Props) {
return (
<div className="dm-opt-metrics" role="group" aria-label="Optimization results">
{KPIS.map((kpi, i) => (
<MetricCard key={kpi.key} kpi={kpi} scroll={scroll} index={i} />
))}
</div>
);
}
export default React.memo(MetricsPanel);

View File

@@ -0,0 +1,85 @@
"use client";
import React, { useMemo, 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 { COLORS } from "./constants";
import { damp, lerp, seeded } from "./math";
import HologramCity from "./HologramCity";
import RouteSystem from "./RouteSystem";
import VehicleFleet from "./VehicleFleet";
import AICore from "./AICore";
type Props = {
progress: React.RefObject<number>;
reduced?: boolean;
isMobile?: boolean;
/** Pause the render loop when the section is scrolled off-screen. */
active?: boolean;
};
/** Slow cinematic camera move from a high chaotic view to a settled framing. */
function CameraRig({ progress }: { progress: React.RefObject<number> }) {
const eased = useRef(0);
useFrame((state, dt) => {
const p = progress.current ?? 0;
eased.current = damp(eased.current, p, 1.5, dt);
const e = eased.current;
const t = state.clock.elapsedTime;
const radius = lerp(17, 13, e);
const angle = lerp(-0.5, 0.45, e) + t * 0.02;
const height = lerp(9, 6.5, e) + Math.sin(t * 0.4) * 0.3;
const cam = state.camera;
cam.position.x = Math.sin(angle) * radius;
cam.position.z = Math.cos(angle) * radius;
cam.position.y = height;
cam.lookAt(0, 2.4, 0);
});
return null;
}
function OptimizationCanvas({ progress, reduced = false, isMobile = false, active = true }: Props) {
const cityCount = isMobile ? 48 : 90;
return (
<Canvas
flat
dpr={[1, isMobile || reduced ? 1.3 : 1.6]}
camera={{ position: [0, 9, 19], fov: 50, near: 0.1, far: 120 }}
gl={{ antialias: !isMobile, powerPreference: "high-performance", alpha: false }}
frameloop={active ? "always" : "never"}
>
<color attach="background" args={[COLORS.bg]} />
<fog attach="fog" args={[COLORS.bg, 18, 52]} />
<ambientLight intensity={0.6} />
<CameraRig progress={progress} />
<HologramCity progress={progress} count={cityCount} reduced={reduced} />
<RouteSystem progress={progress} reduced={reduced} isMobile={isMobile} />
<VehicleFleet progress={progress} reduced={reduced} />
<AICore progress={progress} reduced={reduced} />
{/* Bloom is what turns the additive-blended wireframes/beams into a real
glowing hologram. Skipped under reduced-motion; lighter on mobile. */}
{!reduced && (
<EffectComposer multisampling={isMobile ? 0 : 2}>
<Bloom
mipmapBlur
intensity={isMobile ? 0.7 : 1.0}
luminanceThreshold={0.15}
luminanceSmoothing={0.04}
radius={isMobile ? 0.6 : 0.75}
kernelSize={KernelSize.MEDIUM}
/>
<Vignette eskil={false} offset={0.25} darkness={0.5} />
</EffectComposer>
)}
</Canvas>
);
}
export default React.memo(OptimizationCanvas);

View File

@@ -0,0 +1,881 @@
"use client";
import React, { useEffect, useRef, useState } from "react";
import dynamic from "next/dynamic";
import { motion, AnimatePresence, useMotionValue, useTransform } from "framer-motion";
import gsap from "gsap";
import { ScrollTrigger } from "gsap/ScrollTrigger";
import Lenis from "lenis";
import {
COLORS,
PHASE_LABELS,
PhaseKey,
phaseFromProgress,
rgba,
} from "./constants";
import MetricsPanel from "./MetricsPanel";
// 3D scene is client-only and code-split so it never blocks first paint.
const OptimizationCanvas = dynamic(() => import("./OptimizationCanvas"), {
ssr: false,
});
const PHASE_ORDER: PhaseKey[] = [
"chaos",
"scan",
"dissolve",
"optimize",
"reorganize",
"metrics",
];
const WORKFLOW_STEPS = [
{ label: "Analyze", icon: "🔍", activateAt: 0 },
{ label: "Optimize", icon: "⚡", activateAt: 2 },
{ label: "Assign", icon: "🚛", activateAt: 3 },
{ label: "Execute", icon: "📡", activateAt: 4 },
{ label: "Monitor", icon: "📊", activateAt: 5 },
];
export default function OptimizationSection() {
const containerRef = useRef<HTMLDivElement>(null);
const progressRef = useRef(0);
const scroll = useMotionValue(0);
const [phase, setPhase] = useState<PhaseKey>("chaos");
const [pinState, setPinState] = useState<"before" | "pinned" | "after">("before");
const [mountScene, setMountScene] = useState(false);
const [sceneActive, setSceneActive] = useState(false);
const [isMobile, setIsMobile] = useState(false);
const [reduced, setReduced] = useState(false);
const [orders, setOrders] = useState(59);
const [accuracy, setAccuracy] = useState(98.7);
const [activeVehicles, setActiveVehicles] = useState(5);
const [carbon, setCarbon] = useState(-12.0);
const [routeHealth, setRouteHealth] = useState(99.4);
// Interval timers for high-fidelity live dashboard fluctuations
useEffect(() => {
const ordersInterval = setInterval(() => {
setOrders((prev) => prev + (Math.random() > 0.4 ? 1 : 0));
}, 4500);
const accuracyInterval = setInterval(() => {
setAccuracy((prev) => {
const delta = (Math.random() - 0.5) * 0.15;
const next = prev + delta;
return parseFloat(Math.min(99.1, Math.max(98.4, next)).toFixed(2));
});
}, 2800);
const vehicleInterval = setInterval(() => {
setActiveVehicles((prev) => (prev === 5 ? (Math.random() > 0.5 ? 4 : 5) : (Math.random() > 0.3 ? 5 : 4)));
}, 3500);
const carbonInterval = setInterval(() => {
setCarbon((prev) => {
const delta = (Math.random() - 0.5) * 0.2;
const next = prev + delta;
return parseFloat(Math.min(-11.5, Math.max(-12.8, next)).toFixed(1));
});
}, 3200);
const healthInterval = setInterval(() => {
setRouteHealth((prev) => {
const delta = (Math.random() - 0.5) * 0.12;
const next = prev + delta;
return parseFloat(Math.min(99.9, Math.max(98.8, next)).toFixed(2));
});
}, 2500);
return () => {
clearInterval(ordersInterval);
clearInterval(accuracyInterval);
clearInterval(vehicleInterval);
clearInterval(carbonInterval);
clearInterval(healthInterval);
};
}, []);
// Environment detection (client only).
useEffect(() => {
const mqMobile = window.matchMedia("(max-width: 767px)");
const mqReduce = window.matchMedia("(prefers-reduced-motion: reduce)");
const sync = () => {
setIsMobile(mqMobile.matches);
setReduced(mqReduce.matches);
};
sync();
mqMobile.addEventListener("change", sync);
mqReduce.addEventListener("change", sync);
return () => {
mqMobile.removeEventListener("change", sync);
mqReduce.removeEventListener("change", sync);
};
}, []);
// Mount the WebGL scene when the section approaches the viewport, and pause
// its render loop entirely once it scrolls off-screen (battery / 60fps).
useEffect(() => {
const el = containerRef.current;
if (!el) return;
const mountIo = new IntersectionObserver(
(entries) => {
if (entries.some((e) => e.isIntersecting)) {
setMountScene(true);
// Activate immediately on mount so the render loop starts even on a
// direct scroll-jump (the activeIo below only fires on later changes).
setSceneActive(true);
mountIo.disconnect();
}
},
{ rootMargin: "120% 0px" },
);
const activeIo = new IntersectionObserver(
(entries) => setSceneActive(entries.some((e) => e.isIntersecting)),
{ rootMargin: "10% 0px" },
);
mountIo.observe(el);
activeIo.observe(el);
return () => {
mountIo.disconnect();
activeIo.disconnect();
};
}, []);
// Drive the shared scroll progress with GSAP ScrollTrigger.
// We pin via our own `position: fixed` (toggled by `pinState`) rather than
// GSAP's pin or CSS `position: sticky`. CSS sticky is broken by an ancestor
// (.body-container) using `overflow: hidden`, and GSAP's pin offsets the
// element by the fixed site-header height. A self-managed fixed element
// pins to the viewport top deterministically and ignores ancestor overflow.
useEffect(() => {
const el = containerRef.current;
if (!el) return;
gsap.registerPlugin(ScrollTrigger);
// Smooth scroll (Lenis), driven by a SINGLE rAF source — GSAP's ticker.
// Previously lenis.raf() was called from both a manual requestAnimationFrame
// loop AND gsap.ticker, double-stepping the integrator every frame, which is
// what made scrolling stutter. One source keeps Lenis + ScrollTrigger locked.
const lenis = new Lenis({
duration: 1.05,
easing: (t) => Math.min(1, 1.001 - Math.pow(2, -10 * t)),
orientation: "vertical",
gestureOrientation: "vertical",
smoothWheel: true,
});
lenis.on("scroll", ScrollTrigger.update);
const tickerCb = (time: number) => lenis.raf(time * 1000); // ticker is in seconds, Lenis wants ms
gsap.ticker.add(tickerCb);
gsap.ticker.lagSmoothing(0);
let lastPhase: PhaseKey = "chaos";
let lastPin: "before" | "pinned" | "after" = "before";
const st = ScrollTrigger.create({
trigger: el,
start: "top top",
end: "bottom bottom",
scrub: 0.4,
invalidateOnRefresh: true,
onUpdate: (self) => {
const p = self.progress;
progressRef.current = p;
scroll.set(p);
const next = phaseFromProgress(p);
if (next !== lastPhase) {
lastPhase = next;
setPhase(next);
}
const ns = p <= 0.0002 ? "before" : p >= 0.9998 ? "after" : "pinned";
if (ns !== lastPin) {
lastPin = ns;
setPinState(ns);
}
},
});
const refresh = setTimeout(() => ScrollTrigger.refresh(), 300);
return () => {
clearTimeout(refresh);
st.kill();
gsap.ticker.remove(tickerCb);
lenis.destroy();
};
}, [scroll]);
// Overlay reactions to scroll (no React re-render — direct DOM updates).
const leftOpacity = useTransform(scroll, [0.3, 0.55], [1, 0.32]);
const leftBlur = useTransform(scroll, [0.3, 0.55], [0, 3]);
const leftFilter = useTransform(leftBlur, (b) => `blur(${b}px)`);
const rightOpacity = useTransform(scroll, [0.42, 0.66], [0.36, 1]);
const scanWidth = useTransform(scroll, [0, 1], ["0%", "100%"]);
const scanLineY = useTransform(scroll, [0.2, 0.42], ["8%", "92%"]);
const scanLineOpacity = useTransform(scroll, [0.18, 0.22, 0.42, 0.46], [0, 1, 1, 0]);
const dividerOpacity = useTransform(scroll, [0.45, 0.6], [0.15, 0.75]);
const phaseIndex = PHASE_ORDER.indexOf(phase);
return (
<section
ref={containerRef}
className={`dm-opt is-${pinState}`}
aria-label="AI Logistics Optimization"
>
<div className="dm-opt-sticky">
<div className="dm-opt-card">
{/* Static backdrop (also the canvas loading state) */}
<div className="dm-opt-backdrop" aria-hidden />
{/* 3D scene */}
{mountScene && (
<div className="dm-opt-canvas">
<OptimizationCanvas
progress={progressRef}
reduced={reduced}
isMobile={isMobile}
active={sceneActive}
/>
</div>
)}
{/* Depth vignette */}
<div className="dm-opt-vignette" aria-hidden />
{/* Floating AI Status Badge replacing giant abstract sphere centerpiece */}
<AnimatePresence mode="wait">
{phase !== "chaos" && (
<motion.div
key={phase}
className="dm-opt-floating-badge"
initial={{ scale: 0.85, opacity: 0 }}
animate={{
scale: 1,
opacity: 1,
}}
exit={{
scale: 0.85,
opacity: 0,
}}
transition={{ duration: 0.28, ease: "easeInOut" }}
style={{
position: "absolute",
top: "40%",
left: "50%",
x: "-50%",
y: "-50%",
zIndex: 10,
pointerEvents: "none",
}}
>
<div className="dm-opt-floating-badge__inner">
<span className={`dm-opt-floating-badge__dot is-${phase}`} />
<span className="dm-opt-floating-badge__text">
{PHASE_LABELS[phase]}
</span>
</div>
</motion.div>
)}
</AnimatePresence>
{/* Center scan divider */}
<motion.div
className="dm-opt-divider"
style={{ opacity: dividerOpacity }}
aria-hidden
/>
{/* AI scan sweep line */}
<motion.div
className="dm-opt-scanline"
style={{ top: scanLineY, opacity: scanLineOpacity }}
aria-hidden
/>
{/* UI overlay */}
<div className="dm-opt-ui">
{/* Header */}
<header className="dm-opt-head">
<motion.div
className="dm-opt-eyebrow"
initial={{ opacity: 0, y: 16 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true }}
transition={{ duration: 0.6 }}
>
<span className="dm-opt-dot" /> Doormile AI Control Tower
</motion.div>
<motion.h2
initial={{ opacity: 0, y: 22 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true }}
transition={{ duration: 0.7, delay: 0.05 }}
>
AI Logistics Optimization Engine
</motion.h2>
<motion.p
initial={{ opacity: 0, y: 22 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true }}
transition={{ duration: 0.7, delay: 0.12 }}
>
Watch Doormile&apos;s AI engine transform chaotic logistics into precision-optimized delivery networks reducing distance, fleet size, delays, and cost in real time.
</motion.p>
{/* Workflow step indicators */}
<motion.div
className="dm-opt-steps"
initial={{ opacity: 0, y: 14 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true }}
transition={{ duration: 0.7, delay: 0.18 }}
>
{WORKFLOW_STEPS.map((step, i) => {
const isActive = phaseIndex >= step.activateAt;
const isCurrent = phaseIndex === step.activateAt;
return (
<React.Fragment key={step.label}>
{i > 0 && <span className={`dm-opt-steps__line ${isActive ? 'is-active' : ''}`} />}
<span className={`dm-opt-steps__pill ${isActive ? 'is-active' : ''} ${isCurrent ? 'is-current' : ''}`}>
<span className="dm-opt-steps__icon">{step.icon}</span>
<span className="dm-opt-steps__text">{step.label}</span>
</span>
</React.Fragment>
);
})}
</motion.div>
{/* Scan progress bar */}
<div className="dm-opt-progress">
<div className="dm-opt-progress__track">
<motion.div className="dm-opt-progress__fill" style={{ width: scanWidth }} />
</div>
</div>
</header>
{/* Side comparison panels */}
<div className="dm-opt-compare">
<motion.aside
className="dm-opt-panel dm-opt-panel--bad"
style={{ opacity: leftOpacity, filter: leftFilter }}
>
<div className="dm-opt-panel__badge">
<span className="dm-opt-pulse dm-opt-pulse--red" /> System: Congested
</div>
<h3>Without Optimization</h3>
<ul>
<li><span className="dm-opt-marker dm-opt-marker--x"></span> Chaotic overlapping routes</li>
<li><span className="dm-opt-marker dm-opt-marker--x"></span> Duplicate &amp; idle trips</li>
<li><span className="dm-opt-marker dm-opt-marker--x"></span> 8 vehicles required</li>
<li><span className="dm-opt-marker dm-opt-marker--x"></span> 23 delivery delays</li>
<li><span className="dm-opt-marker dm-opt-marker--x"></span> +18% cost overrun</li>
</ul>
</motion.aside>
<motion.aside
className="dm-opt-panel dm-opt-panel--good"
style={{ opacity: rightOpacity }}
>
<div className="dm-opt-panel__badge dm-opt-panel__badge--good">
<span className="dm-opt-pulse dm-opt-pulse--green" /> System: Optimized
</div>
<h3>With Doormile AI</h3>
<ul>
<li><span className="dm-opt-marker dm-opt-marker--ok"></span> Optimized route clusters</li>
<li><span className="dm-opt-marker dm-opt-marker--ok"></span> Intelligent vehicle assignment</li>
<li><span className="dm-opt-marker dm-opt-marker--ok"></span> Multi-trip &amp; EV planning</li>
<li><span className="dm-opt-marker dm-opt-marker--ok"></span> Zero delivery delays</li>
<li><span className="dm-opt-marker dm-opt-marker--ok"></span> 18% cost saved</li>
<li><span className="dm-opt-marker dm-opt-marker--ok"></span> Carbon footprint reduced</li>
</ul>
</motion.aside>
</div>
{/* KPI metrics */}
<div className="dm-opt-foot">
<MetricsPanel scroll={scroll} />
{/* Bottom insight bar */}
<motion.div
className="dm-opt-insight"
initial={{ opacity: 0, y: 12 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true }}
transition={{ duration: 0.6, delay: 0.3 }}
>
<span className="dm-opt-insight__dot" />
<span className="dm-opt-insight__text">Live Analytics: <strong>{orders} Orders</strong></span>
<span className="dm-opt-insight__sep" />
<span className="dm-opt-insight__text">AI Accuracy: <strong>{accuracy}%</strong></span>
<span className="dm-opt-insight__sep" />
<span className="dm-opt-insight__text">Fleet: <strong>{activeVehicles}/5 EV Active</strong></span>
<span className="dm-opt-insight__sep" />
<span className="dm-opt-insight__text">Route Health: <strong>{routeHealth}%</strong></span>
<span className="dm-opt-insight__sep" />
<span className="dm-opt-insight__text">Carbon: <strong>{carbon}%</strong></span>
</motion.div>
</div>
</div>
</div>
</div>
<style>{styles}</style>
</section>
);
}
const styles = `
/* ===== OUTER SECTION: Transparent so the card floats ===== */
.dm-opt {
position: relative;
height: 230vh;
background: transparent;
margin-bottom: 120px;
}
.dm-opt-sticky {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100vh;
overflow: hidden;
background: transparent;
}
.dm-opt.is-pinned .dm-opt-sticky { position: fixed; top: 0; left: 0; }
.dm-opt.is-after .dm-opt-sticky { position: absolute; top: auto; bottom: 0; }
/* ===== FLOATING CARD — the only colored surface ===== */
.dm-opt-card {
position: absolute !important;
top: 110px !important;
left: 40px !important;
right: 40px !important;
bottom: 24px !important;
border-radius: 60px !important;
overflow: hidden !important;
background: linear-gradient(165deg, #06101f 0%, #020617 35%, #040d1c 70%, #030a18 100%) !important;
border: 1.5px solid ${rgba("#ffffff", 0.08)} !important;
box-shadow:
0 0 0 1px ${rgba(COLORS.cyan, 0.04)},
0 4px 30px -4px rgba(0, 0, 0, 0.7),
0 20px 80px -20px rgba(0, 0, 0, 0.6),
0 0 120px -30px ${rgba(COLORS.cyan, 0.08)},
inset 0 1px 0 ${rgba("#ffffff", 0.06)},
inset 0 -1px 0 ${rgba("#ffffff", 0.02)} !important;
box-sizing: border-box !important;
}
/* Animated subtle grid pattern */
.dm-opt-card::before {
content: ""; position: absolute; inset: 0; z-index: 0; pointer-events: none;
opacity: 0.035;
background-image:
linear-gradient(${rgba(COLORS.cyan, 0.5)} 1px, transparent 1px),
linear-gradient(90deg, ${rgba(COLORS.cyan, 0.5)} 1px, transparent 1px);
background-size: 60px 60px;
animation: dmOptGridDrift 25s linear infinite;
}
@keyframes dmOptGridDrift {
0% { background-position: 0 0; }
100% { background-position: 60px 60px; }
}
/* Radial center glow behind 3D scene */
.dm-opt-card::after {
content: ""; position: absolute; inset: 0; z-index: 0; pointer-events: none;
background:
radial-gradient(ellipse 50% 45% at 50% 48%, ${rgba(COLORS.cyan, 0.08)} 0%, transparent 70%),
radial-gradient(ellipse 60% 40% at 50% 90%, ${rgba(COLORS.green, 0.05)} 0%, transparent 60%),
radial-gradient(ellipse 80% 50% at 50% 10%, ${rgba(COLORS.cyan, 0.04)} 0%, transparent 50%);
}
@media (max-width: 1024px) {
.dm-opt-card {
top: 96px !important;
left: 20px !important;
right: 20px !important;
bottom: 16px !important;
border-radius: 42px !important;
}
}
@media (max-width: 767px) {
.dm-opt-card {
top: 86px !important;
left: 10px !important;
right: 10px !important;
bottom: 10px !important;
border-radius: 28px !important;
}
}
/* ===== INNER LAYERS ===== */
.dm-opt-backdrop {
position: absolute;
inset: 0; z-index: 0;
background:
radial-gradient(100% 70% at 50% 6%, ${rgba(COLORS.cyan, 0.06)} 0%, transparent 55%),
radial-gradient(80% 60% at 50% 100%, ${rgba(COLORS.green, 0.05)} 0%, transparent 55%);
}
.dm-opt-canvas { position: absolute; inset: 0; z-index: 1; }
.dm-opt-canvas canvas { display: block; }
.dm-opt-vignette {
position: absolute; inset: 0; z-index: 2; pointer-events: none;
background:
radial-gradient(110% 90% at 50% 50%, transparent 48%, ${rgba("#020617", 0.88)} 100%),
linear-gradient(180deg, ${rgba("#020617", 0.6)} 0%, transparent 20%, transparent 65%, ${rgba("#020617", 0.92)} 100%);
}
.dm-opt-divider {
position: absolute; left: 50%; top: 14%; bottom: 28%;
width: 1px; z-index: 3; pointer-events: none; transform: translateX(-0.5px);
background: linear-gradient(180deg, transparent, ${rgba(COLORS.cyan, 0.6)}, transparent);
box-shadow: 0 0 16px ${rgba(COLORS.cyan, 0.4)};
}
.dm-opt-scanline {
position: absolute; left: 6%; right: 6%; height: 2px; z-index: 3; pointer-events: none;
background: linear-gradient(90deg, transparent, ${COLORS.cyan}, transparent);
box-shadow: 0 0 22px ${rgba(COLORS.cyan, 0.8)};
}
/* ===== FLOATING AI STATUS BADGE ===== */
.dm-opt-floating-badge {
pointer-events: none;
}
.dm-opt-floating-badge__inner {
display: inline-flex;
align-items: center;
gap: 10px;
padding: 8px 18px;
border-radius: 999px;
background: ${rgba(COLORS.ink, 0.85)};
border: 1.5px solid ${rgba(COLORS.cyan, 0.3)};
box-shadow:
0 10px 30px -5px rgba(0, 0, 0, 0.65),
0 0 24px -2px ${rgba(COLORS.cyan, 0.18)},
inset 0 1px 0 rgba(255, 255, 255, 0.08);
backdrop-filter: blur(14px);
-webkit-backdrop-filter: blur(14px);
transition: border-color 0.4s ease, box-shadow 0.4s ease;
}
.dm-opt-floating-badge__dot {
width: 7px;
height: 7px;
border-radius: 50%;
background: ${COLORS.cyan};
box-shadow: 0 0 10px ${COLORS.cyan};
animation: dmOptPulse 1.4s ease-in-out infinite;
transition: background 0.4s ease, box-shadow 0.4s ease;
}
.dm-opt-floating-badge__dot.is-scan {
background: ${COLORS.cyan};
box-shadow: 0 0 10px ${COLORS.cyan};
}
.dm-opt-floating-badge__dot.is-dissolve,
.dm-opt-floating-badge__dot.is-optimize {
background: ${COLORS.amber};
box-shadow: 0 0 10px ${COLORS.amber};
}
.dm-opt-floating-badge__dot.is-reorganize {
background: #C084FC;
box-shadow: 0 0 10px #C084FC;
}
.dm-opt-floating-badge__dot.is-metrics {
background: ${COLORS.green};
box-shadow: 0 0 10px ${COLORS.green};
}
.dm-opt-floating-badge__text {
font-family: var(--font-space-grotesk), var(--font-manrope), system-ui, sans-serif;
font-size: 10px;
font-weight: 700;
letter-spacing: 0.16em;
text-transform: uppercase;
color: #F8FAFC;
text-shadow: 0 2px 4px rgba(0, 0, 0, 0.5);
}
/* ===== UI OVERLAY ===== */
.dm-opt-ui { position: absolute; inset: 0; z-index: 4; pointer-events: none; }
.dm-opt-ui h2, .dm-opt-ui h3, .dm-opt-metric__value, .dm-opt-eyebrow, .dm-opt-phase {
font-family: var(--font-space-grotesk), var(--font-manrope), system-ui, sans-serif;
}
/* ===== HEADER — compact, no dead space ===== */
.dm-opt-head {
position: absolute; top: clamp(18px, 3vh, 36px); left: 50%;
transform: translateX(-50%); width: min(640px, 90vw); text-align: center;
}
.dm-opt-eyebrow {
display: inline-flex; align-items: center; gap: 7px;
font-size: 11px; letter-spacing: 0.22em; text-transform: uppercase;
color: ${COLORS.cyan}; padding: 5px 14px; border-radius: 999px;
background: ${rgba(COLORS.cyan, 0.06)}; border: 1px solid ${rgba(COLORS.cyan, 0.25)};
backdrop-filter: blur(8px);
}
.dm-opt-dot { width: 6px; height: 6px; border-radius: 50%; background: ${COLORS.cyan}; box-shadow: 0 0 10px ${COLORS.cyan}; }
.dm-opt .dm-opt-head h2 {
margin: 8px 0 4px !important; padding: 0 !important; color: #F8FAFC !important;
font-weight: 700 !important; text-transform: none !important;
font-size: clamp(22px, 2.4vw, 36px) !important; line-height: 1.1 !important;
letter-spacing: -0.015em !important;
}
.dm-opt .dm-opt-head p {
margin: 0 auto !important; padding: 0 !important; color: ${COLORS.textDim} !important;
max-width: 440px; font-size: clamp(11px, 1vw, 13px) !important; line-height: 1.45 !important;
}
/* ===== WORKFLOW STEPS ===== */
.dm-opt-steps {
display: flex; align-items: center; justify-content: center; gap: 0;
margin-top: 12px; flex-wrap: wrap;
}
.dm-opt-steps__pill {
display: inline-flex; align-items: center; gap: 4px;
padding: 4px 10px; border-radius: 999px; font-weight: 600;
letter-spacing: 0.04em; text-transform: uppercase;
color: ${COLORS.slate};
background: ${rgba(COLORS.ink, 0.5)};
border: 1px solid ${rgba(COLORS.slate, 0.2)};
backdrop-filter: blur(6px);
transition: all 0.45s cubic-bezier(0.22, 1, 0.36, 1);
}
.dm-opt-steps__pill.is-active {
color: #E2E8F0;
background: ${rgba(COLORS.cyan, 0.10)};
border-color: ${rgba(COLORS.cyan, 0.35)};
box-shadow: 0 0 18px -6px ${rgba(COLORS.cyan, 0.5)};
}
.dm-opt-steps__pill.is-current {
color: ${COLORS.cyan};
border-color: ${rgba(COLORS.cyan, 0.6)};
box-shadow: 0 0 24px -4px ${rgba(COLORS.cyan, 0.6)};
}
.dm-opt-steps__icon { font-size: 11px; }
.dm-opt-steps__text { font-size: 9.5px; }
.dm-opt-steps__line {
display: block; width: 16px; height: 1px; margin: 0 2px;
background: ${rgba(COLORS.slate, 0.3)};
transition: background 0.45s ease;
}
.dm-opt-steps__line.is-active {
background: linear-gradient(90deg, ${COLORS.cyan}, ${COLORS.green});
box-shadow: 0 0 6px ${rgba(COLORS.cyan, 0.5)};
}
/* ===== PROGRESS BAR ===== */
.dm-opt-progress { margin-top: 10px; }
.dm-opt-progress__track {
height: 2px; border-radius: 999px; overflow: hidden;
background: ${rgba(COLORS.cyan, 0.10)}; max-width: 420px; margin: 0 auto;
}
.dm-opt-progress__fill {
height: 100%; border-radius: 999px;
background: linear-gradient(90deg, ${COLORS.cyan}, ${COLORS.green});
box-shadow: 0 0 12px ${rgba(COLORS.cyan, 0.6)};
}
.dm-opt-status {
display: inline-flex; align-items: center; gap: 8px; margin-top: 8px;
padding: 4px 11px; border-radius: 999px;
background: ${rgba(COLORS.ink, 0.55)}; border: 1px solid ${rgba(COLORS.cyan, 0.18)};
backdrop-filter: blur(8px);
}
.dm-opt-status__dot {
width: 5px; height: 5px; border-radius: 50%; background: ${COLORS.cyan};
box-shadow: 0 0 8px ${COLORS.cyan}; animation: dmOptPulse 1.4s ease-in-out infinite;
}
.dm-opt-status__label {
font-size: 9.5px; letter-spacing: 0.14em; text-transform: uppercase; color: #E2E8F0; font-weight: 600;
}
.dm-opt-status__step { font-size: 9.5px; color: ${COLORS.cyan}; font-weight: 600; }
/* ===== COMPARE PANELS — tighter, stronger ===== */
.dm-opt-compare {
position: absolute; top: 50%; left: 0; right: 0; transform: translateY(-50%);
display: flex; justify-content: space-between; align-items: center;
gap: 12px; padding: 0 clamp(12px, 2.5vw, 36px);
}
.dm-opt-panel {
pointer-events: auto; width: clamp(230px, 26vw, 340px);
padding: 18px 20px; border-radius: 20px;
background: ${rgba(COLORS.ink, 0.72)};
border: 1px solid ${rgba(COLORS.slate, 0.22)};
backdrop-filter: blur(20px); -webkit-backdrop-filter: blur(20px);
}
.dm-opt-panel--bad {
border-color: ${rgba(COLORS.red, 0.45)};
box-shadow:
0 0 30px -8px ${rgba(COLORS.red, 0.35)},
0 20px 50px -20px ${rgba(COLORS.red, 0.25)},
inset 0 1px 0 ${rgba(COLORS.red, 0.08)};
}
.dm-opt-panel--good {
border-color: ${rgba(COLORS.green, 0.45)};
box-shadow:
0 0 30px -8px ${rgba(COLORS.green, 0.35)},
0 20px 50px -20px ${rgba(COLORS.green, 0.25)},
inset 0 1px 0 ${rgba(COLORS.green, 0.08)};
}
.dm-opt .dm-opt-panel h3 {
margin: 10px 0 12px !important; padding: 0 !important; color: #F1F5F9 !important;
font-size: clamp(15px, 1.4vw, 19px) !important; font-weight: 600 !important;
line-height: 1.15 !important; text-transform: none !important; letter-spacing: -0.01em !important;
}
.dm-opt .dm-opt-panel ul { list-style: none !important; margin: 0 !important; padding: 0 !important; display: grid; gap: 7px; }
.dm-opt .dm-opt-panel li {
position: relative; padding-left: 0 !important; margin: 0 !important;
color: ${COLORS.textDim} !important; font-size: 12.5px !important; line-height: 1.35 !important;
display: flex; align-items: center; gap: 8px;
}
.dm-opt .dm-opt-panel li::marker { content: "" !important; }
/* Remove old dot pseudo — now using ✖/✔ markers */
.dm-opt .dm-opt-panel li::before { content: none !important; display: none !important; }
/* ✖/✔ markers */
.dm-opt-marker {
display: inline-flex; align-items: center; justify-content: center;
width: 18px; height: 18px; border-radius: 6px; flex-shrink: 0;
font-size: 10px; font-weight: 700; line-height: 1;
}
.dm-opt-marker--x {
background: ${rgba(COLORS.red, 0.15)};
color: ${COLORS.red};
border: 1px solid ${rgba(COLORS.red, 0.35)};
box-shadow: 0 0 8px ${rgba(COLORS.red, 0.3)};
}
.dm-opt-marker--ok {
background: ${rgba(COLORS.green, 0.15)};
color: ${COLORS.green};
border: 1px solid ${rgba(COLORS.green, 0.35)};
box-shadow: 0 0 8px ${rgba(COLORS.green, 0.3)};
}
.dm-opt-panel__badge {
display: inline-flex; align-items: center; gap: 7px;
font-size: 10px; letter-spacing: 0.16em; text-transform: uppercase; font-weight: 700;
color: ${COLORS.red}; padding: 5px 10px; border-radius: 999px;
background: ${rgba(COLORS.red, 0.1)}; border: 1px solid ${rgba(COLORS.red, 0.35)};
}
.dm-opt-panel__badge--good { color: ${COLORS.green}; background: ${rgba(COLORS.green, 0.1)}; border-color: ${rgba(COLORS.green, 0.35)}; }
.dm-opt-pulse { width: 6px; height: 6px; border-radius: 50%; animation: dmOptPulse 1.4s ease-in-out infinite; }
.dm-opt-pulse--red { background: ${COLORS.red}; box-shadow: 0 0 10px ${COLORS.red}; }
.dm-opt-pulse--green { background: ${COLORS.green}; box-shadow: 0 0 10px ${COLORS.green}; }
@keyframes dmOptPulse { 0%,100% { transform: scale(1); opacity: 1; } 50% { transform: scale(1.5); opacity: 0.5; } }
/* ===== METRICS ===== */
.dm-opt-foot {
position: absolute; left: 0; right: 0; bottom: clamp(12px, 2.5vh, 28px);
padding: 0 clamp(12px, 3vw, 36px);
}
.dm-opt-metrics {
pointer-events: auto; display: grid; grid-template-columns: repeat(5, 1fr);
gap: 10px; max-width: 1100px; margin: 0 auto;
}
.dm-opt-metric {
position: relative; padding: 14px 14px 12px; border-radius: 16px;
background: ${rgba(COLORS.ink, 0.72)};
border: 1px solid ${rgba(COLORS.slate, 0.25)};
backdrop-filter: blur(20px); -webkit-backdrop-filter: blur(20px);
overflow: hidden;
opacity: 0; transform: translateY(22px);
animation: dmOptCardIn 0.6s cubic-bezier(0.22, 1, 0.36, 1) forwards;
box-shadow: 0 8px 32px -8px rgba(0, 0, 0, 0.5), inset 0 1px 0 ${rgba("#ffffff", 0.04)};
transition: transform 0.4s cubic-bezier(0.22, 1, 0.36, 1), border-color 0.4s ease, box-shadow 0.4s ease, background 0.4s ease !important;
cursor: pointer;
}
.dm-opt-metric:hover {
transform: translateY(-5px) scale(1.028) !important;
background: ${rgba(COLORS.ink, 0.9)} !important;
border-color: ${rgba(COLORS.cyan, 0.52)} !important;
box-shadow:
0 18px 48px -8px ${rgba(COLORS.cyan, 0.25)},
0 4px 12px -2px ${rgba(COLORS.cyan, 0.12)},
inset 0 1px 0 rgba(255, 255, 255, 0.1) !important;
}
.dm-opt-metric:hover .dm-opt-metric__sparkline {
opacity: 0.72 !important;
}
.dm-opt-metric__sparkline {
transition: opacity 0.4s ease;
}
@keyframes dmOptCardIn { to { opacity: 1; transform: translateY(0); } }
.dm-opt-metric__top { display: flex; align-items: center; justify-content: space-between; }
.dm-opt-metric__label {
font-size: 10px; letter-spacing: 0.04em; color: ${COLORS.textDim}; text-transform: uppercase;
}
.dm-opt-metric__arrow { font-size: 11px; }
.dm-opt-metric__value { margin-top: 6px; font-size: clamp(20px, 2.8vw, 32px); font-weight: 700; line-height: 1; }
.dm-opt-metric__bar { margin-top: 10px; height: 3px; border-radius: 999px; background: ${rgba(COLORS.slate, 0.2)}; overflow: hidden; }
.dm-opt-metric__fill { height: 100%; border-radius: 999px; transform-origin: left center; }
/* ===== BOTTOM INSIGHT BAR ===== */
.dm-opt-insight {
display: flex; align-items: center; justify-content: center; gap: 8px;
max-width: 760px; margin: 10px auto 0;
padding: 7px 18px; border-radius: 999px;
background: ${rgba(COLORS.ink, 0.6)};
border: 1px solid ${rgba(COLORS.cyan, 0.12)};
backdrop-filter: blur(14px); -webkit-backdrop-filter: blur(14px);
pointer-events: auto;
}
.dm-opt-insight__dot {
width: 6px; height: 6px; border-radius: 50%; flex-shrink: 0;
background: ${COLORS.green};
box-shadow: 0 0 12px ${COLORS.green};
animation: dmOptPulse 2s ease-in-out infinite;
}
.dm-opt-insight__text {
font-size: 10.5px; color: ${COLORS.textDim}; line-height: 1.3;
letter-spacing: 0.03em; font-weight: 500;
}
.dm-opt-insight__text strong {
color: #E2E8F0; font-weight: 700;
}
.dm-opt-insight__sep {
width: 1px; height: 12px; flex-shrink: 0;
background: ${rgba(COLORS.slate, 0.35)};
}
/* ===== RESPONSIVE ===== */
@media (max-width: 1024px) {
.dm-opt-panel { width: clamp(190px, 30vw, 280px); padding: 14px 16px; }
.dm-opt-panel li { font-size: 11.5px; }
.dm-opt-marker { width: 16px; height: 16px; font-size: 9px; }
}
@media (max-width: 767px) {
.dm-opt { height: 200vh; }
.dm-opt-card {
top: 8px !important; left: 8px !important; right: 8px !important; bottom: 8px !important;
border-radius: 24px !important;
}
.dm-opt-compare {
top: auto; bottom: 148px; transform: none;
flex-direction: row; align-items: stretch; gap: 6px; padding: 0 10px;
}
.dm-opt-panel { width: 50%; padding: 10px 11px; border-radius: 14px; }
.dm-opt-panel ul { gap: 4px; }
.dm-opt-panel li { font-size: 10px; }
.dm-opt-panel li::before { display: none !important; }
.dm-opt-marker { width: 14px; height: 14px; font-size: 8px; border-radius: 4px; }
.dm-opt-panel h3 { margin: 6px 0 5px; font-size: 14px; }
.dm-opt-panel__badge { font-size: 8px; padding: 3px 7px; }
.dm-opt-metrics { grid-template-columns: repeat(5, 1fr); gap: 4px; }
.dm-opt-metric { padding: 7px 5px; border-radius: 10px; }
.dm-opt-metric__label { font-size: 7.5px; letter-spacing: 0; }
.dm-opt-metric__value { font-size: 14px; }
.dm-opt-metric__bar { margin-top: 6px; }
.dm-opt-head h2 { font-size: 22px; margin: 6px 0 4px; }
.dm-opt-head p { font-size: 10.5px; }
.dm-opt-phases { display: none; }
.dm-opt-steps { gap: 0; }
.dm-opt-steps__pill { padding: 3px 6px; }
.dm-opt-steps__icon { font-size: 9px; }
.dm-opt-steps__text { font-size: 7.5px; }
.dm-opt-steps__line { width: 6px; }
.dm-opt-insight { padding: 5px 10px; gap: 5px; }
.dm-opt-insight__text { font-size: 8.5px; }
.dm-opt-insight__sep { height: 10px; }
}
@media (prefers-reduced-motion: reduce) {
.dm-opt-pulse { animation: none; }
.dm-opt-metric { animation: none; opacity: 1; transform: none; }
.dm-opt-insight__dot { animation: none; }
.dm-opt-card::before { animation: none; }
}
`;

View File

@@ -0,0 +1,514 @@
"use client";
import React, { useMemo, useRef } from "react";
import { useFrame } from "@react-three/fiber";
import * as THREE from "three";
import { COLORS } from "./constants";
import { buildRoutes } from "./routes";
import { smoothstep, seeded } from "./math";
type Props = {
progress: React.RefObject<number>;
reduced?: boolean;
isMobile?: boolean;
};
const RADIAL = 6;
const TUBULAR = 120;
type TubeHandle = { geo: THREE.TubeGeometry; mat: THREE.MeshBasicMaterial };
type PulsingRingProps = {
position: THREE.Vector3;
color: string;
maxScale?: number;
pulseSpeed?: number;
};
/**
* Clean floating radar ring that pulses outwards and fades.
*/
function PulsingRing({ position, color, maxScale = 2.2, pulseSpeed = 2.4 }: PulsingRingProps) {
const ref = useRef<THREE.Mesh>(null);
const matRef = useRef<THREE.MeshBasicMaterial>(null);
useFrame((state) => {
if (!ref.current || !matRef.current) return;
const t = state.clock.elapsedTime;
const cycle = (t * (pulseSpeed / 4)) % 1; // 0 -> 1 loop
const s = 0.4 + cycle * (maxScale - 0.4);
ref.current.scale.setScalar(s);
matRef.current.opacity = Math.sin((1 - cycle) * Math.PI) * 0.72;
});
return (
<mesh ref={ref} position={[position.x, 0.03, position.z]} rotation={[-Math.PI / 2, 0, 0]}>
<ringGeometry args={[0.22, 0.28, 24]} />
<meshBasicMaterial
ref={matRef}
color={color}
transparent
opacity={0}
depthWrite={false}
blending={THREE.AdditiveBlending}
/>
</mesh>
);
}
const PulsingRingMemo = React.memo(PulsingRing);
/* ── Chaotic Route Flow Particles & Chevrons ── */
const CHAOS_TMP_POS = new THREE.Vector3();
const CHAOS_TMP_TAN = new THREE.Vector3();
function ChaoticFlow({ progress, chaoticCurves }: {
progress: React.RefObject<number>;
chaoticCurves: THREE.CatmullRomCurve3[];
}) {
const particlesPerCurve = 4;
const count = chaoticCurves.length * particlesPerCurve;
const particleRefs = useRef<(THREE.Mesh | null)[]>([]);
const chevronRefs = useRef<(THREE.Group | null)[]>([]);
const particleOffsets = useMemo(
() => Array.from({ length: count }, (_, i) => seeded(i * 13 + 3)),
[count]
);
const chevronOffsets = useMemo(
() => Array.from({ length: chaoticCurves.length * 2 }, (_, i) => (i % 2) / 2),
[chaoticCurves.length]
);
useFrame((state) => {
const p = progress.current ?? 0;
const vis = 1 - smoothstep(0.48, 0.60, p);
const isVisible = vis > 0.01;
particleRefs.current.forEach(m => { if (m) m.visible = isVisible; });
chevronRefs.current.forEach(g => { if (g) g.visible = isVisible; });
if (!isVisible) return;
const t = state.clock.elapsedTime;
// Animate tiny red flow particles
for (let i = 0; i < count; i++) {
const mesh = particleRefs.current[i];
if (!mesh) continue;
const curveIdx = Math.floor(i / particlesPerCurve);
const curve = chaoticCurves[curveIdx];
const speed = 0.05 + seeded(i * 9) * 0.03;
const u = (particleOffsets[i] + t * speed) % 1;
curve.getPointAt(u, CHAOS_TMP_POS);
mesh.position.copy(CHAOS_TMP_POS);
const mat = mesh.material as THREE.MeshBasicMaterial;
mat.opacity = vis * 0.6 * Math.sin(u * Math.PI);
}
// Animate red chevrons (erratic flow)
for (let i = 0; i < chaoticCurves.length * 2; i++) {
const group = chevronRefs.current[i];
if (!group) continue;
const curveIdx = Math.floor(i / 2);
const curve = chaoticCurves[curveIdx];
const u = (chevronOffsets[i] + t * 0.04) % 1;
curve.getPointAt(u, CHAOS_TMP_POS);
curve.getTangentAt(u, CHAOS_TMP_TAN);
group.position.copy(CHAOS_TMP_POS);
group.rotation.y = Math.atan2(CHAOS_TMP_TAN.x, CHAOS_TMP_TAN.z);
group.traverse((o) => {
const m = (o as THREE.Mesh).material as THREE.MeshBasicMaterial | undefined;
if (m && "opacity" in m) {
m.opacity = vis * 0.5 * Math.sin(u * Math.PI);
}
});
}
});
return (
<group>
{/* Chaotic Red Flow Particles */}
{Array.from({ length: count }).map((_, i) => (
<mesh
key={`cp-${i}`}
ref={(el) => { particleRefs.current[i] = el; }}
visible={false}
>
<sphereGeometry args={[0.06, 6, 6]} />
<meshBasicMaterial
color={COLORS.red}
transparent
opacity={0}
depthWrite={false}
blending={THREE.AdditiveBlending}
/>
</mesh>
))}
{/* Chaotic Red Chevrons */}
{Array.from({ length: chaoticCurves.length * 2 }).map((_, i) => (
<group
key={`cc-${i}`}
ref={(el) => { chevronRefs.current[i] = el; }}
visible={false}
>
<mesh rotation={[Math.PI / 2, 0, 0]}>
<coneGeometry args={[0.06, 0.16, 4]} />
<meshBasicMaterial
color={COLORS.red}
transparent
opacity={0}
depthWrite={false}
blending={THREE.AdditiveBlending}
/>
</mesh>
</group>
))}
</group>
);
}
const ChaoticFlowMemo = React.memo(ChaoticFlow);
/* ── Optimized Route Flow Particles, Chevrons & Delivery Pulses ── */
const OPT_TMP_POS = new THREE.Vector3();
const OPT_TMP_TAN = new THREE.Vector3();
function OptimizedFlow({ progress, optimizedCurves }: {
progress: React.RefObject<number>;
optimizedCurves: THREE.CatmullRomCurve3[];
}) {
const particlesPerCurve = 6;
const count = optimizedCurves.length * particlesPerCurve;
const particleRefs = useRef<(THREE.Mesh | null)[]>([]);
const chevronRefs = useRef<(THREE.Group | null)[]>([]);
const pulseRefs = useRef<(THREE.Mesh | null)[]>([]);
const particleOffsets = useMemo(
() => Array.from({ length: count }, (_, i) => seeded(i * 17 + 4)),
[count]
);
const chevronOffsets = useMemo(
() => Array.from({ length: optimizedCurves.length * 3 }, (_, i) => (i % 3) / 3),
[optimizedCurves.length]
);
useFrame((state) => {
const p = progress.current ?? 0;
const vis = smoothstep(0.52, 0.70, p);
const isVisible = vis > 0.01;
particleRefs.current.forEach(m => { if (m) m.visible = isVisible; });
chevronRefs.current.forEach(g => { if (g) g.visible = isVisible; });
pulseRefs.current.forEach(m => { if (m) m.visible = isVisible; });
if (!isVisible) return;
const t = state.clock.elapsedTime;
// 1. Animate smooth green/cyan flow particles
for (let i = 0; i < count; i++) {
const mesh = particleRefs.current[i];
if (!mesh) continue;
const curveIdx = Math.floor(i / particlesPerCurve);
const curve = optimizedCurves[curveIdx];
const speed = 0.08 + seeded(i * 11) * 0.04;
const u = (particleOffsets[i] + t * speed) % 1;
curve.getPointAt(u, OPT_TMP_POS);
mesh.position.copy(OPT_TMP_POS);
const mat = mesh.material as THREE.MeshBasicMaterial;
mat.opacity = vis * 0.75 * Math.sin(u * Math.PI);
const s = 0.04 + Math.sin(t * 6 + i) * 0.012;
mesh.scale.setScalar(s / 0.05);
}
// 2. Animate direction chevrons (fast, steady green chevrons)
for (let i = 0; i < optimizedCurves.length * 3; i++) {
const group = chevronRefs.current[i];
if (!group) continue;
const curveIdx = Math.floor(i / 3);
const curve = optimizedCurves[curveIdx];
const u = (chevronOffsets[i] + t * 0.16) % 1;
curve.getPointAt(u, OPT_TMP_POS);
curve.getTangentAt(u, OPT_TMP_TAN);
group.position.copy(OPT_TMP_POS);
group.rotation.y = Math.atan2(OPT_TMP_TAN.x, OPT_TMP_TAN.z);
group.rotation.x = Math.atan2(OPT_TMP_TAN.y, Math.sqrt(OPT_TMP_TAN.x * OPT_TMP_TAN.x + OPT_TMP_TAN.z * OPT_TMP_TAN.z)) - Math.PI / 2;
group.traverse((o) => {
const m = (o as THREE.Mesh).material as THREE.MeshBasicMaterial | undefined;
if (m && "opacity" in m) {
m.opacity = vis * 0.85 * Math.sin(u * Math.PI);
}
});
}
// 3. Animate expanding delivery pulses (shooting data pulses)
for (let i = 0; i < optimizedCurves.length; i++) {
const mesh = pulseRefs.current[i];
if (!mesh) continue;
const curve = optimizedCurves[i];
const u = (t * 0.55 + i * 0.2) % 1;
curve.getPointAt(u, OPT_TMP_POS);
mesh.position.copy(OPT_TMP_POS);
const mat = mesh.material as THREE.MeshBasicMaterial;
mat.opacity = vis * 0.9 * Math.sin(u * Math.PI);
const s = 0.08 + Math.sin(t * 12 + i) * 0.02;
mesh.scale.setScalar(s / 0.07);
}
});
return (
<group>
{/* Tiny Cyan/Green Flow Particles */}
{Array.from({ length: count }).map((_, i) => {
const color = (Math.floor(i / particlesPerCurve) % 2 === 0) ? COLORS.cyan : COLORS.green;
return (
<mesh
key={`op-${i}`}
ref={(el) => { particleRefs.current[i] = el; }}
visible={false}
>
<sphereGeometry args={[0.05, 8, 8]} />
<meshBasicMaterial
color={color}
transparent
opacity={0}
depthWrite={false}
blending={THREE.AdditiveBlending}
/>
</mesh>
);
})}
{/* Glowing Chevrons */}
{Array.from({ length: optimizedCurves.length * 3 }).map((_, i) => {
const color = (Math.floor(i / 3) % 2 === 0) ? COLORS.cyan : COLORS.green;
return (
<group
key={`oc-${i}`}
ref={(el) => { chevronRefs.current[i] = el; }}
visible={false}
>
<mesh rotation={[Math.PI / 2, 0, 0]}>
<coneGeometry args={[0.07, 0.20, 4]} />
<meshBasicMaterial
color={color}
transparent
opacity={0}
depthWrite={false}
blending={THREE.AdditiveBlending}
/>
</mesh>
</group>
);
})}
{/* Large Delivery Pulses */}
{optimizedCurves.map((_, i) => {
const color = (i % 2 === 0) ? COLORS.cyan : COLORS.green;
return (
<mesh
key={`pulse-${i}`}
ref={(el) => { pulseRefs.current[i] = el; }}
visible={false}
>
<sphereGeometry args={[0.07, 12, 12]} />
<meshBasicMaterial
color={color}
transparent
opacity={0}
depthWrite={false}
blending={THREE.AdditiveBlending}
/>
</mesh>
);
})}
</group>
);
}
const OptimizedFlowMemo = React.memo(OptimizedFlow);
/**
* Glowing route network. Chaotic red routes are pre-drawn then "erased" via
* geometry draw-range during the dissolve phase; optimized cyan/green routes
* draw themselves in during the optimize phase. Delivery nodes flip red→green.
*/
function RouteSystem({ progress, reduced = false, isMobile = false }: Props) {
const { chaotic, optimized, chaosNodes, optimizedNodes } = useMemo(
() => buildRoutes(),
[],
);
const chaosRefs = useRef<TubeHandle[]>([]);
const optRefs = useRef<TubeHandle[]>([]);
const chaosNodeGroup = useRef<THREE.Group>(null);
const optNodeGroup = useRef<THREE.Group>(null);
const registerChaos = (i: number) => (geo: THREE.TubeGeometry | null) => {
if (geo) chaosRefs.current[i] = { ...(chaosRefs.current[i] ?? {}), geo } as TubeHandle;
};
const registerChaosMat = (i: number) => (mat: THREE.MeshBasicMaterial | null) => {
if (mat) chaosRefs.current[i] = { ...(chaosRefs.current[i] ?? {}), mat } as TubeHandle;
};
const registerOpt = (i: number) => (geo: THREE.TubeGeometry | null) => {
if (geo) optRefs.current[i] = { ...(optRefs.current[i] ?? {}), geo } as TubeHandle;
};
const registerOptMat = (i: number) => (mat: THREE.MeshBasicMaterial | null) => {
if (mat) optRefs.current[i] = { ...(optRefs.current[i] ?? {}), mat } as TubeHandle;
};
const setDraw = (geo: THREE.TubeGeometry, frac: number) => {
if (!geo.index) return;
const total = geo.index.count;
const step = RADIAL * 6;
const count = Math.max(0, Math.floor((total * frac) / step) * step);
geo.setDrawRange(0, count);
};
useFrame((state) => {
const p = progress.current ?? 0;
const t = state.clock.elapsedTime;
// Chaotic routes: full → erased during dissolve, with warning flicker.
const chaosDraw = 1 - smoothstep(0.4, 0.56, p);
const chaosBase = (1 - smoothstep(0.38, 0.55, p)) * 0.85;
const flicker = 0.7 + Math.sin(t * 7) * 0.18;
for (const h of chaosRefs.current) {
if (!h?.geo) continue;
setDraw(h.geo, chaosDraw);
if (h.mat) h.mat.opacity = chaosBase * flicker;
}
// Optimized routes: draw-in during optimize phase + flowing glow.
const optDraw = smoothstep(0.55, 0.74, p);
const optBase = smoothstep(0.55, 0.66, p);
for (let i = 0; i < optRefs.current.length; i++) {
const h = optRefs.current[i];
if (!h?.geo) continue;
setDraw(h.geo, optDraw);
if (h.mat) {
// Keep the tube a crisp, saturated energy line rather than a
// bloom-blown white slab — the flow energy comes from the pulses + trails.
const flow = 0.52 + Math.sin(t * 3 - i * 0.7) * 0.12;
h.mat.opacity = optBase * flow;
}
}
// Delivery nodes
if (chaosNodeGroup.current) {
const s = 0.0001 + (1 - smoothstep(0.4, 0.55, p));
chaosNodeGroup.current.scale.setScalar(s);
}
if (optNodeGroup.current) {
const appear = smoothstep(0.6, 0.82, p);
const pop = appear * (1 + Math.sin(t * 4) * 0.06 * appear);
optNodeGroup.current.scale.setScalar(0.0001 + pop);
}
});
return (
<group>
{/* Chaotic routes (red) */}
{chaotic.map((r, i) => (
<mesh key={`c${i}`} frustumCulled={false}>
<tubeGeometry
ref={registerChaos(i)}
args={[r.curve, TUBULAR, 0.08, RADIAL, false]}
/>
<meshBasicMaterial
ref={registerChaosMat(i)}
color={COLORS.red}
transparent
opacity={0.85}
depthWrite={false}
blending={reduced ? THREE.NormalBlending : THREE.AdditiveBlending}
/>
</mesh>
))}
{/* Optimized routes (cyan → green) */}
{optimized.map((r, i) => (
<mesh key={`o${i}`} frustumCulled={false}>
<tubeGeometry
ref={registerOpt(i)}
args={[r.curve, TUBULAR, 0.10, RADIAL, false]}
/>
<meshBasicMaterial
ref={registerOptMat(i)}
color={i % 2 === 0 ? COLORS.cyan : COLORS.green}
transparent
opacity={0}
depthWrite={false}
blending={reduced ? THREE.NormalBlending : THREE.AdditiveBlending}
/>
</mesh>
))}
{/* Chaos delivery nodes (red, delayed) with pulsing warning alerts */}
<group ref={chaosNodeGroup}>
{chaosNodes.map((n, i) => (
<group key={`cn-group-${i}`}>
<mesh position={n}>
<sphereGeometry args={[0.18, 12, 12]} />
<meshBasicMaterial
color={COLORS.red}
transparent
opacity={0.9}
blending={THREE.AdditiveBlending}
/>
</mesh>
<PulsingRingMemo position={n} color={COLORS.red} />
</group>
))}
</group>
{/* Optimized delivery nodes (green, fulfilled) with pulsing success alerts */}
<group ref={optNodeGroup} scale={0.0001}>
{optimizedNodes.map((n, i) => (
<group key={`on-group-${i}`}>
<mesh position={n}>
<sphereGeometry args={[0.15, 14, 14]} />
<meshBasicMaterial
color={COLORS.green}
transparent
opacity={0.95}
blending={THREE.AdditiveBlending}
/>
</mesh>
<PulsingRingMemo position={n} color={COLORS.green} maxScale={1.8} pulseSpeed={3.0} />
</group>
))}
</group>
{/* Flowing delivery particles along routes */}
{!isMobile && (
<>
<ChaoticFlowMemo
progress={progress}
chaoticCurves={chaotic.map(r => r.curve)}
/>
<OptimizedFlowMemo
progress={progress}
optimizedCurves={optimized.map(r => r.curve)}
/>
</>
)}
</group>
);
}
export default React.memo(RouteSystem);

View File

@@ -0,0 +1,294 @@
"use client";
import React, { useMemo, useRef } from "react";
import { useFrame } from "@react-three/fiber";
import * as THREE from "three";
import { COLORS } from "./constants";
import { buildRoutes, RouteDef } from "./routes";
import { seeded, smoothstep } from "./math";
type Props = {
progress: React.RefObject<number>;
reduced?: boolean;
};
type VType = "truck" | "van" | "bike";
type VehicleDef = {
curve: THREE.CatmullRomCurve3;
type: VType;
speed: number;
offset: number;
color: string;
nodes: THREE.Vector3[];
};
const TMP_POS = new THREE.Vector3();
const TMP_TAN = new THREE.Vector3();
const TRAIL_TMP = new THREE.Vector3();
// Glowing energy comet-tail trailing each optimized vehicle (see ref image:
// a delivery van with flowing green light streaks). Sampled along the vehicle's
// own route curve behind its current position, so it always hugs the track and
// never smears on a scroll-jump.
const TRAIL_N = 16;
const TRAIL_GAP = 0.011;
/** Flat billboarded truck cutout from the user's artwork (modelled facing +Z;
* the fleet loop rotates each one to face the camera). */
function TruckBillboard({
texture,
tint,
width,
aspect,
}: {
texture: THREE.Texture;
tint: string;
width: number;
aspect: number;
}) {
const h = width / aspect;
return (
<mesh position={[0, h / 2.4, 0]}>
<planeGeometry args={[width, h]} />
<meshBasicMaterial
map={texture}
color={tint}
transparent
depthWrite={false}
side={THREE.DoubleSide}
toneMapped={false}
/>
</mesh>
);
}
const TruckBillboardMemo = React.memo(TruckBillboard);
const HUB = new THREE.Vector3(0, 0.5, 0);
/**
* Moving fleet. 12 erratic vehicles ride the chaotic routes; on the reorganize
* phase they fade out and 8 optimally-assigned vehicles take over the clean
* routes. Position + heading are driven along the shared curves each frame.
*/
function VehicleFleet({ progress, reduced = false }: Props) {
const { chaotic, optimized } = useMemo(() => buildRoutes(), []);
const types: VType[] = ["truck", "van", "bike"];
const chaosFleet = useMemo<VehicleDef[]>(() => {
return Array.from({ length: 7 }, (_, i) => ({
curve: (chaotic[i % chaotic.length] as RouteDef).curve,
type: types[i % 3],
speed: 0.018 + seeded(i * 5 + 1) * 0.03,
offset: seeded(i * 5 + 2),
color: COLORS.red,
nodes: (chaotic[i % chaotic.length] as RouteDef).nodes || [],
}));
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [chaotic]);
const optFleet = useMemo<VehicleDef[]>(() => {
return Array.from({ length: 5 }, (_, i) => ({
curve: (optimized[i % optimized.length] as RouteDef).curve,
type: types[i % 3],
speed: 0.05 + seeded(i * 7 + 1) * 0.025,
offset: seeded(i * 7 + 2),
color: i % 2 === 0 ? COLORS.cyan : COLORS.green,
nodes: (optimized[i % optimized.length] as RouteDef).nodes || [],
}));
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [optimized]);
const chaosRefs = useRef<(THREE.Group | null)[]>([]);
const optRefs = useRef<(THREE.Group | null)[]>([]);
const optTrailRefs = useRef<(THREE.Mesh | null)[]>([]);
// The optimized fleet uses the user's isometric truck artwork as a flat
// billboarded cutout (loaded imperatively so we don't need a Suspense fence).
const truckTex = useMemo(() => {
const t = new THREE.TextureLoader().load("/images/truck.png");
t.colorSpace = THREE.SRGBColorSpace;
t.anisotropy = 8;
return t;
}, []);
const TRUCK_ASPECT = 1080 / 948; // rasterized PNG dimensions
const TRUCK_W = 1.75;
// Persistent progress values
const chaosProgress = useRef<number[]>([]);
const optProgress = useRef<number[]>([]);
// Re-sync on length change (not just when empty) so a fleet-size edit / HMR
// doesn't leave indices reading `undefined` and crashing getPointAt().
if (chaosProgress.current.length !== chaosFleet.length) {
chaosProgress.current = chaosFleet.map((v) => v.offset);
}
if (optProgress.current.length !== optFleet.length) {
optProgress.current = optFleet.map((v) => v.offset);
}
const place = (
group: THREE.Group | null,
def: VehicleDef,
progressArray: number[],
index: number,
dt: number,
opacity: number,
) => {
if (!group || !def?.curve) return;
const visible = opacity > 0.02;
group.visible = visible;
if (!visible) return;
let u = progressArray[index];
if (u === undefined || Number.isNaN(u)) u = progressArray[index] = def.offset ?? 0;
// Get position to compute distance to nodes
def.curve.getPointAt(u, TMP_POS);
// Compute distance to nearest node
let minDist = TMP_POS.distanceTo(HUB);
if (def.nodes && def.nodes.length > 0) {
def.nodes.forEach((node) => {
const dist = TMP_POS.distanceTo(node);
if (dist < minDist) minDist = dist;
});
}
// Slow at nodes (speed reduction to 20%), accelerate on long corridors (up to 125%)
const speedFactor = 0.20 + smoothstep(0.4, 2.5, minDist) * 1.05;
// Increment progress continuously
u = (u + dt * def.speed * speedFactor) % 1;
progressArray[index] = u;
// Get final position and orientation
def.curve.getPointAt(u, TMP_POS);
def.curve.getTangentAt(u, TMP_TAN);
group.position.copy(TMP_POS);
group.rotation.y = Math.atan2(TMP_TAN.x, TMP_TAN.z);
group.traverse((o) => {
const m = (o as THREE.Mesh).material as THREE.Material | undefined;
if (m && "opacity" in m) {
(m as THREE.MeshBasicMaterial).opacity = opacity * baseOpacity(o);
}
});
};
// preserve per-material relative opacity (fill vs wire vs light)
const baseOpacity = (o: THREE.Object3D) => {
const m = (o as THREE.Mesh).material as THREE.MeshBasicMaterial | undefined;
if (!m) return 1;
if (m.map) return 1; // textured truck cutout fades at full strength
if ((m as THREE.MeshBasicMaterial).wireframe) return 1;
if (m.color && m.color.getHex() === 0xffffff) return 1;
if (m.color && m.color.getHex() === 0xef4444) return 1; // headlight/taillight colors
return 0.2;
};
useFrame((state, dt) => {
const p = progress.current ?? 0;
const t = state.clock.elapsedTime;
const safeDt = Math.min(0.06, dt);
const chaosFadeIn = smoothstep(0.10, 0.22, p);
const chaosFadeOut = 1 - smoothstep(0.70, 0.82, p);
const chaosOp = chaosFadeIn * chaosFadeOut * (0.85 + Math.sin(t * 6) * 0.1);
const optOp = smoothstep(0.68, 0.82, p);
const cam = state.camera;
for (let i = 0; i < chaosFleet.length; i++) {
place(chaosRefs.current[i], chaosFleet[i], chaosProgress.current, i, safeDt, chaosOp);
const g = chaosRefs.current[i];
if (g && g.visible) {
g.rotation.y = Math.atan2(cam.position.x - g.position.x, cam.position.z - g.position.z);
}
}
for (let i = 0; i < optFleet.length; i++) {
place(optRefs.current[i], optFleet[i], optProgress.current, i, safeDt, optOp);
// The truck is 2D cutout art, so billboard it around Y to always face the
// camera (overrides the tangent heading set inside place()).
const g = optRefs.current[i];
if (g && g.visible) {
g.rotation.y = Math.atan2(cam.position.x - g.position.x, cam.position.z - g.position.z);
}
// Energy comet-tail behind each optimized vehicle.
const u = optProgress.current[i];
const curve = optFleet[i]?.curve;
if (!curve || u === undefined || Number.isNaN(u)) continue;
for (let k = 0; k < TRAIL_N; k++) {
const mesh = optTrailRefs.current[i * TRAIL_N + k];
if (!mesh) continue;
if (optOp < 0.02) { mesh.visible = false; continue; }
let uk = u - (k + 1) * TRAIL_GAP;
if (uk < 0) uk = 0;
mesh.visible = true;
curve.getPointAt(uk, TRAIL_TMP);
mesh.position.copy(TRAIL_TMP);
const taper = 1 - k / TRAIL_N;
const size = 0.05 + taper * 0.13;
mesh.scale.setScalar(size / 0.1);
const mat = mesh.material as THREE.MeshBasicMaterial;
mat.opacity = optOp * taper * taper * (0.65 + Math.sin(t * 9 - k * 0.7) * 0.35);
}
}
});
return (
<group>
{/* Chaotic fleet — the user's truck art tinted red (congested/before) */}
{chaosFleet.map((_v, i) => (
<group
key={`cv${i}`}
ref={(el) => {
chaosRefs.current[i] = el;
}}
>
<TruckBillboardMemo texture={truckTex} tint="#ff5a5a" width={TRUCK_W * 0.92} aspect={TRUCK_ASPECT} />
</group>
))}
{/* Optimized fleet — the truck art in clean white */}
{optFleet.map((_v, i) => (
<group
key={`ov${i}`}
visible={false}
ref={(el) => {
optRefs.current[i] = el;
}}
>
<TruckBillboardMemo texture={truckTex} tint="#d7dce4" width={TRUCK_W} aspect={TRUCK_ASPECT} />
</group>
))}
{/* Glowing energy comet-tails for the optimized fleet */}
{optFleet.map((v, i) =>
Array.from({ length: TRAIL_N }, (_, k) => (
<mesh
key={`ot-${i}-${k}`}
visible={false}
ref={(el) => {
optTrailRefs.current[i * TRAIL_N + k] = el;
}}
>
<sphereGeometry args={[0.1, 8, 8]} />
<meshBasicMaterial
color={v.color}
transparent
opacity={0}
depthWrite={false}
blending={reduced ? THREE.NormalBlending : THREE.AdditiveBlending}
/>
</mesh>
)),
)}
</group>
);
}
export default React.memo(VehicleFleet);

View File

@@ -0,0 +1,118 @@
/**
* Shared design tokens + KPI data for the AI Logistics Optimization section.
* Centralizing here keeps the 3D scene, overlay and metrics panel in sync.
*/
export const COLORS = {
bg: "#020617",
cyan: "#00E5FF",
green: "#22C55E",
red: "#EF4444",
amber: "#F59E0B",
ink: "#0B1220",
slate: "#64748B",
textDim: "rgba(226,232,240,0.66)",
} as const;
/** rgba helper used by inline styles + scoped CSS. */
export function rgba(hex: string, alpha: number): string {
const h = hex.replace("#", "");
const r = parseInt(h.substring(0, 2), 16);
const g = parseInt(h.substring(2, 4), 16);
const b = parseInt(h.substring(4, 6), 16);
return `rgba(${r}, ${g}, ${b}, ${alpha})`;
}
export type Kpi = {
key: string;
/** numeric value the counter animates between */
before: number;
after: number;
prefixBefore?: string;
prefixAfter?: string;
suffix?: string;
labelBefore: string;
labelAfter: string;
/** lower number is the better outcome (drives the up/down arrow) */
goodWhenLower: boolean;
};
export const KPIS: Kpi[] = [
{
key: "distance",
before: 312,
after: 182,
suffix: " km",
labelBefore: "Route Distance",
labelAfter: "Route Distance",
goodWhenLower: true,
},
{
key: "vehicles",
before: 8,
after: 5,
labelBefore: "Vehicles",
labelAfter: "Vehicles",
goodWhenLower: true,
},
{
key: "orders",
before: 59,
after: 59,
labelBefore: "Orders",
labelAfter: "Orders Fulfilled",
goodWhenLower: false,
},
{
key: "delayed",
before: 23,
after: 0,
labelBefore: "Delayed",
labelAfter: "Delayed",
goodWhenLower: true,
},
{
key: "cost",
before: 18,
after: 18,
prefixBefore: "+",
prefixAfter: "",
suffix: "%",
labelBefore: "Cost Overrun",
labelAfter: "Cost Saved",
goodWhenLower: false,
},
];
/**
* Scroll-phase thresholds (normalized 0 → 1). The whole narrative is driven by
* a single scroll progress value shared between GSAP, R3F and Framer Motion.
*/
export const PHASES = {
chaos: 0.0, // Stage 1 & 2: Network appears & vehicles active
scan: 0.28, // Stage 3: AI scans routes
dissolve: 0.42, // Stage 4 & 5: Optimize begins & bad routes dissolve
optimize: 0.56, // Stage 6: Optimized routes appear
reorganize: 0.70, // Reassigning vehicles
metrics: 0.84, // Stage 7: KPIs update & complete
} as const;
export type PhaseKey = keyof typeof PHASES;
export function phaseFromProgress(p: number): PhaseKey {
if (p >= PHASES.metrics) return "metrics";
if (p >= PHASES.reorganize) return "reorganize";
if (p >= PHASES.optimize) return "optimize";
if (p >= PHASES.dissolve) return "dissolve";
if (p >= PHASES.scan) return "scan";
return "chaos";
}
export const PHASE_LABELS: Record<PhaseKey, string> = {
chaos: "Monitoring network",
scan: "AI SCANNING NETWORK",
dissolve: "AI OPTIMIZING ROUTES",
optimize: "AI OPTIMIZING ROUTES",
reorganize: "AI REASSIGNING VEHICLES",
metrics: "AI COMPLETE",
};

View File

@@ -0,0 +1,36 @@
/** Small framerate-independent math helpers shared by the 3D scene. */
export const clamp01 = (v: number) => (v < 0 ? 0 : v > 1 ? 1 : v);
export const lerp = (a: number, b: number, t: number) => a + (b - a) * t;
/** Linear remap of `v` from [inMin,inMax] to [outMin,outMax], clamped. */
export function remap(
v: number,
inMin: number,
inMax: number,
outMin = 0,
outMax = 1,
): number {
const t = clamp01((v - inMin) / (inMax - inMin || 1));
return outMin + (outMax - outMin) * t;
}
export function smoothstep(edge0: number, edge1: number, x: number): number {
const t = clamp01((x - edge0) / (edge1 - edge0 || 1));
return t * t * (3 - 2 * t);
}
/**
* Exponential damp toward a target — frame-rate independent.
* lambda ~ responsiveness (higher = snappier).
*/
export function damp(current: number, target: number, lambda: number, dt: number): number {
return lerp(current, target, 1 - Math.exp(-lambda * dt));
}
/** Deterministic pseudo-random in [0,1) — avoids Math.random for stable SSR/scene. */
export function seeded(i: number): number {
const x = Math.sin(i * 127.1 + 311.7) * 43758.5453;
return x - Math.floor(x);
}

View File

@@ -0,0 +1,91 @@
import * as THREE from "three";
import { seeded } from "./math";
/**
* Deterministic route + delivery-node generation shared by RouteSystem and
* VehicleFleet so glowing paths and moving vehicles stay perfectly aligned.
*/
export const HUB = new THREE.Vector3(0, 0.5, 0);
export const ROUTE_Y = 0.45;
export type RouteDef = {
curve: THREE.CatmullRomCurve3;
nodes: THREE.Vector3[];
};
export type SceneRoutes = {
chaotic: RouteDef[];
optimized: RouteDef[];
chaosNodes: THREE.Vector3[];
optimizedNodes: THREE.Vector3[];
};
function arc(points: THREE.Vector3[]): THREE.CatmullRomCurve3 {
return new THREE.CatmullRomCurve3(points, false, "catmullrom", 0.5);
}
let cached: SceneRoutes | null = null;
export function buildRoutes(): SceneRoutes {
if (cached) return cached;
// --- Optimized clusters: 5 tidy zones around the hub --------------------
const clusterCount = 5;
const optimized: RouteDef[] = [];
const optimizedNodes: THREE.Vector3[] = [];
for (let c = 0; c < clusterCount; c++) {
const baseAngle = (c / clusterCount) * Math.PI * 2 + 0.3;
const radius = 7 + seeded(c * 7 + 1) * 3;
const cx = Math.cos(baseAngle) * radius;
const cz = Math.sin(baseAngle) * radius;
// 2-3 stops per cluster (multi-trip planning)
const stops = 2 + Math.floor(seeded(c * 7 + 2) * 2);
const pts: THREE.Vector3[] = [HUB.clone()];
// gentle lift-off control point
pts.push(new THREE.Vector3(cx * 0.35, ROUTE_Y + 1.4, cz * 0.35));
const nodes: THREE.Vector3[] = [];
for (let s = 0; s < stops; s++) {
const jx = (seeded(c * 31 + s * 5 + 3) - 0.5) * 3.2;
const jz = (seeded(c * 31 + s * 5 + 4) - 0.5) * 3.2;
const node = new THREE.Vector3(cx + jx, ROUTE_Y, cz + jz);
nodes.push(node);
optimizedNodes.push(node);
pts.push(node.clone());
}
optimized.push({ curve: arc(pts), nodes });
}
// --- Chaotic routes: tangled, overlapping, wandering --------------------
const chaotic: RouteDef[] = [];
const chaosNodes: THREE.Vector3[] = [];
const chaosCount = 6;
for (let r = 0; r < chaosCount; r++) {
const segs = 4 + Math.floor(seeded(r * 13 + 1) * 3);
const pts: THREE.Vector3[] = [HUB.clone()];
let angle = seeded(r * 13 + 2) * Math.PI * 2;
const nodes: THREE.Vector3[] = [];
for (let s = 0; s < segs; s++) {
angle += (seeded(r * 13 + s * 3 + 3) - 0.5) * 2.6; // erratic turns
const reach = 2.6 + seeded(r * 13 + s * 3 + 4) * 6.4; // tighter, more central
const px = Math.cos(angle) * reach + (seeded(r * 17 + s) - 0.5) * 2.6;
const pz = Math.sin(angle) * reach + (seeded(r * 19 + s) - 0.5) * 2.6;
const py = ROUTE_Y + seeded(r * 23 + s) * 1.1;
const p = new THREE.Vector3(px, py, pz);
pts.push(p);
if (s === segs - 1) {
const node = new THREE.Vector3(px, ROUTE_Y, pz);
chaosNodes.push(node);
nodes.push(node);
}
}
chaotic.push({ curve: arc(pts), nodes });
}
cached = { chaotic, optimized, chaosNodes, optimizedNodes };
return cached;
}