diff --git a/web/src/plugins/radio/RadioTab.tsx b/web/src/plugins/radio/RadioTab.tsx index 7015166..8312e2a 100644 --- a/web/src/plugins/radio/RadioTab.tsx +++ b/web/src/plugins/radio/RadioTab.tsx @@ -1,5 +1,6 @@ import { useState, useEffect, useRef, useCallback } from 'react'; import Globe from 'globe.gl'; +import { CanvasTexture, Sprite, SpriteMaterial } from 'three'; // ── Types ── interface RadioPlace { @@ -62,13 +63,13 @@ const THEMES = [ { id: 'cherry', color: '#e74c6f', label: 'Cherry' }, ]; -function createParticleTexture(color: string): string { +function createMarkerTexture(color: string): CanvasTexture | null { const canvas = document.createElement('canvas'); canvas.width = 64; canvas.height = 64; const ctx = canvas.getContext('2d'); - if (!ctx) return ''; + if (!ctx) return null; const gradient = ctx.createRadialGradient(32, 32, 3, 32, 32, 32); gradient.addColorStop(0, 'rgba(255,255,255,1)'); @@ -79,7 +80,11 @@ function createParticleTexture(color: string): string { ctx.fillStyle = gradient; ctx.fillRect(0, 0, 64, 64); - return canvas.toDataURL('image/png'); + return new CanvasTexture(canvas); +} + +function markerScale(size: number): number { + return Math.max(0.55, Math.min(1.75, 0.42 + size * 0.035)); } // ── Component ── @@ -174,11 +179,21 @@ export default function RadioTab({ data }: { data: any }) { if (globeRef.current && containerRef.current) { const style = getComputedStyle(containerRef.current.parentElement!); const accentRgb = style.getPropertyValue('--accent-rgb').trim(); - const particleTexture = createParticleTexture(`rgb(${accentRgb})`); - globeRef.current - .particlesColor(`rgb(${accentRgb})`) - .particlesTexture(particleTexture) - .atmosphereColor(`rgb(${accentRgb})`); + const spriteTexture = createMarkerTexture(`rgb(${accentRgb})`); + globeRef.current.atmosphereColor(`rgb(${accentRgb})`); + globeRef.current.customLayerData(places); + globeRef.current.customThreeObject((d: any) => { + const material = new SpriteMaterial({ + map: spriteTexture ?? undefined, + color: `rgb(${accentRgb})`, + transparent: true, + depthWrite: false, + }); + const sprite = new Sprite(material); + const scale = markerScale(d.size ?? 1); + sprite.scale.set(scale, scale, 1); + return sprite; + }); } }, [theme]); @@ -216,33 +231,41 @@ export default function RadioTab({ data }: { data: any }) { if (!containerRef.current || places.length === 0) return; if (globeRef.current) { - globeRef.current.particlesData([places]); + globeRef.current.customLayerData(places); return; } // Read accent color from theme const initStyle = getComputedStyle(containerRef.current.parentElement!); const initRgb = initStyle.getPropertyValue('--accent-rgb').trim() || '230, 126, 34'; - const particleTexture = createParticleTexture(`rgb(${initRgb})`); + const markerTexture = createMarkerTexture(`rgb(${initRgb})`); const globe = new Globe(containerRef.current) .backgroundColor('rgba(0,0,0,0)') .atmosphereColor(`rgb(${initRgb})`) .atmosphereAltitude(0.12) .globeImageUrl('/nasa-blue-marble.jpg') - .particlesData([places] as any) - .particlesList((d: any) => d) - .particleLat((d: any) => d.geo[1]) - .particleLng((d: any) => d.geo[0]) - .particleAltitude(0.0008) - .particlesSize(6) - .particlesSizeAttenuation(false) - .particlesColor(`rgb(${initRgb})`) - .particlesTexture(particleTexture) - .particleLabel((d: any) => + .customLayerData(places) + .customLayerLabel((d: any) => `