import React, { useRef, useEffect, useState, useMemo } from "react"; import * as THREE from "three"; import { useGPS } from "./useGPS"; import type { WorldBounds } from "./useGPS"; // ========================================== // 1. Premium 2D HUD GPS Overlay Component // ========================================== export interface GPSMinimapHUDProps { bwMaskUrl: string; colorMapUrl: string; gridWidth: number; gridHeight: number; worldBounds: WorldBounds; playerPos: { x: number; z: number }; destPos?: { x: number; z: number }; size?: number; // Size of HUD in pixels } /** * A beautiful, glassmorphic 2D HUD overlay that renders the GPS Minimap * in the corner of the screen. */ export const GPSMinimapHUD: React.FC = ({ bwMaskUrl, colorMapUrl, gridWidth, gridHeight, worldBounds, playerPos, destPos, size = 200, }) => { const canvasRef = useRef(null); const gpsOptions = useMemo( () => ({ bwMaskUrl, colorMapUrl, gridWidth, gridHeight, worldBounds, }), [bwMaskUrl, colorMapUrl, gridWidth, gridHeight, worldBounds], ); const { calculateWorldPath, renderGPSToCanvas, loading, error } = useGPS(gpsOptions); useEffect(() => { if (loading || error || !canvasRef.current) return; // Calculate A* path in world coordinates const path = destPos ? calculateWorldPath(playerPos, destPos) : []; // Render path onto HUD canvas renderGPSToCanvas(canvasRef.current, path, playerPos, destPos, { pathColor: "#3b82f6", // Premium vibrant blue pathWidth: 5, playerColor: "#ef4444", // Hot red for player playerSize: 6, destColor: "#10b981", // Emerald green for destination destSize: 6, }); }, [ playerPos, destPos, loading, error, calculateWorldPath, renderGPSToCanvas, ]); return (
{loading &&
Initializing GPS...
} {error && (
GPS Error: {error}
)} {!loading && !error && ( )}
); }; // ========================================== // 2. 3D Handlebar Screen Mesh Component (R3F) // ========================================== export interface GPSBikeScreenProps { bwMaskUrl: string; colorMapUrl: string; gridWidth: number; gridHeight: number; worldBounds: WorldBounds; playerPos: { x: number; z: number }; destPos?: { x: number; z: number }; width?: number; // 3D Plane Width height?: number; // 3D Plane Height } /** * A Three.js 3D plane mesh that renders the GPS dynamically as a CanvasTexture. * This can be directly attached to the bike's handlebars in your 3D world. */ export const GPSBikeScreen: React.FC = ({ bwMaskUrl, colorMapUrl, gridWidth, gridHeight, worldBounds, playerPos, destPos, width = 0.4, height = 0.4, }) => { // Offscreen canvas to render the GPS texture onto const [offscreenCanvas] = useState(() => { const canvas = document.createElement("canvas"); canvas.width = 512; canvas.height = 512; return canvas; }); const textureRef = useRef(null); const gpsOptions = useMemo( () => ({ bwMaskUrl, colorMapUrl, gridWidth, gridHeight, worldBounds, }), [bwMaskUrl, colorMapUrl, gridWidth, gridHeight, worldBounds], ); const { calculateWorldPath, renderGPSToCanvas, loading } = useGPS(gpsOptions); useEffect(() => { if (loading) return; // Calculate A* path const path = destPos ? calculateWorldPath(playerPos, destPos) : []; // Render path onto our offscreen canvas renderGPSToCanvas(offscreenCanvas, path, playerPos, destPos, { pathColor: "#60a5fa", // Bright neon blue pathWidth: 8, playerColor: "#ff0055", // Neon pink-red for bike playerSize: 10, destColor: "#00ffcc", // Vibrant cyan for target destSize: 10, }); // Notify Three.js that the texture needs an update if (textureRef.current) { textureRef.current.needsUpdate = true; } }, [ playerPos, destPos, loading, calculateWorldPath, renderGPSToCanvas, offscreenCanvas, ]); return ( ); }; // ========================================== // Styles for HUD (Premium Glassmorphism) // ========================================== const hudStyles = { container: (size: number): React.CSSProperties => ({ position: "absolute", bottom: "24px", right: "24px", width: `${size}px`, height: `${size}px`, borderRadius: "24px", overflow: "hidden", border: "1px solid rgba(255, 255, 255, 0.15)", boxShadow: "0 8px 32px 0 rgba(0, 0, 0, 0.37), 0 0 15px rgba(59, 130, 246, 0.2)", backdropFilter: "blur(8px)", WebkitBackdropFilter: "blur(8px)", background: "rgba(15, 23, 42, 0.6)", // Sleek dark slate display: "flex", alignItems: "center", justifyContent: "center", zIndex: 1000, pointerEvents: "none", transition: "all 0.3s cubic-bezier(0.4, 0, 0.2, 1)", }), canvas: (size: number): React.CSSProperties => ({ width: `${size}px`, height: `${size}px`, display: "block", }), statusText: { color: "#94a3b8", fontFamily: 'system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif', fontSize: "12px", fontWeight: 500, letterSpacing: "0.05em", } as React.CSSProperties, };