Fix radio marker sprites and size scaling

This commit is contained in:
Daniel 2026-03-06 14:07:59 +01:00
parent cf113f65ca
commit 3d59eda3da

View file

@ -1,5 +1,6 @@
import { useState, useEffect, useRef, useCallback } from 'react'; import { useState, useEffect, useRef, useCallback } from 'react';
import Globe from 'globe.gl'; import Globe from 'globe.gl';
import { CanvasTexture, Sprite, SpriteMaterial } from 'three';
// ── Types ── // ── Types ──
interface RadioPlace { interface RadioPlace {
@ -62,13 +63,13 @@ const THEMES = [
{ id: 'cherry', color: '#e74c6f', label: 'Cherry' }, { id: 'cherry', color: '#e74c6f', label: 'Cherry' },
]; ];
function createParticleTexture(color: string): string { function createMarkerTexture(color: string): CanvasTexture | null {
const canvas = document.createElement('canvas'); const canvas = document.createElement('canvas');
canvas.width = 64; canvas.width = 64;
canvas.height = 64; canvas.height = 64;
const ctx = canvas.getContext('2d'); const ctx = canvas.getContext('2d');
if (!ctx) return ''; if (!ctx) return null;
const gradient = ctx.createRadialGradient(32, 32, 3, 32, 32, 32); const gradient = ctx.createRadialGradient(32, 32, 3, 32, 32, 32);
gradient.addColorStop(0, 'rgba(255,255,255,1)'); gradient.addColorStop(0, 'rgba(255,255,255,1)');
@ -79,7 +80,11 @@ function createParticleTexture(color: string): string {
ctx.fillStyle = gradient; ctx.fillStyle = gradient;
ctx.fillRect(0, 0, 64, 64); 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 ── // ── Component ──
@ -174,11 +179,21 @@ export default function RadioTab({ data }: { data: any }) {
if (globeRef.current && containerRef.current) { if (globeRef.current && containerRef.current) {
const style = getComputedStyle(containerRef.current.parentElement!); const style = getComputedStyle(containerRef.current.parentElement!);
const accentRgb = style.getPropertyValue('--accent-rgb').trim(); const accentRgb = style.getPropertyValue('--accent-rgb').trim();
const particleTexture = createParticleTexture(`rgb(${accentRgb})`); const spriteTexture = createMarkerTexture(`rgb(${accentRgb})`);
globeRef.current globeRef.current.atmosphereColor(`rgb(${accentRgb})`);
.particlesColor(`rgb(${accentRgb})`) globeRef.current.customLayerData(places);
.particlesTexture(particleTexture) globeRef.current.customThreeObject((d: any) => {
.atmosphereColor(`rgb(${accentRgb})`); 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]); }, [theme]);
@ -216,33 +231,41 @@ export default function RadioTab({ data }: { data: any }) {
if (!containerRef.current || places.length === 0) return; if (!containerRef.current || places.length === 0) return;
if (globeRef.current) { if (globeRef.current) {
globeRef.current.particlesData([places]); globeRef.current.customLayerData(places);
return; return;
} }
// Read accent color from theme // Read accent color from theme
const initStyle = getComputedStyle(containerRef.current.parentElement!); const initStyle = getComputedStyle(containerRef.current.parentElement!);
const initRgb = initStyle.getPropertyValue('--accent-rgb').trim() || '230, 126, 34'; 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) const globe = new Globe(containerRef.current)
.backgroundColor('rgba(0,0,0,0)') .backgroundColor('rgba(0,0,0,0)')
.atmosphereColor(`rgb(${initRgb})`) .atmosphereColor(`rgb(${initRgb})`)
.atmosphereAltitude(0.12) .atmosphereAltitude(0.12)
.globeImageUrl('/nasa-blue-marble.jpg') .globeImageUrl('/nasa-blue-marble.jpg')
.particlesData([places] as any) .customLayerData(places)
.particlesList((d: any) => d) .customLayerLabel((d: any) =>
.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) =>
`<div style="font-family:system-ui;font-size:13px;color:#fff;background:rgba(30,31,34,0.92);padding:6px 10px;border-radius:6px;border:1px solid rgba(${initRgb},0.3);pointer-events:none">` + `<div style="font-family:system-ui;font-size:13px;color:#fff;background:rgba(30,31,34,0.92);padding:6px 10px;border-radius:6px;border:1px solid rgba(${initRgb},0.3);pointer-events:none">` +
`<b>${d.title}</b><br/><span style="color:#949ba4;font-size:11px">${d.country}</span></div>` `<b>${d.title}</b><br/><span style="color:#949ba4;font-size:11px">${d.country}</span></div>`
) )
.onParticleClick((d: any) => handlePointClickRef.current?.(d)) .customThreeObject((d: any) => {
const material = new SpriteMaterial({
map: markerTexture ?? undefined,
color: `rgb(${initRgb})`,
transparent: true,
depthWrite: false,
});
const sprite = new Sprite(material);
const scale = markerScale(d.size ?? 1);
sprite.scale.set(scale, scale, 1);
return sprite;
})
.customThreeObjectUpdate((obj: any, d: any) => {
const scale = markerScale(d.size ?? 1);
obj.scale.set(scale, scale, 1);
})
.onCustomLayerClick((d: any) => handlePointClickRef.current?.(d))
.width(containerRef.current.clientWidth) .width(containerRef.current.clientWidth)
.height(containerRef.current.clientHeight); .height(containerRef.current.clientHeight);