Fix: Crisp radio station dots with zoom-scaling
Replace sprite-based markers (objectsData + SpriteMaterial with soft 64x64 gradient texture → blurry at zoom) with optimized point meshes: - pointResolution(24): smooth 24-sided circles (no hexagons) - pointAltitude(0.001): nearly flat on surface (no cylinder effect) - sqrt-based zoom scaling: dots shrink when zooming in, grow when zooming out → visually consistent at all zoom levels - Removed three.js Sprite/SpriteMaterial/CanvasTexture imports Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
99421f4577
commit
a923463f83
1 changed files with 35 additions and 62 deletions
|
|
@ -1,6 +1,5 @@
|
|||
import { useState, useEffect, useRef, useCallback } from 'react';
|
||||
import Globe from 'globe.gl';
|
||||
import { CanvasTexture, Sprite, SpriteMaterial } from 'three';
|
||||
|
||||
// ── Types ──
|
||||
interface RadioPlace {
|
||||
|
|
@ -63,29 +62,9 @@ const THEMES = [
|
|||
{ id: 'cherry', color: '#e74c6f', label: 'Cherry' },
|
||||
];
|
||||
|
||||
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 null;
|
||||
|
||||
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 new CanvasTexture(canvas);
|
||||
}
|
||||
|
||||
function markerScale(size: number): number {
|
||||
return Math.max(0.55, Math.min(1.75, 0.42 + size * 0.035));
|
||||
}
|
||||
// ── Zoom scaling constants ──
|
||||
const BASE_ALT = 2.0;
|
||||
const BASE_RADIUS = 0.18;
|
||||
|
||||
// ── Component ──
|
||||
export default function RadioTab({ data }: { data: any }) {
|
||||
|
|
@ -179,21 +158,9 @@ 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 spriteTexture = createMarkerTexture(`rgb(${accentRgb})`);
|
||||
globeRef.current.atmosphereColor(`rgb(${accentRgb})`);
|
||||
globeRef.current.objectsData(places);
|
||||
globeRef.current.objectThreeObject((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;
|
||||
});
|
||||
globeRef.current
|
||||
.pointColor(() => `rgba(${accentRgb}, 0.85)`)
|
||||
.atmosphereColor(`rgba(${accentRgb}, 0.25)`);
|
||||
}
|
||||
}, [theme]);
|
||||
|
||||
|
|
@ -231,41 +198,32 @@ export default function RadioTab({ data }: { data: any }) {
|
|||
if (!containerRef.current || places.length === 0) return;
|
||||
|
||||
if (globeRef.current) {
|
||||
globeRef.current.objectsData(places);
|
||||
globeRef.current.pointsData(places);
|
||||
return;
|
||||
}
|
||||
|
||||
// Read accent color from theme
|
||||
const initStyle = getComputedStyle(containerRef.current.parentElement!);
|
||||
const initRgb = initStyle.getPropertyValue('--accent-rgb').trim() || '230, 126, 34';
|
||||
const markerTexture = createMarkerTexture(`rgb(${initRgb})`);
|
||||
|
||||
const globe = new Globe(containerRef.current)
|
||||
.backgroundColor('rgba(0,0,0,0)')
|
||||
.atmosphereColor(`rgb(${initRgb})`)
|
||||
.atmosphereColor(`rgba(${initRgb}, 0.25)`)
|
||||
.atmosphereAltitude(0.12)
|
||||
.globeImageUrl('/nasa-blue-marble.jpg')
|
||||
.objectsData(places)
|
||||
.objectLat((d: any) => d.geo[1])
|
||||
.objectLng((d: any) => d.geo[0])
|
||||
.objectAltitude(0.0012)
|
||||
.objectFacesSurface(false)
|
||||
.objectLabel((d: any) =>
|
||||
.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(BASE_RADIUS)
|
||||
.pointAltitude(0.001)
|
||||
.pointResolution(24)
|
||||
.pointLabel((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">` +
|
||||
`<b>${d.title}</b><br/><span style="color:#949ba4;font-size:11px">${d.country}</span></div>`
|
||||
)
|
||||
.objectThreeObject((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;
|
||||
})
|
||||
.onObjectClick((d: any) => handlePointClickRef.current?.(d))
|
||||
.onPointClick((d: any) => handlePointClickRef.current?.(d))
|
||||
.width(containerRef.current.clientWidth)
|
||||
.height(containerRef.current.clientHeight);
|
||||
|
||||
|
|
@ -273,7 +231,7 @@ export default function RadioTab({ data }: { data: any }) {
|
|||
globe.renderer().setPixelRatio(window.devicePixelRatio);
|
||||
|
||||
// Start-Position: Europa
|
||||
globe.pointOfView({ lat: 48, lng: 10, altitude: 2.0 });
|
||||
globe.pointOfView({ lat: 48, lng: 10, altitude: BASE_ALT });
|
||||
|
||||
// Auto-Rotation
|
||||
const controls = globe.controls() as any;
|
||||
|
|
@ -282,6 +240,20 @@ export default function RadioTab({ data }: { data: any }) {
|
|||
controls.autoRotateSpeed = 0.3;
|
||||
}
|
||||
|
||||
// ── Zoom-based dot scaling ──
|
||||
// Dots scale with sqrt(altitude) so they stay visually consistent:
|
||||
// zoomed out (alt 2.0) → radius 0.18°, zoomed in (alt 0.4) → ~0.08°
|
||||
let lastAlt = BASE_ALT;
|
||||
const onControlsChange = () => {
|
||||
const pov = globe.pointOfView();
|
||||
const alt = pov.altitude;
|
||||
if (Math.abs(alt - lastAlt) / lastAlt < 0.05) return;
|
||||
lastAlt = alt;
|
||||
const scale = Math.sqrt(alt / BASE_ALT);
|
||||
globe.pointRadius(() => BASE_RADIUS * Math.max(0.15, Math.min(2.5, scale)));
|
||||
};
|
||||
controls.addEventListener('change', onControlsChange);
|
||||
|
||||
globeRef.current = globe;
|
||||
|
||||
// Pause rotation on any globe interaction (drag, scroll, touch)
|
||||
|
|
@ -301,6 +273,7 @@ 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);
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue