update the loading issue

This commit is contained in:
2026-06-03 21:56:28 +05:30
parent 6b37649ed4
commit 123092f4b8
14 changed files with 340 additions and 145 deletions

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@@ -10,17 +10,24 @@ import Lenis from "lenis";
* SmoothScroll * SmoothScroll
* --------------------------------------------------------------------------- * ---------------------------------------------------------------------------
* One global Lenis instance, driven by a SINGLE rAF source (GSAP's ticker) and * One global Lenis instance, driven by a SINGLE rAF source (GSAP's ticker) and
* kept locked to ScrollTrigger. Deliberately gated OFF on: * kept locked to ScrollTrigger via `lenis.on("scroll", ScrollTrigger.update)`.
* - /miletruth — it stacks 3 pinned WebGL sections; JS scroll-smoothing there *
* fights the pins and causes the very lag we're trying to remove. Native * Enabled on every route. /miletruth's three scroll-driven WebGL sections use a
* scroll is used on that route. * self-managed `position: fixed` pin (toggled from ScrollTrigger.onUpdate via
* `self.progress`) — NOT GSAP's pin-spacer — so Lenis driving real document
* scroll keeps their progress correct and just smooths the wheel input. (It was
* previously gated off here when those sections used GSAP pins; that no longer
* applies, and native scroll there felt noticeably janky next to every Lenis
* route.)
*
* Still gated OFF on:
* - touch devices — native momentum is smoother than emulated inertia. * - touch devices — native momentum is smoother than emulated inertia.
* - prefers-reduced-motion. * - prefers-reduced-motion.
* *
* Re-evaluates on every route change: the effect cleanup destroys the previous * Re-evaluates on every route change: the effect cleanup destroys the previous
* instance, so entering /miletruth tears Lenis down and leaving it re-inits. * instance and re-inits on the next route.
*/ */
const DISABLED_ROUTES = ["/miletruth"]; const DISABLED_ROUTES: string[] = [];
export default function SmoothScroll() { export default function SmoothScroll() {
const pathname = usePathname(); const pathname = usePathname();

View File

@@ -11,8 +11,8 @@ html {
} }
/* Lenis global smooth scroll (src/animations/SmoothScroll.tsx). These classes are /* Lenis global smooth scroll (src/animations/SmoothScroll.tsx). These classes are
only present on routes/devices where Lenis is active; on /miletruth and touch only present on routes/devices where Lenis is active; on touch devices and with
devices Lenis is off and native scroll-behavior:smooth (above) applies. */ prefers-reduced-motion Lenis is off and native scroll-behavior:smooth (above) applies. */
html.lenis, html.lenis,
html.lenis body { html.lenis body {
height: auto; height: auto;

View File

@@ -63,7 +63,7 @@ export default function RootLayout({
children: React.ReactNode; children: React.ReactNode;
}>) { }>) {
return ( return (
<html lang="en-US" className={`${manrope.variable} ${spaceGrotesk.variable} ${syne.variable} ${dmSans.variable} ${inter.variable}`}> <html lang="en-US" data-scroll-behavior="smooth" className={`${manrope.variable} ${spaceGrotesk.variable} ${syne.variable} ${dmSans.variable} ${inter.variable}`}>
<head> <head>
{/* FontAwesome icons */} {/* FontAwesome icons */}
<link <link

View File

@@ -2,7 +2,7 @@ import React from "react";
import MileTruthHero from "../../components/sections/MileTruthHero"; import MileTruthHero from "../../components/sections/MileTruthHero";
import Workflow1 from "../../components/sections/Workflow1"; import Workflow1 from "../../components/sections/Workflow1";
import Workflow2 from "../../components/sections/Workflow2"; import Workflow2 from "../../components/sections/Workflow2";
import Workflow3 from "../../components/sections/Workflow3"; import Workflow3Lazy from "../../components/sections/Workflow3Lazy";
export const metadata = { export const metadata = {
title: "MileTruth Doormile", title: "MileTruth Doormile",
@@ -18,7 +18,7 @@ export default function MileTruthPage() {
<MileTruthHero /> <MileTruthHero />
<Workflow1 /> <Workflow1 />
<Workflow2 /> <Workflow2 />
<Workflow3 /> <Workflow3Lazy />
</div> </div>
</div> </div>
</div> </div>

View File

@@ -83,6 +83,7 @@ export default function LoadingScreen() {
height={38} height={38}
priority priority
className="dm-loader__logo" className="dm-loader__logo"
style={{ height: "auto" }}
/> />
</div> </div>

View File

@@ -2,18 +2,26 @@
import React, { useEffect, useRef, useState } from "react"; import React, { useEffect, useRef, useState } from "react";
import dynamic from "next/dynamic"; import dynamic from "next/dynamic";
import { motion, useMotionValue, useMotionValueEvent, useTransform, type MotionValue } from "framer-motion"; import { motion, useMotionValue, useTransform, type MotionValue } from "framer-motion";
import gsap from "gsap"; import gsap from "gsap";
import { ScrollTrigger } from "gsap/ScrollTrigger"; import { ScrollTrigger } from "gsap/ScrollTrigger";
import { P, STRATEGIES, ENGINE_STEPS, CONSTRAINT_LIST, STRATEGY_SCORES } from "./theme"; import { P, STRATEGIES, ENGINE_STEPS, CONSTRAINT_LIST, STRATEGY_SCORES } from "./theme";
const LogisticsBrainCanvas = dynamic(() => import("./LogisticsBrainCanvas"), { ssr: false }); const LogisticsBrainCanvas = dynamic(() => import("./LogisticsBrainCanvas"), { ssr: false });
/** Rounds a MotionValue to an integer for the animated stat counters. */ /** Rounds a MotionValue to an integer for the animated stat counters.
* Writes to the DOM imperatively so it never triggers a setState during the
* parent's render (which `useTransform` emits synchronously on re-render). */
function Counter({ mv }: { mv: MotionValue<number> }) { function Counter({ mv }: { mv: MotionValue<number> }) {
const [v, setV] = useState(0); const ref = useRef<HTMLSpanElement>(null);
useMotionValueEvent(mv, "change", (x) => setV(Math.round(x))); useEffect(() => {
return <>{v}</>; const write = (x: number) => {
if (ref.current) ref.current.textContent = String(Math.round(x));
};
write(mv.get());
return mv.on("change", write);
}, [mv]);
return <span ref={ref}>{Math.round(mv.get())}</span>;
} }
/** Active step index from scroll progress (1 before the engine starts). */ /** Active step index from scroll progress (1 before the engine starts). */

View File

@@ -154,11 +154,11 @@ export default function OptimizationSection() {
if (!el) return; if (!el) return;
gsap.registerPlugin(ScrollTrigger); gsap.registerPlugin(ScrollTrigger);
// NOTE: /miletruth runs on native scroll (no Lenis). Smooth-scrolling this // NOTE: global Lenis (src/animations/SmoothScroll.tsx) is active on this
// page fought the three stacked pinned WebGL sections and caused scroll lag; // route. Because the pin is a self-managed `position: fixed` driven by
// global Lenis (src/animations/SmoothScroll.tsx) is intentionally disabled on // `self.progress` (not a GSAP pin-spacer) and Lenis scrolls the real
// this route. ScrollTrigger's scrub + self-managed fixed pin work as-is on // document hooked to ScrollTrigger.update, the scrub + fixed pin work
// native scroll. // unchanged under smooth scroll.
let lastPhase: PhaseKey = "chaos"; let lastPhase: PhaseKey = "chaos";
let lastPin: "before" | "pinned" | "after" = "before"; let lastPin: "before" | "pinned" | "after" = "before";
const st = ScrollTrigger.create({ const st = ScrollTrigger.create({

View File

@@ -134,7 +134,8 @@ function VehicleFleet({ progress, reduced = false }: Props) {
if (!visible) return; if (!visible) return;
let u = progressArray[index]; let u = progressArray[index];
if (u === undefined || Number.isNaN(u)) u = progressArray[index] = def.offset ?? 0; if (u === undefined || Number.isNaN(u))
u = progressArray[index] = def.offset ?? 0;
// Get position to compute distance to nodes // Get position to compute distance to nodes
def.curve.getPointAt(u, TMP_POS); def.curve.getPointAt(u, TMP_POS);
@@ -149,7 +150,7 @@ function VehicleFleet({ progress, reduced = false }: Props) {
} }
// Slow at nodes (speed reduction to 20%), accelerate on long corridors (up to 125%) // 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; const speedFactor = 0.2 + smoothstep(0.4, 2.5, minDist) * 1.05;
// Increment progress continuously // Increment progress continuously
u = (u + dt * def.speed * speedFactor) % 1; u = (u + dt * def.speed * speedFactor) % 1;
@@ -195,26 +196,46 @@ function VehicleFleet({ progress, reduced = false }: Props) {
const t = state.clock.elapsedTime; const t = state.clock.elapsedTime;
const safeDt = Math.min(0.06, dt); const safeDt = Math.min(0.06, dt);
const chaosFadeIn = smoothstep(0.10, 0.22, p); const chaosFadeIn = smoothstep(0.1, 0.22, p);
const chaosFadeOut = 1 - smoothstep(0.70, 0.82, p); const chaosFadeOut = 1 - smoothstep(0.7, 0.82, p);
const chaosOp = chaosFadeIn * chaosFadeOut * (0.85 + Math.sin(t * 6) * 0.1); const chaosOp = chaosFadeIn * chaosFadeOut * (0.85 + Math.sin(t * 6) * 0.1);
const optOp = smoothstep(0.68, 0.82, p); const optOp = smoothstep(0.68, 0.82, p);
const cam = state.camera; const cam = state.camera;
for (let i = 0; i < chaosFleet.length; i++) { for (let i = 0; i < chaosFleet.length; i++) {
place(chaosRefs.current[i], chaosFleet[i], chaosProgress.current, i, safeDt, chaosOp); place(
chaosRefs.current[i],
chaosFleet[i],
chaosProgress.current,
i,
safeDt,
chaosOp,
);
const g = chaosRefs.current[i]; const g = chaosRefs.current[i];
if (g && g.visible) { if (g && g.visible) {
g.rotation.y = Math.atan2(cam.position.x - g.position.x, cam.position.z - g.position.z); 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++) { for (let i = 0; i < optFleet.length; i++) {
place(optRefs.current[i], optFleet[i], optProgress.current, i, safeDt, optOp); 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 // The truck is 2D cutout art, so billboard it around Y to always face the
// camera (overrides the tangent heading set inside place()). // camera (overrides the tangent heading set inside place()).
const g = optRefs.current[i]; const g = optRefs.current[i];
if (g && g.visible) { if (g && g.visible) {
g.rotation.y = Math.atan2(cam.position.x - g.position.x, cam.position.z - g.position.z); g.rotation.y = Math.atan2(
cam.position.x - g.position.x,
cam.position.z - g.position.z,
);
} }
// Energy comet-tail behind each optimized vehicle. // Energy comet-tail behind each optimized vehicle.
const u = optProgress.current[i]; const u = optProgress.current[i];
@@ -223,7 +244,10 @@ function VehicleFleet({ progress, reduced = false }: Props) {
for (let k = 0; k < TRAIL_N; k++) { for (let k = 0; k < TRAIL_N; k++) {
const mesh = optTrailRefs.current[i * TRAIL_N + k]; const mesh = optTrailRefs.current[i * TRAIL_N + k];
if (!mesh) continue; if (!mesh) continue;
if (optOp < 0.02) { mesh.visible = false; continue; } if (optOp < 0.02) {
mesh.visible = false;
continue;
}
let uk = u - (k + 1) * TRAIL_GAP; let uk = u - (k + 1) * TRAIL_GAP;
if (uk < 0) uk = 0; if (uk < 0) uk = 0;
mesh.visible = true; mesh.visible = true;
@@ -233,7 +257,8 @@ function VehicleFleet({ progress, reduced = false }: Props) {
const size = 0.05 + taper * 0.13; const size = 0.05 + taper * 0.13;
mesh.scale.setScalar(size / 0.1); mesh.scale.setScalar(size / 0.1);
const mat = mesh.material as THREE.MeshBasicMaterial; const mat = mesh.material as THREE.MeshBasicMaterial;
mat.opacity = optOp * taper * taper * (0.65 + Math.sin(t * 9 - k * 0.7) * 0.35); mat.opacity =
optOp * taper * taper * (0.65 + Math.sin(t * 9 - k * 0.7) * 0.35);
} }
} }
}); });
@@ -248,7 +273,12 @@ function VehicleFleet({ progress, reduced = false }: Props) {
chaosRefs.current[i] = el; chaosRefs.current[i] = el;
}} }}
> >
<TruckBillboardMemo texture={truckTex} tint="#ff5a5a" width={TRUCK_W * 0.92} aspect={TRUCK_ASPECT} /> <TruckBillboardMemo
texture={truckTex}
tint="#ff5a5a"
width={TRUCK_W * 0.92}
aspect={TRUCK_ASPECT}
/>
</group> </group>
))} ))}
{/* Optimized fleet — the truck art in clean white */} {/* Optimized fleet — the truck art in clean white */}
@@ -260,7 +290,12 @@ function VehicleFleet({ progress, reduced = false }: Props) {
optRefs.current[i] = el; optRefs.current[i] = el;
}} }}
> >
<TruckBillboardMemo texture={truckTex} tint="#d7dce4" width={TRUCK_W} aspect={TRUCK_ASPECT} /> <TruckBillboardMemo
texture={truckTex}
tint="#d7dce4"
width={TRUCK_W}
aspect={TRUCK_ASPECT}
/>
</group> </group>
))} ))}
@@ -290,5 +325,3 @@ function VehicleFleet({ progress, reduced = false }: Props) {
} }
export default React.memo(VehicleFleet); export default React.memo(VehicleFleet);

View File

@@ -0,0 +1,48 @@
"use client";
import React, { useEffect, useRef, useState } from "react";
import dynamic from "next/dynamic";
// Workflow 3 is the heaviest, last-on-page section (the Strategy 3D experience +
// its WebGL canvas chunk). It's kept OFF the initial render/compile critical path:
// dynamically imported (ssr:false) and only mounted once the user scrolls within
// ~1.5 viewports of it. Until then we reserve a min-height so the page scroll
// length stays roughly stable and the mount (which happens well below the fold)
// never shifts what the user is currently looking at.
const Workflow3 = dynamic(() => import("./Workflow3"), { ssr: false, loading: () => null });
export default function Workflow3Lazy() {
const sentinelRef = useRef<HTMLDivElement>(null);
const [show, setShow] = useState(false);
useEffect(() => {
const el = sentinelRef.current;
if (!el) return;
const io = new IntersectionObserver(
(entries) => {
if (entries.some((e) => e.isIntersecting)) {
setShow(true);
io.disconnect();
}
},
// Mount well before it enters the viewport so the canvas + GLBs preload
// and ScrollTrigger is ready by the time the user reaches it.
{ rootMargin: "150% 0px" },
);
io.observe(el);
return () => io.disconnect();
}, []);
if (!show) {
// Placeholder reserves a viewport of height; the real section (much taller)
// expands below the fold once mounted, far from the user's current position.
return <div ref={sentinelRef} aria-hidden style={{ minHeight: "100vh" }} />;
}
// `display: contents` so this wrapper adds no box of its own — Workflow3 keeps
// its exact layout/seam with the section above it.
return (
<div ref={sentinelRef} style={{ display: "contents" }}>
<Workflow3 />
</div>
);
}

View File

@@ -1,8 +1,8 @@
"use client"; "use client";
import React, { Suspense, useEffect, useMemo, useRef } from "react"; import React, { Suspense, useEffect, useMemo, useRef, useState } from "react";
import { Canvas, useFrame } from "@react-three/fiber"; import { Canvas, useFrame, useThree } from "@react-three/fiber";
import { RoundedBox, Line, Sparkles, Html, useGLTF, ContactShadows, Environment, Lightformer } from "@react-three/drei"; import { RoundedBox, Line, Sparkles, Html, useGLTF, ContactShadows, Environment, Lightformer, Preload } from "@react-three/drei";
import { EffectComposer, Bloom } from "@react-three/postprocessing"; import { EffectComposer, Bloom } from "@react-three/postprocessing";
import { KernelSize } from "postprocessing"; import { KernelSize } from "postprocessing";
import * as THREE from "three"; import * as THREE from "three";
@@ -26,6 +26,53 @@ const BLUE = "#3B82F6"; // 03 optimization
const ORANGE = "#F59E0B"; // 04 grading const ORANGE = "#F59E0B"; // 04 grading
const RED = "#C01227"; // 05 winner const RED = "#C01227"; // 05 winner
/* ===========================================================================
Dev metrics HUD — enable by appending ?perf to the URL. Samples the live
WebGL renderer (draw calls, triangles, resident geometries/textures, linked
shader programs) plus a rolling FPS, twice a second, into a fixed overlay.
Zero cost when not enabled (component simply isn't mounted).
=========================================================================== */
function PerfHud() {
const gl = useThree((s) => s.gl);
const el = useRef<HTMLDivElement | null>(null);
// Rolling FPS + one-shot timings: page navigation load, and time from this
// canvas mounting to its first rendered frame (≈ Workflow-3 activation cost).
const acc = useRef({ t: 0, frames: 0, mount: 0, firstFrame: 0, nav: 0 });
useEffect(() => {
const a = acc.current;
a.mount = performance.now();
const navEntry = performance.getEntriesByType("navigation")[0] as PerformanceNavigationTiming | undefined;
a.nav = navEntry ? Math.round(navEntry.loadEventEnd || navEntry.domContentLoadedEventEnd) : 0;
const d = document.createElement("div");
d.style.cssText =
"position:fixed;top:10px;left:10px;z-index:99999;font:11px/1.5 ui-monospace,monospace;" +
"background:rgba(0,0,0,.82);color:#39ff14;padding:8px 11px;border-radius:7px;white-space:pre;pointer-events:none";
document.body.appendChild(d);
el.current = d;
return () => { d.remove(); };
}, []);
useFrame((_s, dt) => {
const a = acc.current;
if (!a.firstFrame && a.mount) a.firstFrame = performance.now() - a.mount;
a.frames++; a.t += dt;
if (a.t >= 0.5 && el.current) {
const r = gl.info.render;
const m = gl.info.memory;
el.current.textContent =
`FPS ${(a.frames / a.t).toFixed(0)}\n` +
`draws ${r.calls}${r.calls < 100 ? " ✓" : " ⚠"}\n` +
`triangles ${r.triangles.toLocaleString()}\n` +
`geometries ${m.geometries}\n` +
`textures ${m.textures}\n` +
`programs ${gl.info.programs?.length ?? 0}\n` +
`page load ${a.nav}ms\n` +
`WF3 1st fr ${Math.round(a.firstFrame)}ms`;
a.frames = 0; a.t = 0;
}
});
return null;
}
/* =========================================================================== /* ===========================================================================
Camera — fully scroll-driven (reads a ref, never React state). Dollies forward Camera — fully scroll-driven (reads a ref, never React state). Dollies forward
through the world, riding behind+above the active district and looking slightly through the world, riding behind+above the active district and looking slightly
@@ -62,13 +109,19 @@ function CameraRig({ progress, reduced, isMobile }: { progress: React.RefObject<
Shared helpers Shared helpers
--------------------------------------------------------------------------- */ --------------------------------------------------------------------------- */
/** Fade a district's drei <Html> chips in/out by camera proximity (ref-driven). */ /** Fade a district's drei <Html> chips in/out by camera proximity (ref-driven).
function useLabelFade(i: number, progress: React.RefObject<number>) { * No-ops entirely when the district is asleep (its labels are unmounted then). */
function useLabelFade(i: number, progress: React.RefObject<number>, awake: boolean) {
const labels = useRef<HTMLElement[]>([]); const labels = useRef<HTMLElement[]>([]);
const register = (el: HTMLElement | null) => { const register = (el: HTMLElement | null) => {
if (el && !labels.current.includes(el)) labels.current.push(el); if (el && !labels.current.includes(el)) labels.current.push(el);
}; };
useEffect(() => {
// Labels are conditionally rendered, so their refs churn; drop stale ones each time.
if (!awake) labels.current = [];
}, [awake]);
useFrame(() => { useFrame(() => {
if (!awake) return;
const idx = (progress.current ?? 0) * (N - 1); const idx = (progress.current ?? 0) * (N - 1);
const op = THREE.MathUtils.clamp(1 - (Math.abs(idx - i) - 0.4) / 0.45, 0, 1); const op = THREE.MathUtils.clamp(1 - (Math.abs(idx - i) - 0.4) / 0.45, 0, 1);
for (const el of labels.current) el.style.opacity = String(op); for (const el of labels.current) el.style.opacity = String(op);
@@ -162,14 +215,14 @@ function buildModel(scene: THREE.Object3D, targetSize: number) {
return { root, wheels }; return { root, wheels };
} }
function GLBVehicle({ kind }: { kind: string }) { function GLBVehicle({ kind, awake = true }: { kind: string; awake?: boolean }) {
const tune = VEH_TUNE[kind]; const tune = VEH_TUNE[kind];
const { scene } = useGLTF(MODELS[kind]); const { scene } = useGLTF(MODELS[kind]);
const { root } = useMemo(() => buildModel(scene, tune.size), [scene, tune]); const { root } = useMemo(() => buildModel(scene, tune.size), [scene, tune]);
const g = useRef<THREE.Group>(null); const g = useRef<THREE.Group>(null);
useFrame((state) => { useFrame((state) => {
const grp = g.current; const grp = g.current;
if (!grp) return; if (!grp || !awake) return;
const t = state.clock.elapsedTime; const t = state.clock.elapsedTime;
switch (kind) { switch (kind) {
case "bike": grp.position.y = Math.abs(Math.sin(t * 3)) * 0.025; grp.rotation.z = Math.sin(t * 3) * 0.012; break; case "bike": grp.position.y = Math.abs(Math.sin(t * 3)) * 0.025; grp.rotation.z = Math.sin(t * 3) * 0.012; break;
@@ -195,7 +248,7 @@ const TRUCK_FWD_YAW = 0;
* is smoothed with quaternion slerp (always faces travel, never snaps). Wheels spin in * is smoothed with quaternion slerp (always faces travel, never snaps). Wheels spin in
* proportion to distance travelled; a glow ring marks the active segment under the truck. * proportion to distance travelled; a glow ring marks the active segment under the truck.
*/ */
function RouteTruck({ route, reduced, i, progress }: { route: THREE.CatmullRomCurve3; reduced: boolean; i: number; progress: React.RefObject<number> }) { function RouteTruck({ route, reduced, i, progress, awake = true }: { route: THREE.CatmullRomCurve3; reduced: boolean; i: number; progress: React.RefObject<number>; awake?: boolean }) {
const { scene } = useGLTF(MODELS.truck); const { scene } = useGLTF(MODELS.truck);
const built = useMemo(() => buildModel(scene, 0.85), [scene]); const built = useMemo(() => buildModel(scene, 0.85), [scene]);
const wheels = useRef<THREE.Object3D[]>([]); const wheels = useRef<THREE.Object3D[]>([]);
@@ -207,7 +260,7 @@ function RouteTruck({ route, reduced, i, progress }: { route: THREE.CatmullRomCu
const prevT = useRef(0); const prevT = useRef(0);
useFrame((state, dt) => { useFrame((state, dt) => {
const grp = g.current; const grp = g.current;
if (!grp) return; if (!grp || !awake) return;
const idx = (progress.current ?? 0) * (N - 1); const idx = (progress.current ?? 0) * (N - 1);
const t = THREE.MathUtils.clamp(idx - (i - 0.5), 0, 1); // 0 → hub … 1 → delivery const t = THREE.MathUtils.clamp(idx - (i - 0.5), 0, 1); // 0 → hub … 1 → delivery
const pos = route.getPointAt(t); const pos = route.getPointAt(t);
@@ -268,22 +321,24 @@ function OrderPacket({ curve, offset }: { curve: THREE.Curve<THREE.Vector3>; off
} }
/** A vehicle parked on its own glowing route node (flat on the floor, no white pad). */ /** A vehicle parked on its own glowing route node (flat on the floor, no white pad). */
function RiderAvatar({ rider, register }: { rider: typeof RIDERS[number]; register: (el: HTMLElement | null) => void }) { function RiderAvatar({ rider, register, awake }: { rider: typeof RIDERS[number]; register: (el: HTMLElement | null) => void; awake: boolean }) {
return ( return (
<group position={[rider.x, 0, ROW_Z]}> <group position={[rider.x, 0, ROW_Z]}>
<mesh position={[0, 0.014, 0]} rotation={[-Math.PI / 2, 0, 0]}><ringGeometry args={[0.52, 0.64, 36]} /><meshBasicMaterial color={GREEN} transparent opacity={0.6} side={THREE.DoubleSide} toneMapped={false} /></mesh> <mesh position={[0, 0.014, 0]} rotation={[-Math.PI / 2, 0, 0]}><ringGeometry args={[0.52, 0.64, 36]} /><meshBasicMaterial color={GREEN} transparent opacity={0.6} side={THREE.DoubleSide} toneMapped={false} /></mesh>
<mesh position={[0, 0.008, 0]} rotation={[-Math.PI / 2, 0, 0]}><circleGeometry args={[0.52, 36]} /><meshBasicMaterial color={GREEN} transparent opacity={0.08} side={THREE.DoubleSide} toneMapped={false} /></mesh> <mesh position={[0, 0.008, 0]} rotation={[-Math.PI / 2, 0, 0]}><circleGeometry args={[0.52, 36]} /><meshBasicMaterial color={GREEN} transparent opacity={0.08} side={THREE.DoubleSide} toneMapped={false} /></mesh>
<group position={[0, 0.02, 0]}> <group position={[0, 0.02, 0]}>
<Suspense fallback={null}> <Suspense fallback={null}>
<GLBVehicle kind={rider.kind} /> <GLBVehicle kind={rider.kind} awake={awake} />
</Suspense> </Suspense>
</group> </group>
<Html center distanceFactor={9} position={[0, 1.5, 0]} zIndexRange={[20, 0]} pointerEvents="none"> {awake && (
<div className="dm-st3d-chip" ref={register}> <Html center distanceFactor={9} position={[0, 1.5, 0]} zIndexRange={[20, 0]} pointerEvents="none">
<span className="dm-st3d-chip__ico">{rider.icon}</span> <div className="dm-st3d-chip" ref={register}>
<span className="dm-st3d-chip__txt"><b>Rider {rider.id}</b>{rider.veh}</span> <span className="dm-st3d-chip__ico">{rider.icon}</span>
</div> <span className="dm-st3d-chip__txt"><b>Rider {rider.id}</b>{rider.veh}</span>
</Html> </div>
</Html>
)}
</group> </group>
); );
} }
@@ -291,11 +346,12 @@ function RiderAvatar({ rider, register }: { rider: typeof RIDERS[number]; regist
const ORDERS_SRC: [number, number, number] = [3.3, 0.45, -0.7]; const ORDERS_SRC: [number, number, number] = [3.3, 0.45, -0.7];
const HUB_CORE: [number, number, number] = [0, 0.55, -1.4]; const HUB_CORE: [number, number, number] = [0, 0.55, -1.4];
const IntakeHub = React.memo(function IntakeHub({ i, progress, reduced }: { i: number; progress: React.RefObject<number>; reduced: boolean }) { const IntakeHub = React.memo(function IntakeHub({ i, progress, reduced, awake }: { i: number; progress: React.RefObject<number>; reduced: boolean; awake: boolean }) {
const register = useLabelFade(i, progress); const register = useLabelFade(i, progress, awake);
const counter = useRef<HTMLSpanElement>(null); const counter = useRef<HTMLSpanElement>(null);
const halo = useRef<THREE.Mesh>(null); const halo = useRef<THREE.Mesh>(null);
useFrame((state, dt) => { useFrame((state, dt) => {
if (!awake) return;
if (counter.current) { if (counter.current) {
if (reduced) counter.current.textContent = "59"; if (reduced) counter.current.textContent = "59";
else { else {
@@ -308,14 +364,11 @@ const IntakeHub = React.memo(function IntakeHub({ i, progress, reduced }: { i: n
const intake = useMemo(() => lineGeom(ORDERS_SRC, HUB_CORE), []); const intake = useMemo(() => lineGeom(ORDERS_SRC, HUB_CORE), []);
const routes = useMemo(() => RIDERS.map((r) => lineGeom(HUB_CORE, [r.x, 0.12, ROW_Z])), []); const routes = useMemo(() => RIDERS.map((r) => lineGeom(HUB_CORE, [r.x, 0.12, ROW_Z])), []);
return ( return (
<group position={districtPosition(i)}> <group position={districtPosition(i)} visible={awake}>
{/* orders.csv source node → intake route → hub (order packets flowing in) */} {/* orders.csv source node → intake route → hub (order packets flowing in) */}
<GlowNode position={ORDERS_SRC} color={GREEN} size={0.12} /> <GlowNode position={ORDERS_SRC} color={GREEN} size={0.12} />
<Line points={intake.getPoints(2)} color={GREEN} lineWidth={1.8} transparent opacity={0.5} toneMapped={false} /> <Line points={intake.getPoints(2)} color={GREEN} lineWidth={1.8} transparent opacity={0.5} toneMapped={false} />
{!reduced && [0, 0.5].map((o) => <OrderPacket key={o} curve={intake} offset={o} />)} {awake && !reduced && [0, 0.5].map((o) => <OrderPacket key={o} curve={intake} offset={o} />)}
<Html center distanceFactor={9} position={[ORDERS_SRC[0], ORDERS_SRC[1] + 0.5, ORDERS_SRC[2]]} zIndexRange={[20, 0]} pointerEvents="none">
<div className="dm-st3d-tag" style={{ ["--tc" as string]: GREEN }} ref={register}>📄 orders.csv</div>
</Html>
{/* AI Assignment Hub — a grounded dispatch hub (rim ring + hex dais + glowing core + halo) */} {/* AI Assignment Hub — a grounded dispatch hub (rim ring + hex dais + glowing core + halo) */}
<group position={[0, 0, -1.4]}> <group position={[0, 0, -1.4]}>
@@ -324,25 +377,33 @@ const IntakeHub = React.memo(function IntakeHub({ i, progress, reduced }: { i: n
<mesh position={[0, 0.55, 0]}><icosahedronGeometry args={[0.32, 1]} /><meshStandardMaterial color={GREEN} emissive={GREEN} emissiveIntensity={1.2} toneMapped={false} flatShading /></mesh> <mesh position={[0, 0.55, 0]}><icosahedronGeometry args={[0.32, 1]} /><meshStandardMaterial color={GREEN} emissive={GREEN} emissiveIntensity={1.2} toneMapped={false} flatShading /></mesh>
<mesh ref={halo} position={[0, 0.55, 0]} rotation={[Math.PI / 2.4, 0, 0]}><torusGeometry args={[0.5, 0.025, 10, 40]} /><meshStandardMaterial color={GREEN} emissive={GREEN} emissiveIntensity={1} toneMapped={false} /></mesh> <mesh ref={halo} position={[0, 0.55, 0]} rotation={[Math.PI / 2.4, 0, 0]}><torusGeometry args={[0.5, 0.025, 10, 40]} /><meshStandardMaterial color={GREEN} emissive={GREEN} emissiveIntensity={1} toneMapped={false} /></mesh>
</group> </group>
<Html center distanceFactor={9} position={[0, 1.8, -1.4]} zIndexRange={[20, 0]} pointerEvents="none">
<div className="dm-st3d-count" ref={register}><span ref={counter}>0</span> Orders</div>
</Html>
<Html center distanceFactor={9} position={[0, 1.32, -1.4]} zIndexRange={[20, 0]} pointerEvents="none">
<div className="dm-st3d-tag" style={{ ["--tc" as string]: GREEN }} ref={register}>🤖 AI Assignment Hub</div>
</Html>
{/* assignment routes hub → each vehicle node */} {/* assignment routes hub → each vehicle node */}
{routes.map((c, j) => ( {routes.map((c, j) => (
<group key={j}> <group key={j}>
<Line points={c.getPoints(2)} color={GREEN} lineWidth={1.6} transparent opacity={0.45} toneMapped={false} /> <Line points={c.getPoints(2)} color={GREEN} lineWidth={1.6} transparent opacity={0.45} toneMapped={false} />
{!reduced && <Packet curve={c} color={GREEN} speed={0.55} offset={j * 0.22} />} {awake && !reduced && <Packet curve={c} color={GREEN} speed={0.55} offset={j * 0.22} />}
</group> </group>
))} ))}
{/* soft contact shadow grounding the fleet (baked once for performance) */} {/* soft contact shadow grounding the fleet (baked once for performance) */}
<ContactShadows position={[0, 0.02, ROW_Z]} scale={[7.5, 2.6]} resolution={512} blur={2.6} far={1.2} opacity={0.4} frames={1} color="#1e293b" /> <ContactShadows position={[0, 0.02, ROW_Z]} scale={[7.5, 2.6]} resolution={512} blur={2.6} far={1.2} opacity={0.4} frames={1} color="#1e293b" />
{RIDERS.map((r) => <RiderAvatar key={r.id} rider={r} register={register} />)} {RIDERS.map((r) => <RiderAvatar key={r.id} rider={r} register={register} awake={awake} />)}
{awake && (
<>
<Html center distanceFactor={9} position={[ORDERS_SRC[0], ORDERS_SRC[1] + 0.5, ORDERS_SRC[2]]} zIndexRange={[20, 0]} pointerEvents="none">
<div className="dm-st3d-tag" style={{ ["--tc" as string]: GREEN }} ref={register}>📄 orders.csv</div>
</Html>
<Html center distanceFactor={9} position={[0, 1.8, -1.4]} zIndexRange={[20, 0]} pointerEvents="none">
<div className="dm-st3d-count" ref={register}><span ref={counter}>0</span> Orders</div>
</Html>
<Html center distanceFactor={9} position={[0, 1.32, -1.4]} zIndexRange={[20, 0]} pointerEvents="none">
<div className="dm-st3d-tag" style={{ ["--tc" as string]: GREEN }} ref={register}>🤖 AI Assignment Hub</div>
</Html>
</>
)}
</group> </group>
); );
}); });
@@ -361,12 +422,12 @@ const STRATS = [
]; ];
const CORE: [number, number, number] = [0, 1.45, -2.7]; const CORE: [number, number, number] = [0, 1.45, -2.7];
const StrategyNetwork = React.memo(function StrategyNetwork({ i, progress, reduced }: { i: number; progress: React.RefObject<number>; reduced: boolean }) { const StrategyNetwork = React.memo(function StrategyNetwork({ i, progress, reduced, awake }: { i: number; progress: React.RefObject<number>; reduced: boolean; awake: boolean }) {
const register = useLabelFade(i, progress); const register = useLabelFade(i, progress, awake);
const ring = useRef<THREE.Mesh>(null); const ring = useRef<THREE.Mesh>(null);
const coreMesh = useRef<THREE.Mesh>(null); const coreMesh = useRef<THREE.Mesh>(null);
useFrame((state, dt) => { useFrame((state, dt) => {
if (reduced) return; if (reduced || !awake) return;
if (ring.current) ring.current.rotation.z += dt * 0.6; if (ring.current) ring.current.rotation.z += dt * 0.6;
if (coreMesh.current) coreMesh.current.rotation.y += dt * 0.4; if (coreMesh.current) coreMesh.current.rotation.y += dt * 0.4;
}); });
@@ -381,7 +442,7 @@ const StrategyNetwork = React.memo(function StrategyNetwork({ i, progress, reduc
[], [],
); );
return ( return (
<group position={districtPosition(i)}> <group position={districtPosition(i)} visible={awake}>
{/* dark tech dais + glowing rim (reads as a control platform, not a white slab) */} {/* dark tech dais + glowing rim (reads as a control platform, not a white slab) */}
<mesh position={[0, 0.06, -0.6]}><cylinderGeometry args={[5.4, 5.6, 0.12, 56]} /><meshStandardMaterial color="#161b30" metalness={0.5} roughness={0.42} emissive={PURPLE} emissiveIntensity={0.07} /></mesh> <mesh position={[0, 0.06, -0.6]}><cylinderGeometry args={[5.4, 5.6, 0.12, 56]} /><meshStandardMaterial color="#161b30" metalness={0.5} roughness={0.42} emissive={PURPLE} emissiveIntensity={0.07} /></mesh>
<mesh position={[0, 0.13, -0.6]} rotation={[-Math.PI / 2, 0, 0]}><ringGeometry args={[5.2, 5.42, 64]} /><meshBasicMaterial color={PURPLE} transparent opacity={0.4} side={THREE.DoubleSide} toneMapped={false} /></mesh> <mesh position={[0, 0.13, -0.6]} rotation={[-Math.PI / 2, 0, 0]}><ringGeometry args={[5.2, 5.42, 64]} /><meshBasicMaterial color={PURPLE} transparent opacity={0.4} side={THREE.DoubleSide} toneMapped={false} /></mesh>
@@ -389,22 +450,26 @@ const StrategyNetwork = React.memo(function StrategyNetwork({ i, progress, reduc
<mesh position={[0, 0.65, -2.7]}><cylinderGeometry args={[0.4, 0.62, 1.2, 24]} /><meshStandardMaterial color="#2a3350" metalness={0.5} roughness={0.4} /></mesh> <mesh position={[0, 0.65, -2.7]}><cylinderGeometry args={[0.4, 0.62, 1.2, 24]} /><meshStandardMaterial color="#2a3350" metalness={0.5} roughness={0.4} /></mesh>
<mesh ref={coreMesh} position={CORE}><icosahedronGeometry args={[0.6, 1]} /><meshStandardMaterial color={PURPLE} emissive={PURPLE} emissiveIntensity={1.3} toneMapped={false} flatShading /></mesh> <mesh ref={coreMesh} position={CORE}><icosahedronGeometry args={[0.6, 1]} /><meshStandardMaterial color={PURPLE} emissive={PURPLE} emissiveIntensity={1.3} toneMapped={false} flatShading /></mesh>
<mesh ref={ring} position={CORE} rotation={[Math.PI / 2.4, 0, 0]}><torusGeometry args={[0.98, 0.03, 10, 44]} /><meshStandardMaterial color={PURPLE} emissive={PURPLE} emissiveIntensity={1.1} toneMapped={false} /></mesh> <mesh ref={ring} position={CORE} rotation={[Math.PI / 2.4, 0, 0]}><torusGeometry args={[0.98, 0.03, 10, 44]} /><meshStandardMaterial color={PURPLE} emissive={PURPLE} emissiveIntensity={1.1} toneMapped={false} /></mesh>
<Html center distanceFactor={9} position={[0, 2.5, -2.7]} zIndexRange={[20, 0]} pointerEvents="none"> {awake && (
<div className="dm-st3d-tag" style={{ ["--tc" as string]: PURPLE }} ref={register}>🤖 AI Engine</div> <Html center distanceFactor={9} position={[0, 2.5, -2.7]} zIndexRange={[20, 0]} pointerEvents="none">
</Html> <div className="dm-st3d-tag" style={{ ["--tc" as string]: PURPLE }} ref={register}>🤖 AI Engine</div>
</Html>
)}
{lanes.map((l, j) => { {lanes.map((l, j) => {
const col = l.unified ? PURPLE : "#a99bd6"; const col = l.unified ? PURPLE : "#a99bd6";
return ( return (
<group key={l.name}> <group key={l.name}>
<Line points={l.curve.getPoints(2)} color={col} lineWidth={l.unified ? 2.4 : 1.6} transparent opacity={l.unified ? 0.7 : 0.5} toneMapped={false} /> <Line points={l.curve.getPoints(2)} color={col} lineWidth={l.unified ? 2.4 : 1.6} transparent opacity={l.unified ? 0.7 : 0.5} toneMapped={false} />
{!reduced && <Packet curve={l.curve} color={col} speed={0.5} offset={j * 0.16} size={l.unified ? 0.075 : 0.06} />} {awake && !reduced && <Packet curve={l.curve} color={col} speed={0.5} offset={j * 0.16} size={l.unified ? 0.075 : 0.06} />}
{/* flat glowing route node (consistent with the dispatch network) */} {/* flat glowing route node (consistent with the dispatch network) */}
<mesh position={[l.end[0], 0.135, l.end[2]]} rotation={[-Math.PI / 2, 0, 0]}><ringGeometry args={[0.34, 0.44, 28]} /><meshBasicMaterial color={col} transparent opacity={l.unified ? 0.6 : 0.4} side={THREE.DoubleSide} toneMapped={false} /></mesh> <mesh position={[l.end[0], 0.135, l.end[2]]} rotation={[-Math.PI / 2, 0, 0]}><ringGeometry args={[0.34, 0.44, 28]} /><meshBasicMaterial color={col} transparent opacity={l.unified ? 0.6 : 0.4} side={THREE.DoubleSide} toneMapped={false} /></mesh>
<GlowNode position={l.end} color={col} size={l.unified ? 0.15 : 0.11} /> <GlowNode position={l.end} color={col} size={l.unified ? 0.15 : 0.11} />
<Html center distanceFactor={9} position={[l.end[0], l.end[1] + 0.62, l.end[2]]} zIndexRange={[20, 0]} pointerEvents="none"> {awake && (
<div className={`dm-st3d-tag ${l.unified ? "is-u" : "is-muted"}`} style={{ ["--tc" as string]: l.unified ? PURPLE : "#94a3b8" }} ref={register}>{l.name}</div> <Html center distanceFactor={9} position={[l.end[0], l.end[1] + 0.62, l.end[2]]} zIndexRange={[20, 0]} pointerEvents="none">
</Html> <div className={`dm-st3d-tag ${l.unified ? "is-u" : "is-muted"}`} style={{ ["--tc" as string]: l.unified ? PURPLE : "#94a3b8" }} ref={register}>{l.name}</div>
</Html>
)}
</group> </group>
); );
})} })}
@@ -467,8 +532,8 @@ function DeliveryBadge({ pos, i, progress }: { pos: [number, number, number]; i:
); );
} }
const CityRouteMap = React.memo(function CityRouteMap({ i, progress, reduced }: { i: number; progress: React.RefObject<number>; reduced: boolean }) { const CityRouteMap = React.memo(function CityRouteMap({ i, progress, reduced, awake }: { i: number; progress: React.RefObject<number>; reduced: boolean; awake: boolean }) {
const register = useLabelFade(i, progress); const register = useLabelFade(i, progress, awake);
// OPEN delivery path (Dispatch Hub → … → Delivery). Static "road"; only the truck moves. // OPEN delivery path (Dispatch Hub → … → Delivery). Static "road"; only the truck moves.
const route = useMemo(() => { const route = useMemo(() => {
const pts = [DEPOT3, ...DEST].map(([x, z]) => new THREE.Vector3(x, 0.12, z)); const pts = [DEPOT3, ...DEST].map(([x, z]) => new THREE.Vector3(x, 0.12, z));
@@ -477,11 +542,11 @@ const CityRouteMap = React.memo(function CityRouteMap({ i, progress, reduced }:
const routePts = useMemo(() => route.getPoints(100), [route]); const routePts = useMemo(() => route.getPoints(100), [route]);
const delivery = DEST[DEST.length - 1]; const delivery = DEST[DEST.length - 1];
return ( return (
<group position={districtPosition(i)}> <group position={districtPosition(i)} visible={awake}>
{/* STATIC delivery route (the road) — no flowing lines; the truck is the only mover */} {/* STATIC delivery route (the road) — no flowing lines; the truck is the only mover */}
<Line points={routePts} color={BLUE} lineWidth={2.6} transparent opacity={0.8} toneMapped={false} /> <Line points={routePts} color={BLUE} lineWidth={2.6} transparent opacity={0.8} toneMapped={false} />
<Suspense fallback={null}> <Suspense fallback={null}>
<RouteTruck route={route} reduced={reduced} i={i} progress={progress} /> <RouteTruck route={route} reduced={reduced} i={i} progress={progress} awake={awake} />
</Suspense> </Suspense>
{/* dispatch hub (static) */} {/* dispatch hub (static) */}
@@ -490,41 +555,46 @@ const CityRouteMap = React.memo(function CityRouteMap({ i, progress, reduced }:
<mesh position={[0, 0.13, 0]}><cylinderGeometry args={[0.5, 0.58, 0.26, 6]} /><meshStandardMaterial color="#0f2036" emissive={BLUE} emissiveIntensity={0.15} metalness={0.4} roughness={0.45} /></mesh> <mesh position={[0, 0.13, 0]}><cylinderGeometry args={[0.5, 0.58, 0.26, 6]} /><meshStandardMaterial color="#0f2036" emissive={BLUE} emissiveIntensity={0.15} metalness={0.4} roughness={0.45} /></mesh>
<GlowNode position={[0, 0.52, 0]} color={BLUE} size={0.14} /> <GlowNode position={[0, 0.52, 0]} color={BLUE} size={0.14} />
</group> </group>
<Html center distanceFactor={9} position={[DEPOT3[0], 1.05, DEPOT3[1]]} zIndexRange={[20, 0]} pointerEvents="none">
<div className="dm-st3d-tag" style={{ ["--tc" as string]: BLUE }} ref={register}>🏢 Dispatch Hub</div>
</Html>
{/* route nodes + delivery destination */} {/* route nodes + delivery destination (static geometry; labels gated below) */}
{DEST.map(([x, z], j) => { {DEST.map(([x, z], j) => {
const label = DEST_LABELS[j];
const isDelivery = j === DEST.length - 1; const isDelivery = j === DEST.length - 1;
return ( return (
<group key={j} position={[x, 0, z]}> <group key={j} position={[x, 0, z]}>
<mesh position={[0, 0.02, 0]} rotation={[-Math.PI / 2, 0, 0]}><ringGeometry args={[0.16, 0.23, 20]} /><meshBasicMaterial color={BLUE} transparent opacity={0.55} side={THREE.DoubleSide} toneMapped={false} /></mesh> <mesh position={[0, 0.02, 0]} rotation={[-Math.PI / 2, 0, 0]}><ringGeometry args={[0.16, 0.23, 20]} /><meshBasicMaterial color={BLUE} transparent opacity={0.55} side={THREE.DoubleSide} toneMapped={false} /></mesh>
<mesh position={[0, 0.3, 0]}><cylinderGeometry args={[0.03, 0.03, 0.6, 8]} /><meshStandardMaterial color={BLUE} emissive={BLUE} emissiveIntensity={0.4} /></mesh> <mesh position={[0, 0.3, 0]}><cylinderGeometry args={[0.03, 0.03, 0.6, 8]} /><meshStandardMaterial color={BLUE} emissive={BLUE} emissiveIntensity={0.4} /></mesh>
<GlowNode position={[0, 0.66, 0]} color={BLUE} size={isDelivery ? 0.16 : 0.12} /> <GlowNode position={[0, 0.66, 0]} color={BLUE} size={isDelivery ? 0.16 : 0.12} />
{label && (
<Html center distanceFactor={9} position={[0, 1.06, 0]} zIndexRange={[20, 0]} pointerEvents="none">
<div className="dm-st3d-tag" style={{ ["--tc" as string]: BLUE }} ref={register}>{label}</div>
</Html>
)}
</group> </group>
); );
})} })}
{/* delivery arrival pulse + completion badge (only when the truck arrives) */} {/* delivery arrival pulse (only mounted/animated while this district is active) */}
<DeliveryPulse pos={[delivery[0], 0.02, delivery[1]]} i={i} progress={progress} /> {awake && <DeliveryPulse pos={[delivery[0], 0.02, delivery[1]]} i={i} progress={progress} />}
<DeliveryBadge pos={[delivery[0], 1.42, delivery[1]]} i={i} progress={progress} />
{/* validation overlays + optimization result */} {awake && (
{VALIDATIONS.map((v) => ( <>
<Html key={v.t} center distanceFactor={9} position={v.p} zIndexRange={[20, 0]} pointerEvents="none"> <Html center distanceFactor={9} position={[DEPOT3[0], 1.05, DEPOT3[1]]} zIndexRange={[20, 0]} pointerEvents="none">
<div className="dm-st3d-tag" style={{ ["--tc" as string]: BLUE }} ref={register}> {v.t}</div> <div className="dm-st3d-tag" style={{ ["--tc" as string]: BLUE }} ref={register}>🏢 Dispatch Hub</div>
</Html> </Html>
))} {DEST.map(([x, z], j) => {
<Html center distanceFactor={9} position={[0, 3.2, -0.4]} zIndexRange={[20, 0]} pointerEvents="none"> const label = DEST_LABELS[j];
<div className="dm-st3d-score" style={{ ["--tc" as string]: BLUE }} ref={register}>🗺 Route optimized · <b>18%</b> distance</div> return label ? (
</Html> <Html key={j} center distanceFactor={9} position={[x, 1.06, z]} zIndexRange={[20, 0]} pointerEvents="none">
<div className="dm-st3d-tag" style={{ ["--tc" as string]: BLUE }} ref={register}>{label}</div>
</Html>
) : null;
})}
<DeliveryBadge pos={[delivery[0], 1.42, delivery[1]]} i={i} progress={progress} />
{VALIDATIONS.map((v) => (
<Html key={v.t} center distanceFactor={9} position={v.p} zIndexRange={[20, 0]} pointerEvents="none">
<div className="dm-st3d-tag" style={{ ["--tc" as string]: BLUE }} ref={register}> {v.t}</div>
</Html>
))}
<Html center distanceFactor={9} position={[0, 3.2, -0.4]} zIndexRange={[20, 0]} pointerEvents="none">
<div className="dm-st3d-score" style={{ ["--tc" as string]: BLUE }} ref={register}>🗺 Route optimized · <b>18%</b> distance</div>
</Html>
</>
)}
</group> </group>
); );
}); });
@@ -540,8 +610,8 @@ const KPIS = [
{ n: "Battery", v: 100, a: 0.66 }, { n: "Battery", v: 100, a: 0.66 },
]; ];
const CommandCenter = React.memo(function CommandCenter({ i, progress }: { i: number; progress: React.RefObject<number> }) { const CommandCenter = React.memo(function CommandCenter({ i, progress, awake }: { i: number; progress: React.RefObject<number>; awake: boolean }) {
const register = useLabelFade(i, progress); const register = useLabelFade(i, progress, awake);
const screens = useMemo( const screens = useMemo(
() => () =>
KPIS.map((k) => { KPIS.map((k) => {
@@ -552,7 +622,7 @@ const CommandCenter = React.memo(function CommandCenter({ i, progress }: { i: nu
[], [],
); );
return ( return (
<group position={districtPosition(i)}> <group position={districtPosition(i)} visible={awake}>
<mesh position={[0, 0.45, 1.4]} rotation={[-0.12, 0, 0]}><boxGeometry args={[5.6, 0.18, 1.5]} /><meshStandardMaterial color="#dfe4f1" metalness={0.4} roughness={0.4} /></mesh> <mesh position={[0, 0.45, 1.4]} rotation={[-0.12, 0, 0]}><boxGeometry args={[5.6, 0.18, 1.5]} /><meshStandardMaterial color="#dfe4f1" metalness={0.4} roughness={0.4} /></mesh>
<mesh position={[0, 0.12, 0.9]}><boxGeometry args={[5.2, 0.24, 0.6]} /><meshStandardMaterial color="#cdd5e6" metalness={0.3} roughness={0.5} /></mesh> <mesh position={[0, 0.12, 0.9]}><boxGeometry args={[5.2, 0.24, 0.6]} /><meshStandardMaterial color="#cdd5e6" metalness={0.3} roughness={0.5} /></mesh>
@@ -561,19 +631,23 @@ const CommandCenter = React.memo(function CommandCenter({ i, progress }: { i: nu
<RoundedBox args={[1.7, 1.15, 0.06]} radius={0.06} smoothness={2}> <RoundedBox args={[1.7, 1.15, 0.06]} radius={0.06} smoothness={2}>
<meshStandardMaterial color="#ffffff" emissive={ORANGE} emissiveIntensity={0.18} metalness={0.1} roughness={0.4} transparent opacity={0.92} /> <meshStandardMaterial color="#ffffff" emissive={ORANGE} emissiveIntensity={0.18} metalness={0.1} roughness={0.4} transparent opacity={0.92} />
</RoundedBox> </RoundedBox>
<Html center distanceFactor={8} position={[0, 0, 0.05]} zIndexRange={[20, 0]} pointerEvents="none"> {awake && (
<div className="dm-st3d-kpi" style={{ ["--tc" as string]: ORANGE }} ref={register}> <Html center distanceFactor={8} position={[0, 0, 0.05]} zIndexRange={[20, 0]} pointerEvents="none">
<span className="dm-st3d-kpi__n">{s.n}</span> <div className="dm-st3d-kpi" style={{ ["--tc" as string]: ORANGE }} ref={register}>
<span className="dm-st3d-kpi__v">{s.v}<i>%</i></span> <span className="dm-st3d-kpi__n">{s.n}</span>
<span className="dm-st3d-kpi__bar"><i style={{ width: `${s.v}%` }} /></span> <span className="dm-st3d-kpi__v">{s.v}<i>%</i></span>
</div> <span className="dm-st3d-kpi__bar"><i style={{ width: `${s.v}%` }} /></span>
</Html> </div>
</Html>
)}
</group> </group>
))} ))}
<Html center distanceFactor={9} position={[0, 3.1, -0.4]} zIndexRange={[20, 0]} pointerEvents="none"> {awake && (
<div className="dm-st3d-score" style={{ ["--tc" as string]: ORANGE }} ref={register}>Performance Grade <b>A</b> · 4.5 / 5</div> <Html center distanceFactor={9} position={[0, 3.1, -0.4]} zIndexRange={[20, 0]} pointerEvents="none">
</Html> <div className="dm-st3d-score" style={{ ["--tc" as string]: ORANGE }} ref={register}>Performance Grade <b>A</b> · 4.5 / 5</div>
</Html>
)}
</group> </group>
); );
}); });
@@ -589,7 +663,7 @@ const PODIUM = [
{ n: "Proximity", v: 64, x: 2.4, win: false }, { n: "Proximity", v: 64, x: 2.4, win: false },
]; ];
function Pillar({ p, register }: { p: typeof PODIUM[number]; register: (el: HTMLElement | null) => void }) { function Pillar({ p, register, awake }: { p: typeof PODIUM[number]; register: (el: HTMLElement | null) => void; awake: boolean }) {
const h = 0.6 + (p.v / 100) * 2.2; const h = 0.6 + (p.v / 100) * 2.2;
const col = p.win ? RED : "#94a3b8"; const col = p.win ? RED : "#94a3b8";
return ( return (
@@ -598,30 +672,32 @@ function Pillar({ p, register }: { p: typeof PODIUM[number]; register: (el: HTML
<boxGeometry args={[1.0, h, 1.0]} /> <boxGeometry args={[1.0, h, 1.0]} />
<meshStandardMaterial color={col} emissive={col} emissiveIntensity={p.win ? 0.7 : 0.15} metalness={0.2} roughness={0.4} transparent opacity={p.win ? 0.96 : 0.7} toneMapped={false} /> <meshStandardMaterial color={col} emissive={col} emissiveIntensity={p.win ? 0.7 : 0.15} metalness={0.2} roughness={0.4} transparent opacity={p.win ? 0.96 : 0.7} toneMapped={false} />
</mesh> </mesh>
<Html center distanceFactor={9} position={[0, h + 0.45, 0]} zIndexRange={[20, 0]} pointerEvents="none"> {awake && (
<div className={`dm-st3d-tag ${p.win ? "is-win" : "is-muted"}`} style={{ ["--tc" as string]: col }} ref={register}><b>{p.v}%</b>&nbsp;{p.n}</div> <Html center distanceFactor={9} position={[0, h + 0.45, 0]} zIndexRange={[20, 0]} pointerEvents="none">
</Html> <div className={`dm-st3d-tag ${p.win ? "is-win" : "is-muted"}`} style={{ ["--tc" as string]: col }} ref={register}><b>{p.v}%</b>&nbsp;{p.n}</div>
</Html>
)}
</group> </group>
); );
} }
const WinnerPodium = React.memo(function WinnerPodium({ i, progress, reduced, isMobile }: { i: number; progress: React.RefObject<number>; reduced: boolean; isMobile: boolean }) { const WinnerPodium = React.memo(function WinnerPodium({ i, progress, reduced, isMobile, awake }: { i: number; progress: React.RefObject<number>; reduced: boolean; isMobile: boolean; awake: boolean }) {
const register = useLabelFade(i, progress); const register = useLabelFade(i, progress, awake);
const trophy = useRef<THREE.Group>(null); const trophy = useRef<THREE.Group>(null);
useFrame((state) => { useFrame((state) => {
if (trophy.current && !reduced) { if (trophy.current && !reduced && awake) {
const winH = 0.6 + (88 / 100) * 2.2; const winH = 0.6 + (88 / 100) * 2.2;
trophy.current.position.y = winH + 0.95 + Math.sin(state.clock.elapsedTime * 1.4) * 0.08; trophy.current.position.y = winH + 0.95 + Math.sin(state.clock.elapsedTime * 1.4) * 0.08;
trophy.current.rotation.y += 0.01; trophy.current.rotation.y += 0.01;
} }
}); });
return ( return (
<group position={districtPosition(i)}> <group position={districtPosition(i)} visible={awake}>
<mesh position={[0, 0.05, -0.6]}><cylinderGeometry args={[4.3, 4.5, 0.12, 48]} /><meshStandardMaterial color="#1d1622" metalness={0.5} roughness={0.42} emissive={RED} emissiveIntensity={0.06} /></mesh> <mesh position={[0, 0.05, -0.6]}><cylinderGeometry args={[4.3, 4.5, 0.12, 48]} /><meshStandardMaterial color="#1d1622" metalness={0.5} roughness={0.42} emissive={RED} emissiveIntensity={0.06} /></mesh>
<mesh position={[0, 0.12, -0.6]} rotation={[-Math.PI / 2, 0, 0]}><ringGeometry args={[4.1, 4.32, 56]} /><meshBasicMaterial color={RED} transparent opacity={0.35} side={THREE.DoubleSide} toneMapped={false} /></mesh> <mesh position={[0, 0.12, -0.6]} rotation={[-Math.PI / 2, 0, 0]}><ringGeometry args={[4.1, 4.32, 56]} /><meshBasicMaterial color={RED} transparent opacity={0.35} side={THREE.DoubleSide} toneMapped={false} /></mesh>
<mesh position={[-2.4, 0.14, -0.6]}><cylinderGeometry args={[0.85, 0.9, 0.14, 28]} /><meshStandardMaterial color={RED} emissive={RED} emissiveIntensity={0.9} toneMapped={false} /></mesh> <mesh position={[-2.4, 0.14, -0.6]}><cylinderGeometry args={[0.85, 0.9, 0.14, 28]} /><meshStandardMaterial color={RED} emissive={RED} emissiveIntensity={0.9} toneMapped={false} /></mesh>
{PODIUM.map((p) => <Pillar key={p.n} p={p} register={register} />)} {PODIUM.map((p) => <Pillar key={p.n} p={p} register={register} awake={awake} />)}
<mesh position={[-2.4, 3.0, -0.6]}><coneGeometry args={[1.0, 2.6, 28, 1, true]} /><meshBasicMaterial color={RED} transparent opacity={0.07} side={THREE.DoubleSide} toneMapped={false} /></mesh> <mesh position={[-2.4, 3.0, -0.6]}><coneGeometry args={[1.0, 2.6, 28, 1, true]} /><meshBasicMaterial color={RED} transparent opacity={0.07} side={THREE.DoubleSide} toneMapped={false} /></mesh>
<group ref={trophy} position={[-2.4, 3.5, -0.6]}> <group ref={trophy} position={[-2.4, 3.5, -0.6]}>
@@ -630,16 +706,18 @@ const WinnerPodium = React.memo(function WinnerPodium({ i, progress, reduced, is
<mesh position={[0, -0.22, 0]}><cylinderGeometry args={[0.18, 0.22, 0.1, 16]} /><meshStandardMaterial color="#FFD45A" emissive="#FFB020" emissiveIntensity={1} toneMapped={false} /></mesh> <mesh position={[0, -0.22, 0]}><cylinderGeometry args={[0.18, 0.22, 0.1, 16]} /><meshStandardMaterial color="#FFD45A" emissive="#FFB020" emissiveIntensity={1} toneMapped={false} /></mesh>
</group> </group>
{!reduced && <Sparkles count={isMobile ? 16 : 28} scale={[5, 4, 4]} position={[-2.4, 2.6, -0.6]} size={3.2} speed={0.5} opacity={0.9} color="#ff9aa9" />} {awake && !reduced && <Sparkles count={isMobile ? 16 : 28} scale={[5, 4, 4]} position={[-2.4, 2.6, -0.6]} size={3.2} speed={0.5} opacity={0.9} color="#ff9aa9" />}
<Html center distanceFactor={9} position={[0.7, 3.0, -0.6]} zIndexRange={[20, 0]} pointerEvents="none"> {awake && (
<div className="dm-st3d-winner3d" ref={register}> <Html center distanceFactor={9} position={[0.7, 3.0, -0.6]} zIndexRange={[20, 0]} pointerEvents="none">
<span className="dm-st3d-winner3d__top">🏆 Best Strategy</span> <div className="dm-st3d-winner3d" ref={register}>
<span className="dm-st3d-winner3d__name">EV Aware</span> <span className="dm-st3d-winner3d__top">🏆 Best Strategy</span>
<span className="dm-st3d-winner3d__row"><b>88%</b> Performance Score</span> <span className="dm-st3d-winner3d__name">EV Aware</span>
<span className="dm-st3d-winner3d__row"><b>52/59</b> Orders Fulfilled</span> <span className="dm-st3d-winner3d__row"><b>88%</b> Performance Score</span>
</div> <span className="dm-st3d-winner3d__row"><b>52/59</b> Orders Fulfilled</span>
</Html> </div>
</Html>
)}
</group> </group>
); );
}); });
@@ -704,8 +782,21 @@ function Floor() {
); );
} }
function Scene({ progress, reduced, isMobile, stage }: { progress: React.RefObject<number>; reduced: boolean; isMobile: boolean; stage: number }) { function Scene({ progress, reduced, isMobile, stage, active, perf }: { progress: React.RefObject<number>; reduced: boolean; isMobile: boolean; stage: number; active: boolean; perf: boolean }) {
const near = (i: number) => Math.abs(i - stage) <= 1; // only mount active ± 1 district // Districts stay MOUNTED for the whole scroll (GLB cloned once, shaders compiled
// once → no remount hitch, no blank frames). `near` only toggles per-district
// visibility + per-frame work + <Html> mounting, so off-screen districts render
// nothing and run zero per-frame CPU/DOM cost (only the active ±1 stays awake).
const near = (i: number) => Math.abs(i - stage) <= 1;
// Defer the expensive post-processing + ambient particles until the section is
// actually active for the first time, then latch them on. This keeps Bloom's
// render targets and the Sparkles buffers from being allocated during initial
// page load / while WF3 is still below the fold — without re-allocating them
// (and hitching) every time the user scrolls past and back.
const [everActive, setEverActive] = useState(false);
useEffect(() => { if (active) setEverActive(true); }, [active]);
const heavyFx = everActive && !reduced && !isMobile;
return ( return (
<> <>
<color attach="background" args={[BG]} /> <color attach="background" args={[BG]} />
@@ -727,26 +818,33 @@ function Scene({ progress, reduced, isMobile, stage }: { progress: React.RefObje
<Floor /> <Floor />
<DataRoad /> <DataRoad />
{near(0) && <IntakeHub i={0} progress={progress} reduced={reduced} />} <IntakeHub i={0} progress={progress} reduced={reduced} awake={near(0)} />
{near(1) && <StrategyNetwork i={1} progress={progress} reduced={reduced} />} <StrategyNetwork i={1} progress={progress} reduced={reduced} awake={near(1)} />
{near(2) && <CityRouteMap i={2} progress={progress} reduced={reduced} />} <CityRouteMap i={2} progress={progress} reduced={reduced} awake={near(2)} />
{near(3) && <CommandCenter i={3} progress={progress} />} <CommandCenter i={3} progress={progress} awake={near(3)} />
{near(4) && <WinnerPodium i={4} progress={progress} reduced={reduced} isMobile={isMobile} />} <WinnerPodium i={4} progress={progress} reduced={reduced} isMobile={isMobile} awake={near(4)} />
{!reduced && !isMobile && ( {heavyFx && (
<Sparkles count={30} scale={[18, 7, N * 13]} position={[0, 3, (-(N - 1) * 13) / 2]} size={2} speed={0.22} opacity={0.35} color="#9aa6c4" /> <Sparkles count={30} scale={[18, 7, N * 13]} position={[0, 3, (-(N - 1) * 13) / 2]} size={2} speed={0.22} opacity={0.35} color="#9aa6c4" />
)} )}
{!reduced && !isMobile && ( {heavyFx && (
<EffectComposer multisampling={0}> <EffectComposer multisampling={0}>
<Bloom mipmapBlur intensity={0.7} luminanceThreshold={0.74} luminanceSmoothing={0.06} radius={0.68} kernelSize={KernelSize.SMALL} /> <Bloom mipmapBlur intensity={0.7} luminanceThreshold={0.74} luminanceSmoothing={0.06} radius={0.68} kernelSize={KernelSize.SMALL} />
</EffectComposer> </EffectComposer>
)} )}
{/* Compile every material/texture upfront (districts are all mounted) so the
first time a district becomes visible there's no shader-compile stall. */}
<Preload all />
{perf && <PerfHud />}
</> </>
); );
} }
export default function StrategyCanvas({ progress, reduced = false, isMobile = false, active = true, stage = 0 }: Props) { export default function StrategyCanvas({ progress, reduced = false, isMobile = false, active = true, stage = 0 }: Props) {
// Opt-in dev metrics overlay: append ?perf to the URL.
const perf = typeof window !== "undefined" && new URLSearchParams(window.location.search).has("perf");
return ( return (
<Canvas <Canvas
dpr={[1, isMobile ? 1 : 1.25]} dpr={[1, isMobile ? 1 : 1.25]}
@@ -754,7 +852,7 @@ export default function StrategyCanvas({ progress, reduced = false, isMobile = f
gl={{ antialias: false, powerPreference: "high-performance", alpha: false }} gl={{ antialias: false, powerPreference: "high-performance", alpha: false }}
frameloop={active ? "always" : "never"} frameloop={active ? "always" : "never"}
> >
<Scene progress={progress} reduced={reduced} isMobile={isMobile} stage={stage} /> <Scene progress={progress} reduced={reduced} isMobile={isMobile} stage={stage} active={active} perf={perf} />
</Canvas> </Canvas>
); );
} }