update miletruth page

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

1682
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -9,18 +9,26 @@
"lint": "eslint"
},
"dependencies": {
"@react-three/drei": "^10.7.7",
"@react-three/fiber": "^9.6.1",
"@react-three/postprocessing": "^3.0.4",
"framer-motion": "^12.40.0",
"gsap": "^3.15.0",
"lenis": "^1.3.23",
"next": "16.2.6",
"react": "19.2.4",
"react-dom": "19.2.4"
"react-dom": "19.2.4",
"three": "^0.171.0"
},
"devDependencies": {
"@tailwindcss/postcss": "^4",
"@types/node": "^20",
"@types/react": "^19",
"@types/react-dom": "^19",
"@types/three": "^0.171.0",
"eslint": "^9",
"eslint-config-next": "16.2.6",
"puppeteer-core": "^23.11.1",
"tailwindcss": "^4",
"typescript": "^5"
}

BIN
public/images/home-bg-1.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 MiB

BIN
public/images/truck.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 41 KiB

View File

@@ -3,6 +3,8 @@ 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 OptimizationSection from "../../components/optimization/OptimizationSection";
import PerformanceSection from "../../components/performance/PerformanceSection";
export const metadata = {
title: "MileTruth Doormile",
@@ -16,9 +18,11 @@ export default function MileTruthPage() {
<div className="content-inner">
<div data-elementor-type="wp-page" data-elementor-id="59" className="elementor elementor-59">
<MileTruthHero />
<OptimizationSection />
<Workflow1 />
<Workflow2 />
<Workflow3 />
<PerformanceSection />
</div>
</div>
</div>

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,464 @@
"use client";
import React, { useMemo, useRef } from "react";
import { Canvas, useFrame } from "@react-three/fiber";
import { Html } from "@react-three/drei";
import { EffectComposer, Bloom, Vignette } from "@react-three/postprocessing";
import { KernelSize } from "postprocessing";
import * as THREE from "three";
import { buildPerfRoutes, GATEWAY, LEFT_C, RIGHT_C, ROUTE_Y } from "./perfRoutes";
import { SplineRider, VType } from "./Vehicles";
type Props = {
progress: React.RefObject<number>;
reduced?: boolean;
isMobile?: boolean;
active?: boolean;
};
// Local math helpers (self-contained; avoids fragile cross-folder const imports).
const clamp01 = (v: number) => (v < 0 ? 0 : v > 1 ? 1 : v);
const lerp = (a: number, b: number, t: number) => a + (b - a) * t;
const smoothstep = (e0: number, e1: number, x: number) => {
const t = clamp01((x - e0) / (e1 - e0 || 1));
return t * t * (3 - 2 * t);
};
const damp = (cur: number, target: number, lambda: number, dt: number) =>
lerp(cur, target, 1 - Math.exp(-lambda * dt));
const seeded = (i: number) => {
const x = Math.sin(i * 127.1 + 311.7) * 43758.5453;
return x - Math.floor(x);
};
// Grounded "operational" palette — white / gray / green / orange / red. No cyan/blue/purple.
const C = {
bg: "#14171c",
ground: "#262b33",
road: "#2b3038",
bldA: "#4b5563",
bldB: "#6b7280",
bldC: "#9ca3af",
white: "#e5e7eb",
red: "#ef4444",
orange: "#f97316",
amber: "#fbbf24",
green: "#22c55e",
green2: "#4ade80",
steel: "#475569",
};
/* ───────────── Camera: grounded city flythrough (not orbit) ───────────── */
type Key = { p: number; pos: [number, number, number]; look: [number, number, number] };
const KEYS: Key[] = [
{ p: 0.0, pos: [0, 9, 20], look: [0, 0.8, 0] }, // wide — framed on BOTH districts' content (instant contrast)
{ p: 0.28, pos: [-13, 3.4, 8.5], look: [-8, 1.0, 0] }, // road-level, chaotic left
{ p: 0.5, pos: [0, 6.5, 9], look: [0, 2.2, -1] }, // transformation divide
{ p: 0.72, pos: [13, 3.4, 8.5], look: [8, 1.0, 0] }, // road-level, optimized right
{ p: 1.0, pos: [0, 15, 12], look: [0, 0.2, 4] }, // top-down logistics overview
];
function sampleKeys(e: number) {
let a = KEYS[0], b = KEYS[KEYS.length - 1];
for (let i = 0; i < KEYS.length - 1; i++) {
if (e >= KEYS[i].p && e <= KEYS[i + 1].p) { a = KEYS[i]; b = KEYS[i + 1]; break; }
}
const k = smoothstep(0, 1, (e - a.p) / ((b.p - a.p) || 1));
return {
pos: [lerp(a.pos[0], b.pos[0], k), lerp(a.pos[1], b.pos[1], k), lerp(a.pos[2], b.pos[2], k)] as const,
look: [lerp(a.look[0], b.look[0], k), lerp(a.look[1], b.look[1], k), lerp(a.look[2], b.look[2], k)] as const,
};
}
function CameraRig({ progress }: { progress: React.RefObject<number> }) {
const eased = useRef(0);
const look = useRef(new THREE.Vector3(0, 1.5, 4));
useFrame((state, dt) => {
eased.current = damp(eased.current, clamp01(progress.current ?? 0), 2.4, dt);
const { pos, look: lk } = sampleKeys(eased.current);
const t = state.clock.elapsedTime;
state.camera.position.set(pos[0] + Math.sin(t * 0.25) * 0.25, pos[1] + Math.sin(t * 0.4) * 0.12, pos[2]);
look.current.lerp(new THREE.Vector3(lk[0], lk[1], lk[2]), 0.12);
state.camera.lookAt(look.current);
});
return null;
}
/* ───────────── Roads (flat asphalt ribbons on the ground) ───────────── */
const _p = new THREE.Vector3(), _t = new THREE.Vector3();
function roadRibbon(curve: THREE.CatmullRomCurve3, width: number, segs = 130) {
const half = width / 2;
const pos: number[] = [];
const idx: number[] = [];
for (let i = 0; i <= segs; i++) {
const u = (i / segs) % 1;
curve.getPointAt(u, _p);
curve.getTangentAt(u, _t);
const nx = -_t.z, nz = _t.x;
const len = Math.hypot(nx, nz) || 1;
pos.push(_p.x + (nx / len) * half, ROUTE_Y, _p.z + (nz / len) * half);
pos.push(_p.x - (nx / len) * half, ROUTE_Y, _p.z - (nz / len) * half);
}
for (let i = 0; i < segs; i++) {
const a = i * 2, b = i * 2 + 1, c = i * 2 + 2, d = i * 2 + 3;
idx.push(a, b, c, b, d, c);
}
const g = new THREE.BufferGeometry();
g.setAttribute("position", new THREE.Float32BufferAttribute(pos, 3));
g.setIndex(idx);
g.computeVertexNormals();
return g;
}
function Roads({ routes, edge, width }: { routes: { curve: THREE.CatmullRomCurve3 }[]; edge: string; width: number }) {
const geos = useMemo(() => routes.map((r) => roadRibbon(r.curve, width)), [routes, width]);
return (
<group>
{geos.map((g, i) => (
<mesh key={i} geometry={g}>
<meshStandardMaterial color={C.road} emissive={edge} emissiveIntensity={0.14} roughness={0.85} metalness={0.1} />
</mesh>
))}
</group>
);
}
/* Flowing fleet/data pulses riding the roads. */
const PULSE_TMP = new THREE.Vector3();
function RoadPulses({ routes, color, per = 3 }: { routes: { curve: THREE.CatmullRomCurve3 }[]; color: string; per?: number }) {
const count = routes.length * per;
const refs = useRef<(THREE.Mesh | null)[]>([]);
const offs = useMemo(() => Array.from({ length: count }, (_, i) => seeded(i * 7 + 3)), [count]);
useFrame((state) => {
const t = state.clock.elapsedTime;
for (let i = 0; i < count; i++) {
const m = refs.current[i];
if (!m) continue;
const ri = Math.floor(i / per);
const u = (offs[i] + t * (0.04 + seeded(i) * 0.03)) % 1;
routes[ri].curve.getPointAt(u, PULSE_TMP);
m.position.set(PULSE_TMP.x, ROUTE_Y + 0.12, PULSE_TMP.z);
(m.material as THREE.MeshBasicMaterial).opacity = 0.6 + Math.sin(t * 6 + i) * 0.35;
}
});
return (
<group>
{Array.from({ length: count }, (_, i) => (
<mesh key={i} ref={(el) => { refs.current[i] = el; }}>
<sphereGeometry args={[0.08, 8, 8]} />
<meshBasicMaterial color={color} transparent depthWrite={false} blending={THREE.AdditiveBlending} toneMapped={false} />
</mesh>
))}
</group>
);
}
/* ───────────── City buildings ───────────── */
type Bld = { x: number; z: number; w: number; d: number; h: number; side: number; tone: number; accent: boolean };
function Buildings() {
const blds = useMemo<Bld[]>(() => {
const arr: Bld[] = [];
for (let gx = -15; gx <= 15; gx += 2.6) {
for (let gz = -12.5; gz <= 12.5; gz += 2.6) {
if (Math.abs(gx) < 1.8) continue; // keep gateway band clear
const center = gx < 0 ? LEFT_C : RIGHT_C;
const dCenter = Math.hypot(gx - center.x, gz - center.z);
if (dCenter < 6.4) continue; // keep road area clear
if (gz > 7.2 && Math.abs(gx) < 6.5) continue; // keep KPI tower area clear
if (seeded(gx * 7.3 + gz * 1.7 + 50) < 0.28) continue; // streets / gaps
const jx = (seeded(gx + gz) - 0.5) * 0.6;
const jz = (seeded(gx * 2 + gz) - 0.5) * 0.6;
arr.push({
x: gx + jx, z: gz + jz,
w: 1.2 + seeded(gx * 1.1 + gz) * 0.8,
d: 1.2 + seeded(gx + gz * 1.3) * 0.8,
h: 1.1 + seeded(gx * 3 + gz * 5) * 3.6,
side: gx < 0 ? -1 : 1,
tone: seeded(gx * 9 + gz * 4),
accent: seeded(gx * 4 + gz * 11) > 0.62,
});
}
}
return arr;
}, []);
return (
<group>
{blds.map((b, i) => {
const bodyColor = b.tone < 0.4 ? C.bldA : b.tone < 0.75 ? C.bldB : (b.side > 0 ? C.white : C.bldC);
const accent = b.side < 0 ? (b.tone > 0.5 ? C.orange : C.red) : C.green;
return (
<group key={i} position={[b.x, 0, b.z]}>
<mesh position={[0, b.h / 2, 0]}>
<boxGeometry args={[b.w, b.h, b.d]} />
<meshStandardMaterial color={bodyColor} roughness={0.8} metalness={0.15} />
</mesh>
{b.accent && (
<mesh position={[0, b.h - 0.18, b.d / 2 + 0.001]}>
<planeGeometry args={[b.w * 0.82, 0.12]} />
<meshBasicMaterial color={accent} toneMapped={false} />
</mesh>
)}
</group>
);
})}
</group>
);
}
/* ───────────── Warehouse / dispatch depot ───────────── */
function Warehouse({ x, z, accent }: { x: number; z: number; accent: string }) {
return (
<group position={[x, 0, z]}>
<mesh position={[0, 0.9, 0]}>
<boxGeometry args={[4.2, 1.8, 2.6]} />
<meshStandardMaterial color={C.bldB} roughness={0.8} metalness={0.2} />
</mesh>
{/* sloped roof slab */}
<mesh position={[0, 1.85, 0]}>
<boxGeometry args={[4.3, 0.12, 2.7]} />
<meshStandardMaterial color={C.steel} roughness={0.6} metalness={0.4} />
</mesh>
{/* roller doors */}
{[-1.3, 0, 1.3].map((o, i) => (
<mesh key={i} position={[o, 0.55, 1.31]}>
<boxGeometry args={[0.9, 1.0, 0.05]} />
<meshStandardMaterial color={C.bldA} emissive={accent} emissiveIntensity={0.25} roughness={0.7} />
</mesh>
))}
</group>
);
}
/* ───────────── Ground delivery zones + congestion heat ───────────── */
function GroundMarks() {
const heat = useRef<(THREE.Mesh | null)[]>([]);
useFrame((state) => {
const t = state.clock.elapsedTime;
heat.current.forEach((m, i) => {
if (m) (m.material as THREE.MeshBasicMaterial).opacity = 0.18 + Math.sin(t * 2 + i) * 0.1;
});
});
const leftZones = useMemo(() => Array.from({ length: 4 }, (_, i) => {
const a = (i / 4) * Math.PI * 2 + 0.5;
return [LEFT_C.x + Math.cos(a) * 4.4, LEFT_C.z + Math.sin(a) * 4.4] as [number, number];
}), []);
const rightZones = useMemo(() => Array.from({ length: 4 }, (_, i) => {
const a = (i / 4) * Math.PI * 2 + 0.5;
return [RIGHT_C.x + Math.cos(a) * 4.4, RIGHT_C.z + Math.sin(a) * 4.4] as [number, number];
}), []);
return (
<group>
{/* left congestion heat patches (red/orange) */}
{leftZones.map(([x, z], i) => (
<mesh key={`h${i}`} ref={(el) => { heat.current[i] = el; }} rotation={[-Math.PI / 2, 0, 0]} position={[x, 0.04, z]}>
<circleGeometry args={[1.7, 32]} />
<meshBasicMaterial color={i % 2 ? C.orange : C.red} transparent opacity={0.2} depthWrite={false} blending={THREE.AdditiveBlending} toneMapped={false} />
</mesh>
))}
{/* right optimized coverage zones (green rings) */}
{rightZones.map(([x, z], i) => (
<mesh key={`z${i}`} rotation={[-Math.PI / 2, 0, 0]} position={[x, 0.04, z]}>
<ringGeometry args={[1.3, 1.55, 36]} />
<meshBasicMaterial color={C.green} transparent opacity={0.5} depthWrite={false} blending={THREE.AdditiveBlending} toneMapped={false} />
</mesh>
))}
</group>
);
}
/* ───────────── Central transformation divider (NOT an AI engine) ─────────────
A clean glowing seam between the two worlds + a light "optimization wave" that
sweeps left→right, literally showing the chaotic side being transformed. */
function TransformDivider() {
const sweep = useRef<THREE.Mesh>(null);
const seam = useRef<THREE.MeshBasicMaterial>(null);
useFrame((state) => {
const t = state.clock.elapsedTime;
if (sweep.current) {
const u = (t * 0.16) % 1;
sweep.current.position.x = lerp(-6, 6, u);
(sweep.current.material as THREE.MeshBasicMaterial).opacity = Math.sin(u * Math.PI) * 0.3;
}
if (seam.current) seam.current.opacity = 0.4 + Math.sin(t * 2) * 0.12;
});
return (
<group position={[GATEWAY.x, 0, GATEWAY.z]}>
{/* ground seam marking before | after */}
<mesh rotation={[-Math.PI / 2, 0, 0]} position={[0, 0.06, 0]}>
<planeGeometry args={[0.2, 26]} />
<meshBasicMaterial ref={seam} color={C.green2} transparent opacity={0.4} depthWrite={false} blending={THREE.AdditiveBlending} toneMapped={false} />
</mesh>
{/* vertical seam light */}
<mesh position={[0, 2.2, 0]}>
<boxGeometry args={[0.06, 4.4, 0.06]} />
<meshBasicMaterial color={C.white} transparent opacity={0.5} blending={THREE.AdditiveBlending} toneMapped={false} />
</mesh>
{/* sweeping optimization wave crossing the city */}
<mesh ref={sweep} rotation={[0, Math.PI / 2, 0]} position={[0, 2, 0]}>
<planeGeometry args={[22, 4.2]} />
<meshBasicMaterial color={C.green2} transparent opacity={0.2} side={THREE.DoubleSide} depthWrite={false} blending={THREE.AdditiveBlending} toneMapped={false} />
</mesh>
</group>
);
}
/* Big number that counts up once on mount (drei Html children are normal DOM). */
function CountUp({ value, decimals = 0, suffix = "" }: { value: number; decimals?: number; suffix?: string }) {
const [n, setN] = React.useState(0);
React.useEffect(() => {
let raf = 0;
let start: number | null = null;
const dur = 1700;
const tick = (now: number) => {
if (start === null) start = now;
const k = Math.min(1, (now - start) / dur);
setN(value * (1 - Math.pow(1 - k, 3)));
if (k < 1) raf = requestAnimationFrame(tick);
};
raf = requestAnimationFrame(tick);
return () => cancelAnimationFrame(raf);
}, [value]);
return <>{n.toFixed(decimals)}{suffix}</>;
}
/* ───────────── KPI performance towers (grow with scroll) + BIG counting numbers ───────────── */
type Kpi = { name: string; value: number; decimals?: number; suffix: string; h: number; x: number };
const KPIS: Kpi[] = [
{ name: "Faster Deliveries", value: 32, suffix: "%", h: 5.4, x: -5.1 },
{ name: "Lower Op. Cost", value: 18, suffix: "%", h: 4.4, x: -1.7 },
{ name: "SLA Success", value: 99.2, decimals: 1, suffix: "%", h: 5.7, x: 1.7 },
{ name: "Less Fuel Used", value: 24, suffix: "%", h: 4.0, x: 5.1 },
];
function KpiTowers({ progress }: { progress: React.RefObject<number> }) {
const refs = useRef<(THREE.Group | null)[]>([]);
const Z = 12.5;
useFrame(() => {
const p = clamp01(progress.current ?? 0);
KPIS.forEach((_, i) => {
const g = refs.current[i];
if (!g) return;
const k = smoothstep(0.12 + i * 0.07, 0.55 + i * 0.07, p);
g.scale.y = 0.02 + k * 0.98;
});
});
return (
<group>
{KPIS.map((kpi, i) => (
<group key={kpi.name} position={[kpi.x, 0, Z]}>
<mesh position={[0, 0.03, 0]} rotation={[-Math.PI / 2, 0, 0]}>
<circleGeometry args={[0.9, 28]} />
<meshStandardMaterial color={C.steel} roughness={0.7} />
</mesh>
<group ref={(el) => { refs.current[i] = el; }}>
<mesh position={[0, kpi.h / 2, 0]}>
<boxGeometry args={[0.85, kpi.h, 0.85]} />
<meshStandardMaterial color={C.green} emissive={C.green} emissiveIntensity={0.22} roughness={0.45} metalness={0.3} />
</mesh>
<mesh position={[0, kpi.h + 0.05, 0]}>
<boxGeometry args={[1.06, 0.12, 1.06]} />
<meshBasicMaterial color={C.green2} toneMapped={false} />
</mesh>
</group>
{/* BIG, immediately-readable counting readout integrated on the tower */}
<Html position={[0, kpi.h + 1.05, 0]} center distanceFactor={20} pointerEvents="none">
<div style={{
transform: "translateY(-50%)", textAlign: "center", whiteSpace: "nowrap",
fontFamily: "var(--font-space-grotesk), system-ui, sans-serif", userSelect: "none",
}}>
<div style={{ fontSize: 48, fontWeight: 900, color: "#bbf7d0", textShadow: "0 0 18px rgba(34,197,94,0.85)", lineHeight: 1 }}>
<CountUp value={kpi.value} decimals={kpi.decimals} suffix={kpi.suffix} />
</div>
<div style={{ fontSize: 13, letterSpacing: "0.16em", textTransform: "uppercase", color: "rgba(226,232,240,0.92)", marginTop: 6, fontWeight: 600 }}>{kpi.name}</div>
</div>
</Html>
</group>
))}
</group>
);
}
/* ───────────── Fleet (spline-locked, drives on the roads) ───────────── */
const TYPES: VType[] = ["truck", "van", "auto", "bike"];
function Fleets() {
const { chaotic, optimized } = useMemo(() => buildPerfRoutes(), []);
const badBodies = [C.steel, "#7f1d1d", "#9a3412"];
const goodBodies = [C.white, C.bldC, "#166534"];
return (
<group>
{chaotic.map((r, i) => (
<SplineRider key={`bad-${i}`} curve={r.curve} speed={0.01 + seeded(i * 9 + 1) * 0.008} offset={seeded(i * 9 + 2)}
type={TYPES[i % TYPES.length]} body={badBodies[i % badBodies.length]} accent={i % 2 === 0 ? C.red : C.orange} />
))}
{optimized.map((r, i) => (
<SplineRider key={`good-${i}`} curve={r.curve} speed={0.038 + seeded(i * 11 + 1) * 0.02} offset={seeded(i * 11 + 2)}
type={TYPES[i % TYPES.length]} body={goodBodies[i % goodBodies.length]} accent={C.green} />
))}
</group>
);
}
/* ───────────── Scene ───────────── */
function Scene({ progress, reduced, isMobile }: { progress: React.RefObject<number>; reduced: boolean; isMobile: boolean }) {
const { chaotic, optimized } = useMemo(() => buildPerfRoutes(), []);
return (
<>
<color attach="background" args={["#1b2028"]} />
<fog attach="fog" args={["#1b2028", 40, 100]} />
<ambientLight intensity={0.8} />
<directionalLight position={[10, 20, 10]} intensity={2.2} />
<hemisphereLight args={["#d7dee8", "#15171c", 0.7]} />
{/* ground + faint street grid */}
<mesh rotation={[-Math.PI / 2, 0, 0]} position={[0, 0, 0]}>
<planeGeometry args={[70, 48]} />
<meshStandardMaterial color={C.ground} roughness={0.95} metalness={0.05} />
</mesh>
<gridHelper args={[70, 56, "#3a4049", "#2a2f37"]} position={[0, 0.012, 0]} />
{/* bold per-side atmosphere wash — instant "chaos vs efficiency" read */}
<mesh rotation={[-Math.PI / 2, 0, 0]} position={[LEFT_C.x, 0.02, 0]}>
<circleGeometry args={[13, 48]} />
<meshBasicMaterial color={C.red} transparent opacity={0.07} depthWrite={false} blending={THREE.AdditiveBlending} toneMapped={false} />
</mesh>
<mesh rotation={[-Math.PI / 2, 0, 0]} position={[RIGHT_C.x, 0.02, 0]}>
<circleGeometry args={[13, 48]} />
<meshBasicMaterial color={C.green} transparent opacity={0.08} depthWrite={false} blending={THREE.AdditiveBlending} toneMapped={false} />
</mesh>
<Roads routes={chaotic} edge={C.red} width={0.85} />
<Roads routes={optimized} edge={C.green} width={0.85} />
{!isMobile && <RoadPulses routes={chaotic} color={C.orange} />}
{!isMobile && <RoadPulses routes={optimized} color={C.green2} />}
<GroundMarks />
<Buildings />
<Warehouse x={LEFT_C.x} z={-9.5} accent={C.red} />
<Warehouse x={RIGHT_C.x} z={-9.5} accent={C.green} />
<Fleets />
<TransformDivider />
<KpiTowers progress={progress} />
{!reduced && (
<EffectComposer multisampling={isMobile ? 0 : 2}>
{/* light bloom — only bright emissive (lights, pulses, gateway, tower caps) glow */}
<Bloom mipmapBlur intensity={isMobile ? 0.45 : 0.6} luminanceThreshold={0.55} luminanceSmoothing={0.06} radius={0.6} kernelSize={KernelSize.MEDIUM} />
<Vignette eskil={false} offset={0.3} darkness={0.5} />
</EffectComposer>
)}
</>
);
}
function PerformanceCanvas({ progress, reduced = false, isMobile = false, active = true }: Props) {
return (
<Canvas
dpr={[1, isMobile || reduced ? 1.3 : 1.6]}
camera={{ position: [3, 16, 27], fov: 50, near: 0.1, far: 160 }}
gl={{ antialias: !isMobile, powerPreference: "high-performance", alpha: false }}
frameloop={active ? "always" : "never"}
>
<CameraRig progress={progress} />
<Scene progress={progress} reduced={reduced} isMobile={isMobile} />
</Canvas>
);
}
export default React.memo(PerformanceCanvas);

View File

@@ -0,0 +1,189 @@
"use client";
import React, { useEffect, useRef, useState } from "react";
import dynamic from "next/dynamic";
import { motion, useMotionValue, useTransform } from "framer-motion";
import gsap from "gsap";
import { ScrollTrigger } from "gsap/ScrollTrigger";
const PerformanceCanvas = dynamic(() => import("./PerformanceCanvas"), { ssr: false });
export default function PerformanceSection() {
const containerRef = useRef<HTMLDivElement>(null);
const progressRef = useRef(0);
const scroll = useMotionValue(0);
const [pinState, setPinState] = useState<"before" | "pinned" | "after">("before");
const [mountScene, setMountScene] = useState(false);
const [sceneActive, setSceneActive] = useState(false);
const [isMobile, setIsMobile] = useState(false);
const [reduced, setReduced] = useState(false);
useEffect(() => {
const mqMobile = window.matchMedia("(max-width: 767px)");
const mqReduce = window.matchMedia("(prefers-reduced-motion: reduce)");
const sync = () => { setIsMobile(mqMobile.matches); setReduced(mqReduce.matches); };
sync();
mqMobile.addEventListener("change", sync);
mqReduce.addEventListener("change", sync);
return () => { mqMobile.removeEventListener("change", sync); mqReduce.removeEventListener("change", sync); };
}, []);
useEffect(() => {
const el = containerRef.current;
if (!el) return;
const mountIo = new IntersectionObserver(
(entries) => {
if (entries.some((e) => e.isIntersecting)) {
setMountScene(true);
setSceneActive(true);
mountIo.disconnect();
}
},
{ rootMargin: "120% 0px" },
);
const activeIo = new IntersectionObserver(
(entries) => setSceneActive(entries.some((e) => e.isIntersecting)),
{ rootMargin: "10% 0px" },
);
mountIo.observe(el);
activeIo.observe(el);
return () => { mountIo.disconnect(); activeIo.disconnect(); };
}, []);
useEffect(() => {
const el = containerRef.current;
if (!el) return;
gsap.registerPlugin(ScrollTrigger);
let lastPin: "before" | "pinned" | "after" = "before";
const st = ScrollTrigger.create({
trigger: el,
start: "top top",
end: "bottom bottom",
scrub: 0.4,
invalidateOnRefresh: true,
onUpdate: (self) => {
const p = self.progress;
progressRef.current = p;
scroll.set(p);
const ns = p <= 0.0002 ? "before" : p >= 0.9998 ? "after" : "pinned";
if (ns !== lastPin) { lastPin = ns; setPinState(ns); }
},
});
const refresh = setTimeout(() => ScrollTrigger.refresh(), 300);
return () => { clearTimeout(refresh); st.kill(); };
}, [scroll]);
const beforeOpacity = useTransform(scroll, [0.1, 0.3, 0.46], [0.4, 1, 0.32]);
const afterOpacity = useTransform(scroll, [0.6, 0.74, 0.95], [0.32, 1, 0.7]);
const stageA = useTransform(scroll, [0, 0.4], [1, 0]);
const stageB = useTransform(scroll, [0.4, 0.55, 0.65], [0, 1, 0]);
const stageC = useTransform(scroll, [0.65, 0.85], [0, 1]);
return (
<section ref={containerRef} className={`dm-perf is-${pinState}`} aria-label="Results & Impact — Logistics Performance">
<div className="dm-perf-sticky">
<div className="dm-perf-card">
<div className="dm-perf-backdrop" aria-hidden />
{mountScene && (
<div className="dm-perf-canvas">
<PerformanceCanvas progress={progressRef} reduced={reduced} isMobile={isMobile} active={sceneActive} />
</div>
)}
<div className="dm-perf-vignette" aria-hidden />
<div className="dm-perf-ui">
<header className="dm-perf-head">
<motion.div className="dm-perf-eyebrow" initial={{ opacity: 0, y: 14 }} whileInView={{ opacity: 1, y: 0 }} viewport={{ once: true }} transition={{ duration: 0.6 }}>
<span className="dm-perf-dot" /> Results &amp; Impact
</motion.div>
<motion.h2 initial={{ opacity: 0, y: 18 }} whileInView={{ opacity: 1, y: 0 }} viewport={{ once: true }} transition={{ duration: 0.7, delay: 0.05 }}>
What MileTruth Delivers
</motion.h2>
<motion.p initial={{ opacity: 0, y: 18 }} whileInView={{ opacity: 1, y: 0 }} viewport={{ once: true }} transition={{ duration: 0.7, delay: 0.12 }}>
From congested traditional dispatch to a lean optimized fleet the measurable business results across a live delivery city.
</motion.p>
<div className="dm-perf-status">
<motion.span className="dm-perf-status__item" style={{ opacity: stageA }}><span className="dm-perf-status__dot dm-perf-status__dot--red" /> Traditional dispatch</motion.span>
<motion.span className="dm-perf-status__item" style={{ opacity: stageB }}><span className="dm-perf-status__dot dm-perf-status__dot--amber" /> Transformation gateway</motion.span>
<motion.span className="dm-perf-status__item" style={{ opacity: stageC }}><span className="dm-perf-status__dot dm-perf-status__dot--green" /> Optimized network</motion.span>
</div>
</header>
<motion.div className="dm-perf-label dm-perf-label--before" style={{ opacity: beforeOpacity }}>
<span className="dm-perf-label__tag">Traditional Dispatch</span>
<span className="dm-perf-label__sub">Congestion · long routes · fuel waste · delays</span>
</motion.div>
<motion.div className="dm-perf-label dm-perf-label--after" style={{ opacity: afterOpacity }}>
<span className="dm-perf-label__tag dm-perf-label__tag--good">MileTruth Optimized</span>
<span className="dm-perf-label__sub">Clean corridors · organized fleet · faster coverage</span>
</motion.div>
</div>
</div>
</div>
<style>{styles}</style>
</section>
);
}
const styles = `
.dm-perf { position: relative; height: 250vh; background: transparent; margin-bottom: 120px; }
.dm-perf-sticky { position: absolute; top: 0; left: 0; width: 100%; height: 100vh; overflow: hidden; }
.dm-perf.is-pinned .dm-perf-sticky { position: fixed; top: 0; left: 0; }
.dm-perf.is-after .dm-perf-sticky { position: absolute; top: auto; bottom: 0; }
.dm-perf-card {
position: absolute !important; top: 110px !important; left: 40px !important; right: 40px !important; bottom: 24px !important;
border-radius: 60px !important; overflow: hidden !important;
background: linear-gradient(168deg, #1b1f26 0%, #15181d 45%, #101216 100%) !important;
border: 1.5px solid rgba(255,255,255,0.08) !important;
box-shadow: 0 4px 30px -4px rgba(0,0,0,0.7), 0 20px 80px -20px rgba(0,0,0,0.6), inset 0 1px 0 rgba(255,255,255,0.05) !important;
box-sizing: border-box !important;
}
@media (max-width: 1024px) { .dm-perf-card { top: 96px !important; left: 20px !important; right: 20px !important; bottom: 16px !important; border-radius: 42px !important; } }
@media (max-width: 767px) { .dm-perf-card { top: 86px !important; left: 10px !important; right: 10px !important; bottom: 10px !important; border-radius: 28px !important; } }
.dm-perf-backdrop { position: absolute; inset: 0; z-index: 0;
background: radial-gradient(55% 50% at 20% 60%, rgba(239,68,68,0.07) 0%, transparent 60%),
radial-gradient(55% 50% at 80% 60%, rgba(34,197,94,0.08) 0%, transparent 60%); }
.dm-perf-canvas { position: absolute; inset: 0; z-index: 1; }
.dm-perf-canvas canvas { display: block; }
.dm-perf-vignette { position: absolute; inset: 0; z-index: 2; pointer-events: none;
background: radial-gradient(125% 105% at 50% 46%, transparent 56%, rgba(8,9,12,0.86) 100%),
linear-gradient(180deg, rgba(8,9,12,0.5) 0%, transparent 20%, transparent 66%, rgba(8,9,12,0.9) 100%); }
.dm-perf-ui { position: absolute; inset: 0; z-index: 4; pointer-events: none;
font-family: var(--font-space-grotesk), var(--font-manrope), system-ui, sans-serif; }
.dm-perf-head { position: absolute; top: clamp(18px, 3.4vh, 40px); left: 50%; transform: translateX(-50%); width: min(700px, 92vw); text-align: center; }
.dm-perf-eyebrow { display: inline-flex; align-items: center; gap: 7px; font-size: 11px; letter-spacing: 0.24em; text-transform: uppercase; color: #4ade80;
padding: 5px 14px; border-radius: 999px; background: rgba(34,197,94,0.08); border: 1px solid rgba(34,197,94,0.28); backdrop-filter: blur(8px); }
.dm-perf-dot { width: 6px; height: 6px; border-radius: 50%; background: #22c55e; box-shadow: 0 0 10px #22c55e; }
.dm-perf .dm-perf-head h2 { margin: 10px 0 6px !important; padding: 0 !important; color: #F8FAFC !important; font-weight: 700 !important; text-transform: none !important;
font-size: clamp(22px, 2.6vw, 38px) !important; line-height: 1.08 !important; letter-spacing: -0.015em !important; }
.dm-perf .dm-perf-head p { margin: 0 auto !important; padding: 0 !important; color: rgba(226,232,240,0.66) !important; max-width: 500px; font-size: clamp(11px, 1vw, 13.5px) !important; line-height: 1.45 !important; }
.dm-perf-status { display: inline-flex; align-items: center; gap: 16px; margin-top: 12px; min-height: 18px; }
.dm-perf-status__item { position: relative; display: inline-flex; align-items: center; gap: 7px; font-size: 10.5px; letter-spacing: 0.14em; text-transform: uppercase; color: #E2E8F0; font-weight: 600; }
.dm-perf-status__item:not(:first-child) { position: absolute; left: 50%; transform: translateX(-50%); white-space: nowrap; }
.dm-perf-status__dot { width: 6px; height: 6px; border-radius: 50%; }
.dm-perf-status__dot--red { background: #ef4444; box-shadow: 0 0 10px #ef4444; }
.dm-perf-status__dot--amber { background: #fbbf24; box-shadow: 0 0 10px #fbbf24; }
.dm-perf-status__dot--green { background: #22c55e; box-shadow: 0 0 10px #22c55e; }
.dm-perf-label { position: absolute; top: 44%; transform: translateY(-50%); display: flex; flex-direction: column; gap: 4px; }
.dm-perf-label--before { left: clamp(16px, 4vw, 60px); text-align: left; }
.dm-perf-label--after { right: clamp(16px, 4vw, 60px); text-align: right; align-items: flex-end; }
.dm-perf-label__tag { font-size: clamp(17px, 2vw, 28px); font-weight: 800; letter-spacing: -0.02em; color: #f87171; text-shadow: 0 0 22px rgba(239,68,68,0.45); }
.dm-perf-label__tag--good { color: #4ade80; text-shadow: 0 0 22px rgba(34,197,94,0.5); }
.dm-perf-label__sub { font-size: 10.5px; letter-spacing: 0.05em; color: rgba(226,232,240,0.6); max-width: 180px; }
@media (max-width: 767px) {
.dm-perf { height: 220vh; }
.dm-perf-label__sub { display: none; }
.dm-perf-label__tag { font-size: 15px; }
.dm-perf-head h2 { font-size: 22px; }
.dm-perf-status { gap: 10px; }
}
`;

View File

@@ -0,0 +1,183 @@
"use client";
import React, { useRef } from "react";
import { useFrame } from "@react-three/fiber";
import * as THREE from "three";
export type VType = "bike" | "auto" | "van" | "truck";
const TMP_POS = new THREE.Vector3();
const TMP_TAN = new THREE.Vector3();
/* ── Low-poly procedural vehicle bodies (modelled facing +Z) ──────────────
Body uses a lit standard material; accent strips + lights are emissive so
they pick up bloom. Wheels are a shared group that spins. */
function Wheels({ positions, radius = 0.12, spinRef }: {
positions: [number, number, number][];
radius?: number;
spinRef: React.RefObject<THREE.Group | null>;
}) {
return (
<group ref={spinRef}>
{positions.map((p, i) => (
<mesh key={i} position={p} rotation={[0, 0, Math.PI / 2]}>
<cylinderGeometry args={[radius, radius, 0.08, 12]} />
<meshStandardMaterial color="#0b1220" metalness={0.6} roughness={0.4} />
</mesh>
))}
</group>
);
}
function VehicleBody({ type, body, accent }: { type: VType; body: string; accent: string }) {
const wheels = useRef<THREE.Group>(null);
// spin the wheels for a sense of motion
useFrame((_, dt) => {
if (wheels.current) wheels.current.rotation.x += dt * 9;
});
const std = (color: string, e = 0.15) => (
<meshStandardMaterial color={color} metalness={0.45} roughness={0.45} emissive={color} emissiveIntensity={e} />
);
const glow = (color: string) => (
<meshBasicMaterial color={color} toneMapped={false} />
);
if (type === "bike") {
return (
<group>
<mesh position={[0, 0.26, 0]}>
<boxGeometry args={[0.14, 0.16, 0.5]} />
{std(body, 0.2)}
</mesh>
<mesh position={[0, 0.42, -0.02]}>
<sphereGeometry args={[0.1, 12, 12]} />
{std("#1e293b", 0.1)}
</mesh>
<mesh position={[0, 0.3, 0.34]}>
<boxGeometry args={[0.04, 0.06, 0.12]} />
{glow(accent)}
</mesh>
<mesh position={[0, 0.24, 0.3]}>
<sphereGeometry args={[0.05, 8, 8]} />
{glow("#ffffff")}
</mesh>
<Wheels spinRef={wheels} radius={0.13} positions={[[0, 0.13, 0.28], [0, 0.13, -0.28]]} />
</group>
);
}
if (type === "auto") {
// three-wheeled auto-rickshaw
return (
<group>
<mesh position={[0, 0.34, -0.05]}>
<boxGeometry args={[0.42, 0.4, 0.6]} />
{std(body, 0.18)}
</mesh>
<mesh position={[0, 0.5, 0.05]}>
<boxGeometry args={[0.4, 0.18, 0.5]} />
{std("#0b1220", 0.05)}
</mesh>
<mesh position={[0, 0.3, 0.3]}>
<boxGeometry args={[0.36, 0.32, 0.12]} />
{glow(accent)}
</mesh>
<mesh position={[0.12, 0.22, 0.36]}><sphereGeometry args={[0.045, 8, 8]} />{glow("#ffffff")}</mesh>
<mesh position={[-0.12, 0.22, 0.36]}><sphereGeometry args={[0.045, 8, 8]} />{glow("#ffffff")}</mesh>
<Wheels spinRef={wheels} radius={0.12} positions={[[0, 0.12, 0.28], [0.2, 0.12, -0.28], [-0.2, 0.12, -0.28]]} />
</group>
);
}
if (type === "van") {
return (
<group>
<mesh position={[0, 0.4, -0.05]}>
<boxGeometry args={[0.52, 0.56, 1.0]} />
{std(body, 0.16)}
</mesh>
<mesh position={[0, 0.34, 0.5]}>
<boxGeometry args={[0.5, 0.44, 0.18]} />
{std("#1e293b", 0.05)}
</mesh>
<mesh position={[0, 0.42, -0.04]}>
<boxGeometry args={[0.53, 0.06, 0.8]} />
{glow(accent)}
</mesh>
<mesh position={[0.18, 0.24, 0.56]}><sphereGeometry args={[0.05, 8, 8]} />{glow("#ffffff")}</mesh>
<mesh position={[-0.18, 0.24, 0.56]}><sphereGeometry args={[0.05, 8, 8]} />{glow("#ffffff")}</mesh>
<Wheels spinRef={wheels} radius={0.14} positions={[[0.22, 0.14, 0.42], [-0.22, 0.14, 0.42], [0.22, 0.14, -0.42], [-0.22, 0.14, -0.42]]} />
</group>
);
}
// truck — cab + long box trailer
return (
<group>
<mesh position={[0, 0.36, 0.62]}>
<boxGeometry args={[0.56, 0.5, 0.5]} />
{std(body, 0.18)}
</mesh>
<mesh position={[0, 0.5, -0.2]}>
<boxGeometry args={[0.6, 0.78, 1.3]} />
{std("#cbd5e1", 0.08)}
</mesh>
<mesh position={[0, 0.5, -0.2]}>
<boxGeometry args={[0.61, 0.12, 1.31]} />
{glow(accent)}
</mesh>
<mesh position={[0.2, 0.22, 0.88]}><sphereGeometry args={[0.06, 8, 8]} />{glow("#ffffff")}</mesh>
<mesh position={[-0.2, 0.22, 0.88]}><sphereGeometry args={[0.06, 8, 8]} />{glow("#ffffff")}</mesh>
<Wheels spinRef={wheels} radius={0.15} positions={[
[0.26, 0.15, 0.7], [-0.26, 0.15, 0.7],
[0.26, 0.15, -0.2], [-0.26, 0.15, -0.2],
[0.26, 0.15, -0.6], [-0.26, 0.15, -0.6],
]} />
</group>
);
}
export const VehicleBodyMemo = React.memo(VehicleBody);
/**
* Locks a vehicle perfectly to a spline. Position = curve.getPointAt(u),
* heading = curve.getTangentAt(u). No drifting, no off-route movement.
*/
export function SplineRider({
curve,
speed,
offset,
type,
body,
accent,
}: {
curve: THREE.CatmullRomCurve3;
speed: number;
offset: number;
type: VType;
body: string;
accent: string;
}) {
const ref = useRef<THREE.Group>(null);
const u = useRef(offset);
useFrame((_, dt) => {
const g = ref.current;
if (!g) return;
u.current = (u.current + dt * speed) % 1;
curve.getPointAt(u.current, TMP_POS);
curve.getTangentAt(u.current, TMP_TAN);
g.position.copy(TMP_POS);
g.rotation.y = Math.atan2(TMP_TAN.x, TMP_TAN.z);
// gentle pitch so it follows slope without leaving the path
g.rotation.x = -Math.asin(THREE.MathUtils.clamp(TMP_TAN.y, -0.6, 0.6)) * 0.5;
});
return (
<group ref={ref}>
<VehicleBodyMemo type={type} body={body} accent={accent} />
</group>
);
}

View File

@@ -0,0 +1,69 @@
import * as THREE from "three";
import { seeded } from "../optimization/math";
/**
* Ground-level ROAD network for the Performance "Results & Impact" city.
* - LEFT district (x < 0): traditional dispatch — tangled, overlapping roads.
* - RIGHT district (x > 0): MileTruth optimized — clean, organized corridors.
* All curves are CLOSED and flat on the ground so fleet vehicles drive on the
* roads continuously (spline-locked, no end-of-curve teleport).
*/
export const ROUTE_Y = 0.05; // roads sit on the ground
export const LEFT_C = new THREE.Vector3(-8, 0, 0);
export const RIGHT_C = new THREE.Vector3(8, 0, 0);
export const GATEWAY = new THREE.Vector3(0, 0, 0); // central transformation gateway
export type PerfRoute = { curve: THREE.CatmullRomCurve3 };
function closedFlat(points: THREE.Vector3[]): THREE.CatmullRomCurve3 {
const flat = points.map((p) => new THREE.Vector3(p.x, ROUTE_Y, p.z));
return new THREE.CatmullRomCurve3(flat, true, "catmullrom", 0.5);
}
let cache: { chaotic: PerfRoute[]; optimized: PerfRoute[] } | null = null;
export function buildPerfRoutes() {
if (cache) return cache;
// --- LEFT: tangled, overlapping traffic loops ----------------------------
const chaotic: PerfRoute[] = [];
for (let r = 0; r < 5; r++) {
const pts: THREE.Vector3[] = [];
const n = 6 + Math.floor(seeded(r * 13 + 1) * 3);
let ang = seeded(r * 13 + 2) * Math.PI * 2;
for (let s = 0; s < n; s++) {
ang += (seeded(r * 13 + s * 3 + 3) - 0.5) * 2.7; // erratic detours
const rad = 1.8 + seeded(r * 7 + s + 4) * 4.2;
const x = LEFT_C.x + Math.cos(ang) * rad + (seeded(r * 5 + s) - 0.5) * 2.2;
const z = LEFT_C.z + Math.sin(ang) * rad + (seeded(r * 9 + s) - 0.5) * 2.2;
pts.push(new THREE.Vector3(x, 0, z));
}
chaotic.push({ curve: closedFlat(pts) });
}
// --- RIGHT: clean organized delivery corridors, one per zone -------------
const optimized: PerfRoute[] = [];
const zones: [number, number][] = [
[0, 0],
[2.8, 2.6],
[-2.8, 2.6],
[2.8, -2.6],
[-2.8, -2.6],
];
zones.forEach(([ox, oz], r) => {
const pts: THREE.Vector3[] = [];
const n = 6;
const rad = r === 0 ? 4.8 : 1.5;
for (let s = 0; s < n; s++) {
const a = (s / n) * Math.PI * 2 + r * 0.3;
const x = RIGHT_C.x + ox + Math.cos(a) * rad;
const z = RIGHT_C.z + oz + Math.sin(a) * rad;
pts.push(new THREE.Vector3(x, 0, z));
}
optimized.push({ curve: closedFlat(pts) });
});
cache = { chaotic, optimized };
return cache;
}

View File

@@ -158,10 +158,6 @@ export default function BlogGrid() {
color: #64748b !important;
line-height: 1.6 !important;
margin: 0 0 24px 0 !important;
display: -webkit-box !important;
-webkit-line-clamp: 3 !important;
-webkit-box-orient: vertical !important;
overflow: hidden !important;
text-transform: none !important;
font-family: var(--font-manrope), sans-serif !important;
}

View File

@@ -4,6 +4,17 @@ import Link from "next/link";
export default function BlogsHero() {
return (
<>
<style dangerouslySetInnerHTML={{ __html: `
.blogs-hero-title {
color: #ffffff !important;
font-family: var(--font-manrope), sans-serif !important;
font-size: clamp(34px, 5.5vw, 68px) !important;
font-weight: 850 !important;
text-transform: uppercase !important;
letter-spacing: -1.5px !important;
margin: 0 !important;
}
`}} />
<div className="custom-standard-hero-container">
<div
style={{
@@ -18,7 +29,7 @@ export default function BlogsHero() {
{/* Title / Heading for Blogs */}
<div style={{ textAlign: "center", color: "#fff", zIndex: 5 }}>
<h1 style={{ fontFamily: "var(--font-manrope), sans-serif", fontSize: "clamp(34px, 5.5vw, 68px)", fontWeight: 850, textTransform: "uppercase", letterSpacing: "-1.5px", margin: 0 }}>
<h1 className="blogs-hero-title">
Our <span style={{ color: "#C01227" }}>Blogs</span>
</h1>
</div>

View File

@@ -74,7 +74,15 @@ export default function IndexHero() {
zIndex: activeSlide === 0 ? 2 : 1
}}
>
<div className="content-item slider-item elementor-repeater-item-3264830 slide-style-standard">
<div
className="content-item slider-item elementor-repeater-item-3264830 slide-style-standard"
style={{
backgroundImage: "url('/images/home-bg-1.png')",
backgroundPosition: "center center",
backgroundRepeat: "no-repeat",
backgroundSize: "cover"
}}
>
<div className="slide-content">
<div className="slide-content-inner">
<h1 className="content-slider-item-heading logico-content-wrapper-1">
@@ -107,7 +115,15 @@ export default function IndexHero() {
zIndex: activeSlide === 1 ? 2 : 1
}}
>
<div className="content-item slider-item elementor-repeater-item-6867061 slide-style-standard">
<div
className="content-item slider-item elementor-repeater-item-6867061 slide-style-standard"
style={{
backgroundImage: "url('/images/home-bg-1.png')",
backgroundPosition: "center center",
backgroundRepeat: "no-repeat",
backgroundSize: "cover"
}}
>
<div className="slide-content">
<div className="slide-content-inner">
<h1 className="content-slider-item-heading logico-content-wrapper-1">

View File

@@ -1,170 +1,261 @@
"use client";
import React, { useState } from "react";
import Image from "next/image";
import { motion, AnimatePresence } from "framer-motion";
export default function Workflow2() {
const [activeSlide, setActiveSlide] = useState(0);
const slides = [
{
title: "Innovation",
title: "INNOVATION",
text: "Our Parallel Universe Engine simultaneously evaluates multiple routing strategies to identify the most efficient delivery plan for every dispatch window. By simulating different route combinations in real time, the system ensures faster, smarter, and more cost-effective logistics decisions. This enables businesses to maintain high operational accuracy while adapting dynamically to changing delivery conditions."
},
{
title: "Innovation",
title: "INNOVATION",
text: "The platform solves the EV routing challenge through intelligent battery-aware simulations and advanced optimization logic powered by Google OR-Tools. It balances delivery efficiency, charging constraints, and SLA priorities to maximize fleet performance without compromising reliability. This creates a scalable and future-ready logistics system designed for both traditional and EV fleets."
},
{
title: "Innovation",
title: "INNOVATION",
text: "With sub-45ms inference latency and real-time ETA validation, the engine delivers instant routing decisions with exceptional precision. Multiple strategy universes are benchmarked in parallel to consistently select the best-performing route configuration. The result is highly reliable, SLA-first delivery operations with improved customer experience and operational consistency."
}
];
return (
<>
<style dangerouslySetInnerHTML={{ __html: `
.miletruth-workflow-heading {
font-size: 24px;
font-weight: 700;
margin-bottom: 15px;
color: #1c1c1c;
}
.testimonials-slider-container {
position: relative;
}
.testimonial-item {
opacity: 0;
visibility: hidden;
position: absolute;
top: 0;
left: 0;
width: 100%;
transition: opacity 0.5s ease-in-out, visibility 0.5s ease-in-out;
}
.testimonial-item.active {
opacity: 1;
visibility: visible;
position: relative;
}
`}} />
<section className="dm-workflow-section" aria-label="Doormile Innovation Workflow">
<div className="dm-workflow-card">
{/* Left Column: Overlapping Chevron Graphic */}
<div className="dm-workflow-left">
<svg viewBox="0 0 320 280" fill="none" xmlns="http://www.w3.org/2000/svg" className="dm-workflow-svg">
{/* Top-Left Chevron Outline (Low Opacity) */}
<path
d="M 30,20 C 22,20 16,26 16,34 L 78,85 C 81,88 81,92 78,95 L 16,146 C 16,154 22,160 30,160 L 130,160 C 138,160 145,154 148,146 L 204,95 C 207,92 207,88 204,85 L 148,34 C 145,26 138,20 130,20 Z"
stroke="white"
strokeWidth="2.5"
strokeLinecap="round"
strokeLinejoin="round"
opacity="0.25"
/>
{/* Bottom-Right Chevron Outline (High Opacity) */}
<path
d="M 110,100 C 102,100 96,106 96,114 L 158,165 C 161,168 161,172 158,175 L 96,226 C 96,234 102,240 110,240 L 210,240 C 218,240 225,234 228,226 L 284,175 C 287,172 287,168 284,165 L 228,114 C 225,106 218,100 210,100 Z"
stroke="white"
strokeWidth="2.5"
strokeLinecap="round"
strokeLinejoin="round"
opacity="0.85"
/>
</svg>
</div>
<div className="elementor-63">
<div className="elementor-element elementor-element-285c828 e-con-full e-flex cut-corner-no sticky-container-off e-con e-parent" data-id="285c828" data-element_type="container" data-e-type="container">
{/* Right Column: Quotes & Text Content */}
<div className="dm-workflow-right">
{/* Slanted red quotes block */}
<svg width="32" height="24" viewBox="0 0 32 24" fill="none" xmlns="http://www.w3.org/2000/svg" className="dm-workflow-quote">
<rect x="2" y="2" width="9" height="20" rx="1.5" transform="skewX(-12)" fill="#C01227" />
<rect x="16" y="2" width="9" height="20" rx="1.5" transform="skewX(-12)" fill="#C01227" />
</svg>
{/* Left Column: Image with overlay */}
<div className="elementor-element elementor-element-f3478fa e-con-full e-flex cut-corner-no sticky-container-off e-con e-child" data-id="f3478fa" data-element_type="container" data-e-type="container" data-settings="{&quot;background_background&quot;:&quot;classic&quot;}">
<div className="elementor-element elementor-element-e8ee5be elementor-widget elementor-widget-logico_image_carousel" data-id="e8ee5be" data-element_type="widget" data-e-type="widget" data-widget_type="logico_image_carousel.default">
<div className="elementor-widget-container">
<div className="logico-image-carousel-widget">
<div className="image-slider">
<div className="image-item slider-item">
<div className="image-item-card with-height">
<Image
width={1733}
height={773}
src="/images/miletruth-2.png"
className="attachment-full size-full wp-image-5026"
alt="Workflow 2"
/>
<div className="image-title">
<div className="elementor-element elementor-element-7500280 elementor-widget elementor-widget-logico_heading" data-id="7500280" data-element_type="widget" data-e-type="widget" data-widget_type="logico_heading.default">
<div className="elementor-widget-container" style={{ backgroundColor: "white", padding: "20px", borderRadius: "10px", opacity: 0.9 }}>
<h4 className="logico-title" style={{ color: "#C01227", margin: 0 }}>
Our Competitive Edge
</h4>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
{/* Heading */}
<h3 className="dm-workflow-title">
{slides[activeSlide].title}
</h3>
{/* Slide Paragraph with premium fade-in transition */}
<div className="dm-workflow-text-container">
<AnimatePresence mode="wait">
<motion.p
key={activeSlide}
initial={{ opacity: 0, y: 12 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: -12 }}
transition={{ duration: 0.28, ease: "easeInOut" }}
className="dm-workflow-text"
>
{slides[activeSlide].text}
</motion.p>
</AnimatePresence>
</div>
{/* Right Column: Testimonial/Text Slider */}
<div className="elementor-element elementor-element-79ba100 e-flex e-con-boxed cut-corner-no sticky-container-off e-con e-child" data-id="79ba100" data-element_type="container" data-e-type="container" data-settings="{&quot;background_background&quot;:&quot;classic&quot;}">
<div className="e-con-inner">
<div className="elementor-element elementor-element-9c38369 e-con-full e-flex cut-corner-no sticky-container-off e-con e-child" data-id="9c38369" data-element_type="container" data-e-type="container">
{/* Secondary Image inside right col */}
<div className="elementor-element elementor-element-8f3f74d e-con-full e-flex cut-corner-no sticky-container-off e-con e-child" data-id="8f3f74d" data-element_type="container" data-e-type="container">
<div className="elementor-element elementor-element-f5a66b2 elementor-widget elementor-widget-image" data-id="f5a66b2" data-element_type="widget" data-e-type="widget" data-widget_type="image.default">
<div className="elementor-widget-container">
<Image
width={491}
height={373}
src="/images/home2-pic-2.png"
className="attachment-full size-full wp-image-4396"
alt="Decorative pic"
/>
</div>
</div>
</div>
{/* Slider */}
<div className="elementor-element elementor-element-4071ec8 e-con-full e-flex cut-corner-no sticky-container-off e-con e-child" data-id="4071ec8" data-element_type="container" data-e-type="container">
<div className="elementor-element elementor-element-0a76e77 elementor-widget elementor-widget-logico_testimonial_carousel" data-id="0a76e77" data-element_type="widget" data-e-type="widget" data-widget_type="logico_testimonial_carousel.default">
<div className="elementor-widget-container">
<div className="logico-testimonial-carousel-widget">
<div className="testimonial-carousel-wrapper witch-icon">
<div className="testimonials-slider-container">
<div className="testimonials-slider">
{slides.map((slide, index) => (
<div
key={index}
className={`testimonial-item slider-item ${index === activeSlide ? "active" : ""}`}
>
<div className="testimonial-text">
<h4 className="miletruth-workflow-heading">{slide.title}</h4>
<p>{slide.text}</p>
</div>
</div>
))}
</div>
{/* Slider Navigation Footer */}
<div className="slider-footer slider-footer-view-outside slider-footer-position-after slider-footer-width-full miletruth-workflow-progress">
<div className="slider-footer-content" style={{ display: "flex", justifyContent: "space-between", alignItems: "center" }}>
<div className="slider-pagination" style={{ display: "flex", alignItems: "center", gap: "15px" }}>
<div className="slider-progress-wrapper">
<span className="slider-progress-current">0{activeSlide + 1}</span>
/
<span className="slider-progress-all">03</span>
</div>
<div className="owl-dots owl-dots-workflow-2" style={{ display: "flex", gap: "8px" }}>
{slides.map((_, index) => (
<button
key={index}
type="button"
role="button"
className={`owl-dot ${index === activeSlide ? "active" : ""}`}
onClick={() => setActiveSlide(index)}
style={{ width: "10px", height: "10px", borderRadius: "50%", padding: 0 }}
>
<span></span>
</button>
))}
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
{/* Right-aligned Navigation (Counter + Indication lines) */}
<div className="dm-workflow-nav">
<span className="dm-workflow-counter">
0{activeSlide + 1}/03
</span>
<div className="dm-workflow-bars">
{slides.map((_, index) => (
<button
key={index}
type="button"
aria-label={`Go to slide ${index + 1}`}
className={`dm-workflow-bar ${index === activeSlide ? "is-active" : ""}`}
onClick={() => setActiveSlide(index)}
/>
))}
</div>
</div>
</div>
</div>
</>
<style dangerouslySetInnerHTML={{ __html: styles }} />
</section>
);
}
const styles = `
.dm-workflow-section {
max-width: 1700px;
margin: 20px auto;
padding: 0 40px;
box-sizing: border-box;
}
.dm-workflow-card {
position: relative;
background: #181818;
border-radius: 50px;
border: 1px solid rgba(255, 255, 255, 0.05);
box-shadow:
0 10px 40px -10px rgba(0, 0, 0, 0.5),
inset 0 1px 0 rgba(255, 255, 255, 0.05);
padding: 40px 60px;
display: flex;
align-items: center;
justify-content: space-between;
gap: 40px;
overflow: hidden;
box-sizing: border-box;
}
/* Left Column - Graphic */
.dm-workflow-left {
flex: 1;
display: flex;
align-items: center;
justify-content: center;
max-width: 440px;
}
.dm-workflow-svg {
width: 100%;
height: auto;
filter: drop-shadow(0 8px 24px rgba(0,0,0,0.3));
}
/* Right Column - Text & Slide Content */
.dm-workflow-right {
flex: 1.2;
display: flex;
flex-direction: column;
align-items: flex-start;
gap: 20px;
}
.dm-workflow-quote {
margin-bottom: 5px;
}
.dm-workflow-title {
font-family: var(--font-space-grotesk), var(--font-manrope), system-ui, sans-serif;
font-size: 38px;
font-weight: 700;
color: #F8FAFC !important;
letter-spacing: -0.015em;
margin: 0 !important;
padding: 0 !important;
text-transform: uppercase;
}
.dm-workflow-text-container {
min-height: 110px; /* Prevent layout shift when swapping slide texts */
width: 100%;
}
.dm-workflow-text {
font-family: var(--font-manrope), system-ui, sans-serif;
font-size: 16px;
line-height: 1.65;
color: #A3A3A3;
margin: 0 !important;
padding: 0 !important;
}
/* Navigation footer */
.dm-workflow-nav {
display: flex;
flex-direction: column;
align-items: flex-end;
gap: 10px;
align-self: flex-end;
margin-top: 10px;
}
.dm-workflow-counter {
font-family: var(--font-space-grotesk), sans-serif;
font-size: 13px;
font-weight: 700;
color: #737373;
letter-spacing: 0.08em;
}
.dm-workflow-bars {
display: flex;
gap: 8px;
}
.dm-workflow-bar {
width: 40px;
height: 3px;
border: none;
padding: 0;
background: rgba(255, 255, 255, 0.15);
border-radius: 999px;
cursor: pointer;
transition: all 0.3s ease;
}
.dm-workflow-bar.is-active {
background: #C01227;
}
.dm-workflow-bar:hover {
background: rgba(255, 255, 255, 0.35);
}
.dm-workflow-bar.is-active:hover {
background: #C01227;
}
/* Responsive design */
@media (max-width: 1024px) {
.dm-workflow-card {
padding: 50px 50px;
gap: 50px;
border-radius: 40px;
}
.dm-workflow-title {
font-size: 32px;
}
}
@media (max-width: 768px) {
.dm-workflow-card {
flex-direction: column;
padding: 40px 30px;
gap: 40px;
border-radius: 30px;
}
.dm-workflow-left {
max-width: 280px;
}
.dm-workflow-right {
width: 100%;
}
.dm-workflow-title {
font-size: 28px;
}
.dm-workflow-text-container {
min-height: auto;
}
}
`;