// Monochrome line icons, 1.5px stroke. Lucide-style.
const Icon = ({ d, size = 20, stroke = "currentColor", children, viewBox="0 0 24 24", fill="none" }) => (
);
const IconCircular = (p) => (
);
/*
Hero Route Visualization — ANIMATED.
A single concatenated SVG path (4 Bézier legs) drives a truck via getPointAtLength.
- HUD top-left updates live: active leg, km done, ETA.
- Waypoints (REST / FUEL / TOLL) activate as the truck passes, then fade back down.
- Cities flash when reached. Valencia (base) pulses, and flares green when loop closes.
- One full loop cycles over LOOP_MS; short dwell at Valencia before restart.
*/
const RouteDiagram = () => {
const pathRef = React.useRef(null);
const [progress, setProgress] = React.useState(0); // 0..1 along full route
const [dwelling, setDwelling] = React.useState(false); // true during the pause at VLC
// Real-world leg distances (km). Used for HUD readout + trail tinting.
// Proportions here determine where along the compound path each leg ends.
const LEGS = React.useMemo(() => ([
{ from: "VLC", to: "MAD", km: 357, color: "#0A0A0A" },
{ from: "MAD", to: "ZGZ", km: 315, color: "#0A0A0A" },
{ from: "ZGZ", to: "BCN", km: 296, color: "#0A0A0A" },
{ from: "BCN", to: "VLC", km: 349, color: "#1FB58A" }, // return leg
]), []);
const TOTAL_KM = LEGS.reduce((s,l)=>s+l.km, 0); // 1317 (rounded to 1,480 in the HUD label for 'km planned' including approach)
// Waypoint trigger thresholds (fraction of full path where each is active).
// These roughly map to: REST mid-leg1, FUEL on leg2, TOLL on leg3.
const REST_AT = 0.12;
const FUEL_AT = 0.35;
const TOLL_AT = 0.60;
const WP_WINDOW = 0.09; // how long each stays highlighted, in progress units
const cityReached = (p) => {
// returns which city the truck has most recently passed
if (p < 0.25) return 0; // still on leg 1 (heading MAD)
if (p < 0.50) return 1; // reached MAD
if (p < 0.75) return 2; // reached ZGZ
if (p < 1.00) return 3; // reached BCN
return 4; // back at VLC
};
// animation loop
React.useEffect(() => {
let raf, start = 0, dwellUntil = 0;
const LOOP_MS = 14000;
const DWELL_MS = 1500;
let mounted = true;
const tick = (ts) => {
if (!mounted) return;
if (!start) start = ts;
if (dwellUntil) {
if (ts >= dwellUntil) {
start = ts;
dwellUntil = 0;
setDwelling(false);
setProgress(0);
}
raf = requestAnimationFrame(tick);
return;
}
const p = Math.min(1, (ts - start) / LOOP_MS);
setProgress(p);
if (p >= 1) {
dwellUntil = ts + DWELL_MS;
setDwelling(true);
}
raf = requestAnimationFrame(tick);
};
raf = requestAnimationFrame(tick);
return () => { mounted = false; cancelAnimationFrame(raf); };
}, []);
// lookup current xy + tangent on the compound path
const ptRef = React.useRef({ x: 412, y: 282, angle: 0, len: 0 });
const [, force] = React.useReducer(x => x+1, 0);
React.useEffect(() => {
const el = pathRef.current;
if (!el) return;
const L = el.getTotalLength();
const at = L * progress;
const pt = el.getPointAtLength(at);
const pt2 = el.getPointAtLength(Math.min(L, at + 1));
const angle = Math.atan2(pt2.y - pt.y, pt2.x - pt.x) * 180 / Math.PI;
ptRef.current = { x: pt.x, y: pt.y, angle, len: L };
force();
}, [progress]);
// HUD values
const activeCity = cityReached(progress);
const activeLegIdx = Math.min(3, Math.floor(progress * 4));
const activeLeg = LEGS[activeLegIdx];
const legFrac = (progress * 4) - activeLegIdx;
const kmDone = Math.round(
LEGS.slice(0, activeLegIdx).reduce((s,l)=>s+l.km,0) + activeLeg.km * legFrac
);
const etaH = Math.max(0, Math.round((TOTAL_KM - kmDone) / 70)); // ~70km/h avg
const netNow = Math.round(2840 * Math.min(1, progress * 1.05)); // fill up profit as we progress
// waypoint "hot" helpers
const hot = (trigger) => {
const d = progress - trigger;
if (d < 0 || d > WP_WINDOW) return 0;
// ease in/out
const t = d / WP_WINDOW;
return Math.sin(t * Math.PI); // 0..1..0
};
const hotREST = hot(REST_AT);
const hotFUEL = hot(FUEL_AT);
const hotTOLL = hot(TOLL_AT);
// city "just reached" flash: 0..1 flash intensity
const reachedFlash = (atStart, atEnd) => {
// active around the arrival moment (atEnd)
const d = progress - atEnd;
if (Math.abs(d) > 0.04) return 0;
return 1 - Math.abs(d) / 0.04;
};
const flashMAD = reachedFlash(0, 0.25);
const flashZGZ = reachedFlash(0.25, 0.50);
const flashBCN = reachedFlash(0.50, 0.75);
const flashVLC = dwelling ? 1 : reachedFlash(0.75, 1.0);
// compound path (must match the 4 drawn leg paths below, joined head-to-tail)
const COMPOUND_D = "M 412 282 Q 340 258, 278 230 Q 305 200, 342 180 Q 395 182, 452 192 Q 445 245, 412 282";
return (