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
* ---------------------------------------------------------------------------
* One global Lenis instance, driven by a SINGLE rAF source (GSAP's ticker) and
* kept locked to ScrollTrigger. Deliberately gated OFF on:
* - /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
* scroll is used on that route.
* kept locked to ScrollTrigger via `lenis.on("scroll", ScrollTrigger.update)`.
*
* Enabled on every route. /miletruth's three scroll-driven WebGL sections use a
* 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.
* - prefers-reduced-motion.
*
* 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() {
const pathname = usePathname();

View File

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

View File

@@ -63,7 +63,7 @@ export default function RootLayout({
children: React.ReactNode;
}>) {
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>
{/* FontAwesome icons */}
<link

View File

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

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>
);
}