Files
moon/components/VisibilityMap.tsx
Puechberty Arthur 49fd31f4db first commit
2026-03-30 23:07:36 +02:00

173 lines
5.5 KiB
TypeScript

'use client';
import { useRef, useEffect, useCallback } from 'react';
import { useLocale } from './LocaleProvider';
import { ts } from '@/lib/i18n';
import { getMoonDeclination, getMoonRightAscension, getMoonPhaseInfo } from '@/lib/lunar';
export default function VisibilityMap() {
const { locale } = useLocale();
const canvasRef = useRef<HTMLCanvasElement>(null);
const mapImageRef = useRef<HTMLImageElement | null>(null);
const dateToJD = useCallback((date: Date): number => {
const y = date.getUTCFullYear();
const m = date.getUTCMonth() + 1;
const d = date.getUTCDate() + date.getUTCHours() / 24;
let yr = y;
let mo = m;
if (mo <= 2) {
yr -= 1;
mo += 12;
}
const A = Math.floor(yr / 100);
const B = 2 - A + Math.floor(A / 4);
return Math.floor(365.25 * (yr + 4716)) + Math.floor(30.6001 * (mo + 1)) + d + B - 1524.5;
}, []);
const drawMap = useCallback(() => {
const canvas = canvasRef.current;
if (!canvas) return;
const ctx = canvas.getContext('2d');
if (!ctx) return;
const now = new Date();
const w = canvas.width;
const h = canvas.height;
ctx.clearRect(0, 0, w, h);
const mapImage = mapImageRef.current;
if (mapImage && mapImage.complete && mapImage.naturalWidth > 0) {
ctx.drawImage(mapImage, 0, 0, w, h);
// Slight dark layer for readability of overlays
ctx.fillStyle = 'rgba(10, 14, 39, 0.25)';
ctx.fillRect(0, 0, w, h);
} else {
// Fallback background if image is missing
ctx.fillStyle = '#0a0e27';
ctx.fillRect(0, 0, w, h);
ctx.strokeStyle = 'rgba(99, 102, 241, 0.1)';
ctx.lineWidth = 0.5;
for (let lat = -60; lat <= 60; lat += 30) {
const y = h / 2 - (lat / 90) * (h / 2);
ctx.beginPath();
ctx.moveTo(0, y);
ctx.lineTo(w, y);
ctx.stroke();
}
for (let lon = -150; lon <= 180; lon += 30) {
const x = w / 2 + (lon / 180) * (w / 2);
ctx.beginPath();
ctx.moveTo(x, 0);
ctx.lineTo(x, h);
ctx.stroke();
}
}
// Moon position
const dec = getMoonDeclination(now);
const ra = getMoonRightAscension(now);
// Convert RA to longitude (approximate, accounting for Earth's rotation)
const gmst = (280.46061837 + 360.98564736629 * ((dateToJD(now) - 2451545.0))) % 360;
let moonLon = ra - gmst;
while (moonLon > 180) moonLon -= 360;
while (moonLon < -180) moonLon += 360;
const moonX = w / 2 + (moonLon / 180) * (w / 2);
const moonY = h / 2 - (dec / 90) * (h / 2);
// Visibility zone (large circle)
const visibilityRadius = w * 0.25;
const visGrad = ctx.createRadialGradient(moonX, moonY, 0, moonX, moonY, visibilityRadius);
visGrad.addColorStop(0, 'rgba(251, 191, 36, 0.2)');
visGrad.addColorStop(0.5, 'rgba(251, 191, 36, 0.08)');
visGrad.addColorStop(1, 'rgba(251, 191, 36, 0)');
ctx.fillStyle = visGrad;
ctx.beginPath();
ctx.arc(moonX, moonY, visibilityRadius, 0, Math.PI * 2);
ctx.fill();
// Moon position marker
const moonPhase = getMoonPhaseInfo(now);
// Glow
const glowGrad = ctx.createRadialGradient(moonX, moonY, 0, moonX, moonY, 25);
glowGrad.addColorStop(0, 'rgba(251, 191, 36, 0.8)');
glowGrad.addColorStop(0.5, 'rgba(251, 191, 36, 0.3)');
glowGrad.addColorStop(1, 'rgba(251, 191, 36, 0)');
ctx.fillStyle = glowGrad;
ctx.beginPath();
ctx.arc(moonX, moonY, 25, 0, Math.PI * 2);
ctx.fill();
// Moon dot
ctx.beginPath();
ctx.arc(moonX, moonY, 8, 0, Math.PI * 2);
ctx.fillStyle = '#fbbf24';
ctx.fill();
ctx.strokeStyle = '#fef9c3';
ctx.lineWidth = 2;
ctx.stroke();
// Label
ctx.fillStyle = '#fef9c3';
ctx.font = '12px system-ui';
ctx.textAlign = 'center';
ctx.fillText(`${moonPhase.emoji} ${moonPhase.illumination}%`, moonX, moonY - 18);
// Equator label
ctx.fillStyle = 'rgba(255,255,255,0.2)';
ctx.font = '10px system-ui';
ctx.textAlign = 'left';
ctx.fillText('0°', 4, h / 2 + 4);
}, [dateToJD]);
useEffect(() => {
const mapImage = new Image();
mapImage.src = '/moon-visibility-map.webp';
mapImage.onload = () => {
mapImageRef.current = mapImage;
drawMap();
};
mapImage.onerror = () => {
mapImageRef.current = null;
drawMap();
};
drawMap();
const interval = setInterval(drawMap, 60000); // Update every minute
return () => clearInterval(interval);
}, [drawMap]);
return (
<section id="visibility" aria-label="Moon Visibility World Map" className="section-container">
<div className="text-center mb-10">
<h2 className="section-title">{ts('visibility_title', locale)}</h2>
<p className="section-subtitle mx-auto">{ts('visibility_subtitle', locale)}</p>
</div>
<div className="glass-card overflow-hidden max-w-5xl mx-auto">
<canvas
ref={canvasRef}
width={900}
height={450}
className="w-full h-auto min-h-50"
/>
<div className="flex flex-col sm:flex-row justify-center gap-3 sm:gap-6 py-3 border-t border-white/5 text-xs text-white/40">
<span className="flex items-center gap-1.5">
<span className="w-3 h-3 rounded-full bg-yellow-400 inline-block" /> Moon Position
</span>
<span className="flex items-center gap-1.5">
<span className="w-3 h-3 rounded-full bg-yellow-400/20 inline-block" /> Visibility Zone
</span>
</div>
</div>
</section>
);
}