mirror of
https://github.com/arthur-pbty/moon.git
synced 2026-06-03 15:07:31 +02:00
173 lines
5.5 KiB
TypeScript
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>
|
|
);
|
|
}
|