mirror of
https://github.com/arthur-pbty/moon.git
synced 2026-06-03 23:36:19 +02:00
first commit
This commit is contained in:
@@ -0,0 +1,172 @@
|
||||
'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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user