diff --git a/web/src/plugins/radio/RadioTab.tsx b/web/src/plugins/radio/RadioTab.tsx index 9945e14..7015166 100644 --- a/web/src/plugins/radio/RadioTab.tsx +++ b/web/src/plugins/radio/RadioTab.tsx @@ -62,6 +62,26 @@ const THEMES = [ { id: 'cherry', color: '#e74c6f', label: 'Cherry' }, ]; +function createParticleTexture(color: string): string { + const canvas = document.createElement('canvas'); + canvas.width = 64; + canvas.height = 64; + + const ctx = canvas.getContext('2d'); + if (!ctx) return ''; + + const gradient = ctx.createRadialGradient(32, 32, 3, 32, 32, 32); + gradient.addColorStop(0, 'rgba(255,255,255,1)'); + gradient.addColorStop(0.35, color); + gradient.addColorStop(0.7, color.replace('rgb(', 'rgba(').replace(')', ',0.42)')); + gradient.addColorStop(1, 'rgba(0,0,0,0)'); + + ctx.fillStyle = gradient; + ctx.fillRect(0, 0, 64, 64); + + return canvas.toDataURL('image/png'); +} + // ── Component ── export default function RadioTab({ data }: { data: any }) { const containerRef = useRef(null); @@ -154,8 +174,10 @@ 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 - .pointColor(() => `rgba(${accentRgb}, 0.85)`) + .particlesColor(`rgb(${accentRgb})`) + .particlesTexture(particleTexture) .atmosphereColor(`rgb(${accentRgb})`); } }, [theme]); @@ -194,31 +216,33 @@ export default function RadioTab({ data }: { data: any }) { if (!containerRef.current || places.length === 0) return; if (globeRef.current) { - globeRef.current.pointsData(places); + globeRef.current.particlesData([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 globe = new Globe(containerRef.current) .backgroundColor('rgba(0,0,0,0)') .atmosphereColor(`rgb(${initRgb})`) .atmosphereAltitude(0.12) .globeImageUrl('/nasa-blue-marble.jpg') - .pointsData(places) - // Radio Garden geo format: [lng, lat] - .pointLat((d: any) => d.geo[1]) - .pointLng((d: any) => d.geo[0]) - .pointColor(() => `rgba(${initRgb}, 0.85)`) - .pointRadius((d: any) => Math.max(0.12, Math.min(0.45, 0.06 + (d.size ?? 1) * 0.005))) - .pointAltitude(0.003) - .pointResolution(6) - .pointLabel((d: any) => + .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) => `
` + `${d.title}
${d.country}
` ) - .onPointClick((d: any) => handlePointClickRef.current?.(d)) + .onParticleClick((d: any) => handlePointClickRef.current?.(d)) .width(containerRef.current.clientWidth) .height(containerRef.current.clientHeight); @@ -235,23 +259,6 @@ export default function RadioTab({ data }: { data: any }) { controls.autoRotateSpeed = 0.3; } - // Scale points + load detail tiles on zoom change - let lastAlt = 2.0; - const BASE_ALT = 2.0; - const onControlsChange = () => { - const pov = globe.pointOfView(); - const alt = pov.altitude; - // Only recalc when altitude changed significantly (>5%) - if (Math.abs(alt - lastAlt) / lastAlt < 0.05) return; - lastAlt = alt; - const scale = Math.max(0.1, alt / BASE_ALT); - globe.pointRadius((d: any) => { - const base = Math.max(0.12, Math.min(0.45, 0.06 + (d.size ?? 1) * 0.005)); - return base * scale; - }); - }; - controls.addEventListener('change', onControlsChange); - globeRef.current = globe; // Pause rotation on any globe interaction (drag, scroll, touch) @@ -271,7 +278,6 @@ export default function RadioTab({ data }: { data: any }) { window.addEventListener('resize', onResize); return () => { - controls.removeEventListener('change', onControlsChange); el.removeEventListener('mousedown', onInteract); el.removeEventListener('touchstart', onInteract); el.removeEventListener('wheel', onInteract);