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

View File

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

View File

@@ -2,18 +2,26 @@
import React, { useEffect, useRef, useState } from "react";
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 { ScrollTrigger } from "gsap/ScrollTrigger";
import { P, STRATEGIES, ENGINE_STEPS, CONSTRAINT_LIST, STRATEGY_SCORES } from "./theme";
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> }) {
const [v, setV] = useState(0);
useMotionValueEvent(mv, "change", (x) => setV(Math.round(x)));
return <>{v}</>;
const ref = useRef<HTMLSpanElement>(null);
useEffect(() => {
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). */

View File

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

View File

@@ -134,7 +134,8 @@ function VehicleFleet({ progress, reduced = false }: Props) {
if (!visible) return;
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
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%)
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
u = (u + dt * def.speed * speedFactor) % 1;
@@ -195,26 +196,46 @@ function VehicleFleet({ progress, reduced = false }: Props) {
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 chaosFadeIn = smoothstep(0.1, 0.22, p);
const chaosFadeOut = 1 - smoothstep(0.7, 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);
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);
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);
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);
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];
@@ -223,7 +244,10 @@ function VehicleFleet({ progress, reduced = false }: Props) {
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; }
if (optOp < 0.02) {
mesh.visible = false;
continue;
}
let uk = u - (k + 1) * TRAIL_GAP;
if (uk < 0) uk = 0;
mesh.visible = true;
@@ -233,7 +257,8 @@ function VehicleFleet({ progress, reduced = false }: Props) {
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);
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;
}}
>
<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>
))}
{/* Optimized fleet — the truck art in clean white */}
@@ -260,7 +290,12 @@ function VehicleFleet({ progress, reduced = false }: Props) {
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>
))}
@@ -290,5 +325,3 @@ function VehicleFleet({ progress, reduced = false }: Props) {
}
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";
import React, { Suspense, useEffect, useMemo, useRef } from "react";
import { Canvas, useFrame } from "@react-three/fiber";
import { RoundedBox, Line, Sparkles, Html, useGLTF, ContactShadows, Environment, Lightformer } from "@react-three/drei";
import React, { Suspense, useEffect, useMemo, useRef, useState } from "react";
import { Canvas, useFrame, useThree } from "@react-three/fiber";
import { RoundedBox, Line, Sparkles, Html, useGLTF, ContactShadows, Environment, Lightformer, Preload } from "@react-three/drei";
import { EffectComposer, Bloom } from "@react-three/postprocessing";
import { KernelSize } from "postprocessing";
import * as THREE from "three";
@@ -26,6 +26,53 @@ const BLUE = "#3B82F6"; // 03 optimization
const ORANGE = "#F59E0B"; // 04 grading
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
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
--------------------------------------------------------------------------- */
/** Fade a district's drei <Html> chips in/out by camera proximity (ref-driven). */
function useLabelFade(i: number, progress: React.RefObject<number>) {
/** Fade a district's drei <Html> chips in/out by camera proximity (ref-driven).
* 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 register = (el: HTMLElement | null) => {
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(() => {
if (!awake) return;
const idx = (progress.current ?? 0) * (N - 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);
@@ -162,14 +215,14 @@ function buildModel(scene: THREE.Object3D, targetSize: number) {
return { root, wheels };
}
function GLBVehicle({ kind }: { kind: string }) {
function GLBVehicle({ kind, awake = true }: { kind: string; awake?: boolean }) {
const tune = VEH_TUNE[kind];
const { scene } = useGLTF(MODELS[kind]);
const { root } = useMemo(() => buildModel(scene, tune.size), [scene, tune]);
const g = useRef<THREE.Group>(null);
useFrame((state) => {
const grp = g.current;
if (!grp) return;
if (!grp || !awake) return;
const t = state.clock.elapsedTime;
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;
@@ -195,7 +248,7 @@ const TRUCK_FWD_YAW = 0;
* 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.
*/
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 built = useMemo(() => buildModel(scene, 0.85), [scene]);
const wheels = useRef<THREE.Object3D[]>([]);
@@ -207,7 +260,7 @@ function RouteTruck({ route, reduced, i, progress }: { route: THREE.CatmullRomCu
const prevT = useRef(0);
useFrame((state, dt) => {
const grp = g.current;
if (!grp) return;
if (!grp || !awake) return;
const idx = (progress.current ?? 0) * (N - 1);
const t = THREE.MathUtils.clamp(idx - (i - 0.5), 0, 1); // 0 → hub … 1 → delivery
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). */
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 (
<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.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]}>
<Suspense fallback={null}>
<GLBVehicle kind={rider.kind} />
<GLBVehicle kind={rider.kind} awake={awake} />
</Suspense>
</group>
<Html center distanceFactor={9} position={[0, 1.5, 0]} zIndexRange={[20, 0]} pointerEvents="none">
<div className="dm-st3d-chip" ref={register}>
<span className="dm-st3d-chip__ico">{rider.icon}</span>
<span className="dm-st3d-chip__txt"><b>Rider {rider.id}</b>{rider.veh}</span>
</div>
</Html>
{awake && (
<Html center distanceFactor={9} position={[0, 1.5, 0]} zIndexRange={[20, 0]} pointerEvents="none">
<div className="dm-st3d-chip" ref={register}>
<span className="dm-st3d-chip__ico">{rider.icon}</span>
<span className="dm-st3d-chip__txt"><b>Rider {rider.id}</b>{rider.veh}</span>
</div>
</Html>
)}
</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 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 register = useLabelFade(i, progress);
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, awake);
const counter = useRef<HTMLSpanElement>(null);
const halo = useRef<THREE.Mesh>(null);
useFrame((state, dt) => {
if (!awake) return;
if (counter.current) {
if (reduced) counter.current.textContent = "59";
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 routes = useMemo(() => RIDERS.map((r) => lineGeom(HUB_CORE, [r.x, 0.12, ROW_Z])), []);
return (
<group position={districtPosition(i)}>
<group position={districtPosition(i)} visible={awake}>
{/* orders.csv source node → intake route → hub (order packets flowing in) */}
<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} />
{!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>
{awake && !reduced && [0, 0.5].map((o) => <OrderPacket key={o} curve={intake} offset={o} />)}
{/* AI Assignment Hub — a grounded dispatch hub (rim ring + hex dais + glowing core + halo) */}
<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 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>
<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 */}
{routes.map((c, j) => (
<group key={j}>
<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>
))}
{/* 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" />
{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>
);
});
@@ -361,12 +422,12 @@ const STRATS = [
];
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 register = useLabelFade(i, progress);
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, awake);
const ring = useRef<THREE.Mesh>(null);
const coreMesh = useRef<THREE.Mesh>(null);
useFrame((state, dt) => {
if (reduced) return;
if (reduced || !awake) return;
if (ring.current) ring.current.rotation.z += dt * 0.6;
if (coreMesh.current) coreMesh.current.rotation.y += dt * 0.4;
});
@@ -381,7 +442,7 @@ const StrategyNetwork = React.memo(function StrategyNetwork({ i, progress, reduc
[],
);
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) */}
<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>
@@ -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 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>
<Html center distanceFactor={9} position={[0, 2.5, -2.7]} zIndexRange={[20, 0]} pointerEvents="none">
<div className="dm-st3d-tag" style={{ ["--tc" as string]: PURPLE }} ref={register}>🤖 AI Engine</div>
</Html>
{awake && (
<Html center distanceFactor={9} position={[0, 2.5, -2.7]} zIndexRange={[20, 0]} pointerEvents="none">
<div className="dm-st3d-tag" style={{ ["--tc" as string]: PURPLE }} ref={register}>🤖 AI Engine</div>
</Html>
)}
{lanes.map((l, j) => {
const col = l.unified ? PURPLE : "#a99bd6";
return (
<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} />
{!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) */}
<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} />
<Html center distanceFactor={9} position={[l.end[0], l.end[1] + 0.62, l.end[2]]} zIndexRange={[20, 0]} pointerEvents="none">
<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>
{awake && (
<Html center distanceFactor={9} position={[l.end[0], l.end[1] + 0.62, l.end[2]]} zIndexRange={[20, 0]} pointerEvents="none">
<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>
);
})}
@@ -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 register = useLabelFade(i, progress);
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, awake);
// OPEN delivery path (Dispatch Hub → … → Delivery). Static "road"; only the truck moves.
const route = useMemo(() => {
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 delivery = DEST[DEST.length - 1];
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 */}
<Line points={routePts} color={BLUE} lineWidth={2.6} transparent opacity={0.8} toneMapped={false} />
<Suspense fallback={null}>
<RouteTruck route={route} reduced={reduced} i={i} progress={progress} />
<RouteTruck route={route} reduced={reduced} i={i} progress={progress} awake={awake} />
</Suspense>
{/* 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>
<GlowNode position={[0, 0.52, 0]} color={BLUE} size={0.14} />
</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) => {
const label = DEST_LABELS[j];
const isDelivery = j === DEST.length - 1;
return (
<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.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} />
{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>
);
})}
{/* delivery arrival pulse + completion badge (only when the truck arrives) */}
<DeliveryPulse pos={[delivery[0], 0.02, delivery[1]]} i={i} progress={progress} />
<DeliveryBadge pos={[delivery[0], 1.42, delivery[1]]} i={i} progress={progress} />
{/* delivery arrival pulse (only mounted/animated while this district is active) */}
{awake && <DeliveryPulse pos={[delivery[0], 0.02, delivery[1]]} i={i} progress={progress} />}
{/* validation overlays + optimization result */}
{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>
{awake && (
<>
<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>
{DEST.map(([x, z], j) => {
const label = DEST_LABELS[j];
return label ? (
<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>
);
});
@@ -540,8 +610,8 @@ const KPIS = [
{ n: "Battery", v: 100, a: 0.66 },
];
const CommandCenter = React.memo(function CommandCenter({ i, progress }: { i: number; progress: React.RefObject<number> }) {
const register = useLabelFade(i, progress);
const CommandCenter = React.memo(function CommandCenter({ i, progress, awake }: { i: number; progress: React.RefObject<number>; awake: boolean }) {
const register = useLabelFade(i, progress, awake);
const screens = useMemo(
() =>
KPIS.map((k) => {
@@ -552,7 +622,7 @@ const CommandCenter = React.memo(function CommandCenter({ i, progress }: { i: nu
[],
);
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.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}>
<meshStandardMaterial color="#ffffff" emissive={ORANGE} emissiveIntensity={0.18} metalness={0.1} roughness={0.4} transparent opacity={0.92} />
</RoundedBox>
<Html center distanceFactor={8} position={[0, 0, 0.05]} zIndexRange={[20, 0]} pointerEvents="none">
<div className="dm-st3d-kpi" style={{ ["--tc" as string]: ORANGE }} ref={register}>
<span className="dm-st3d-kpi__n">{s.n}</span>
<span className="dm-st3d-kpi__v">{s.v}<i>%</i></span>
<span className="dm-st3d-kpi__bar"><i style={{ width: `${s.v}%` }} /></span>
</div>
</Html>
{awake && (
<Html center distanceFactor={8} position={[0, 0, 0.05]} zIndexRange={[20, 0]} pointerEvents="none">
<div className="dm-st3d-kpi" style={{ ["--tc" as string]: ORANGE }} ref={register}>
<span className="dm-st3d-kpi__n">{s.n}</span>
<span className="dm-st3d-kpi__v">{s.v}<i>%</i></span>
<span className="dm-st3d-kpi__bar"><i style={{ width: `${s.v}%` }} /></span>
</div>
</Html>
)}
</group>
))}
<Html center distanceFactor={9} position={[0, 3.1, -0.4]} zIndexRange={[20, 0]} pointerEvents="none">
<div className="dm-st3d-score" style={{ ["--tc" as string]: ORANGE }} ref={register}>Performance Grade <b>A</b> · 4.5 / 5</div>
</Html>
{awake && (
<Html center distanceFactor={9} position={[0, 3.1, -0.4]} zIndexRange={[20, 0]} pointerEvents="none">
<div className="dm-st3d-score" style={{ ["--tc" as string]: ORANGE }} ref={register}>Performance Grade <b>A</b> · 4.5 / 5</div>
</Html>
)}
</group>
);
});
@@ -589,7 +663,7 @@ const PODIUM = [
{ 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 col = p.win ? RED : "#94a3b8";
return (
@@ -598,30 +672,32 @@ function Pillar({ p, register }: { p: typeof PODIUM[number]; register: (el: HTML
<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} />
</mesh>
<Html center distanceFactor={9} position={[0, h + 0.45, 0]} zIndexRange={[20, 0]} pointerEvents="none">
<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>
{awake && (
<Html center distanceFactor={9} position={[0, h + 0.45, 0]} zIndexRange={[20, 0]} pointerEvents="none">
<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>
);
}
const WinnerPodium = React.memo(function WinnerPodium({ i, progress, reduced, isMobile }: { i: number; progress: React.RefObject<number>; reduced: boolean; isMobile: boolean }) {
const register = useLabelFade(i, progress);
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, awake);
const trophy = useRef<THREE.Group>(null);
useFrame((state) => {
if (trophy.current && !reduced) {
if (trophy.current && !reduced && awake) {
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.rotation.y += 0.01;
}
});
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.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>
{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>
<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>
</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">
<div className="dm-st3d-winner3d" ref={register}>
<span className="dm-st3d-winner3d__top">🏆 Best Strategy</span>
<span className="dm-st3d-winner3d__name">EV Aware</span>
<span className="dm-st3d-winner3d__row"><b>88%</b> Performance Score</span>
<span className="dm-st3d-winner3d__row"><b>52/59</b> Orders Fulfilled</span>
</div>
</Html>
{awake && (
<Html center distanceFactor={9} position={[0.7, 3.0, -0.6]} zIndexRange={[20, 0]} pointerEvents="none">
<div className="dm-st3d-winner3d" ref={register}>
<span className="dm-st3d-winner3d__top">🏆 Best Strategy</span>
<span className="dm-st3d-winner3d__name">EV Aware</span>
<span className="dm-st3d-winner3d__row"><b>88%</b> Performance Score</span>
<span className="dm-st3d-winner3d__row"><b>52/59</b> Orders Fulfilled</span>
</div>
</Html>
)}
</group>
);
});
@@ -704,8 +782,21 @@ function Floor() {
);
}
function Scene({ progress, reduced, isMobile, stage }: { progress: React.RefObject<number>; reduced: boolean; isMobile: boolean; stage: number }) {
const near = (i: number) => Math.abs(i - stage) <= 1; // only mount active ± 1 district
function Scene({ progress, reduced, isMobile, stage, active, perf }: { progress: React.RefObject<number>; reduced: boolean; isMobile: boolean; stage: number; active: boolean; perf: boolean }) {
// 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 (
<>
<color attach="background" args={[BG]} />
@@ -727,26 +818,33 @@ function Scene({ progress, reduced, isMobile, stage }: { progress: React.RefObje
<Floor />
<DataRoad />
{near(0) && <IntakeHub i={0} progress={progress} reduced={reduced} />}
{near(1) && <StrategyNetwork i={1} progress={progress} reduced={reduced} />}
{near(2) && <CityRouteMap i={2} progress={progress} reduced={reduced} />}
{near(3) && <CommandCenter i={3} progress={progress} />}
{near(4) && <WinnerPodium i={4} progress={progress} reduced={reduced} isMobile={isMobile} />}
<IntakeHub i={0} progress={progress} reduced={reduced} awake={near(0)} />
<StrategyNetwork i={1} progress={progress} reduced={reduced} awake={near(1)} />
<CityRouteMap i={2} progress={progress} reduced={reduced} awake={near(2)} />
<CommandCenter i={3} progress={progress} awake={near(3)} />
<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" />
)}
{!reduced && !isMobile && (
{heavyFx && (
<EffectComposer multisampling={0}>
<Bloom mipmapBlur intensity={0.7} luminanceThreshold={0.74} luminanceSmoothing={0.06} radius={0.68} kernelSize={KernelSize.SMALL} />
</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) {
// Opt-in dev metrics overlay: append ?perf to the URL.
const perf = typeof window !== "undefined" && new URLSearchParams(window.location.search).has("perf");
return (
<Canvas
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 }}
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>
);
}