mirror of
https://github.com/arthur-pbty/moon.git
synced 2026-06-03 15:07:31 +02:00
267 lines
9.0 KiB
TypeScript
267 lines
9.0 KiB
TypeScript
'use client';
|
|
|
|
import { useRef, useEffect, useCallback } from 'react';
|
|
import { useLocale } from './LocaleProvider';
|
|
import { ts } from '@/lib/i18n';
|
|
|
|
export default function Infographics() {
|
|
const { locale } = useLocale();
|
|
const phasesCanvasRef = useRef<HTMLCanvasElement>(null);
|
|
const tidesCanvasRef = useRef<HTMLCanvasElement>(null);
|
|
|
|
const drawPhasesCycle = useCallback(() => {
|
|
const canvas = phasesCanvasRef.current;
|
|
if (!canvas) return;
|
|
const ctx = canvas.getContext('2d');
|
|
if (!ctx) return;
|
|
|
|
const w = canvas.width;
|
|
const h = canvas.height;
|
|
const cx = w / 2;
|
|
const cy = h / 2;
|
|
const radius = Math.min(cx, cy) - 60;
|
|
|
|
ctx.clearRect(0, 0, w, h);
|
|
|
|
// Background
|
|
ctx.fillStyle = '#0a0a1a';
|
|
ctx.fillRect(0, 0, w, h);
|
|
|
|
// Orbit circle
|
|
ctx.beginPath();
|
|
ctx.arc(cx, cy, radius, 0, Math.PI * 2);
|
|
ctx.strokeStyle = 'rgba(99, 102, 241, 0.2)';
|
|
ctx.lineWidth = 2;
|
|
ctx.setLineDash([5, 5]);
|
|
ctx.stroke();
|
|
ctx.setLineDash([]);
|
|
|
|
// Earth at center
|
|
ctx.beginPath();
|
|
ctx.arc(cx, cy, 25, 0, Math.PI * 2);
|
|
const earthGrad = ctx.createRadialGradient(cx - 5, cy - 5, 0, cx, cy, 25);
|
|
earthGrad.addColorStop(0, '#4da6ff');
|
|
earthGrad.addColorStop(1, '#1a5276');
|
|
ctx.fillStyle = earthGrad;
|
|
ctx.fill();
|
|
ctx.fillStyle = '#fff';
|
|
ctx.font = 'bold 10px system-ui';
|
|
ctx.textAlign = 'center';
|
|
ctx.fillText('🌍', cx, cy + 4);
|
|
|
|
// Sun direction arrow
|
|
ctx.fillStyle = 'rgba(251, 191, 36, 0.6)';
|
|
ctx.font = '12px system-ui';
|
|
ctx.fillText('☀️ Sun →', w - 60, 30);
|
|
|
|
// Sun light gradient from right
|
|
const sunGrad = ctx.createLinearGradient(w, 0, 0, 0);
|
|
sunGrad.addColorStop(0, 'rgba(251, 191, 36, 0.03)');
|
|
sunGrad.addColorStop(1, 'rgba(251, 191, 36, 0)');
|
|
ctx.fillStyle = sunGrad;
|
|
ctx.fillRect(0, 0, w, h);
|
|
|
|
// Moon phases around orbit
|
|
const phases = [
|
|
{ angle: 0, emoji: '🌑', label: ts('new_moon', locale), illum: '0%' },
|
|
{ angle: Math.PI / 4, emoji: '🌒', label: ts('waxing_crescent', locale), illum: '25%' },
|
|
{ angle: Math.PI / 2, emoji: '🌓', label: ts('first_quarter', locale), illum: '50%' },
|
|
{ angle: 3 * Math.PI / 4, emoji: '🌔', label: ts('waxing_gibbous', locale), illum: '75%' },
|
|
{ angle: Math.PI, emoji: '🌕', label: ts('full_moon', locale), illum: '100%' },
|
|
{ angle: 5 * Math.PI / 4, emoji: '🌖', label: ts('waning_gibbous', locale), illum: '75%' },
|
|
{ angle: 3 * Math.PI / 2, emoji: '🌗', label: ts('last_quarter', locale), illum: '50%' },
|
|
{ angle: 7 * Math.PI / 4, emoji: '🌘', label: ts('waning_crescent', locale), illum: '25%' },
|
|
];
|
|
|
|
phases.forEach((p) => {
|
|
const x = cx + Math.sin(p.angle) * radius;
|
|
const y = cy - Math.cos(p.angle) * radius;
|
|
|
|
// Moon circle background
|
|
ctx.beginPath();
|
|
ctx.arc(x, y, 22, 0, Math.PI * 2);
|
|
ctx.fillStyle = 'rgba(15, 15, 35, 0.9)';
|
|
ctx.fill();
|
|
ctx.strokeStyle = 'rgba(99, 102, 241, 0.3)';
|
|
ctx.lineWidth = 1;
|
|
ctx.stroke();
|
|
|
|
// Emoji
|
|
ctx.fillStyle = '#fff';
|
|
ctx.font = '22px system-ui';
|
|
ctx.textAlign = 'center';
|
|
ctx.fillText(p.emoji, x, y + 7);
|
|
|
|
// Label
|
|
const labelX = cx + Math.sin(p.angle) * (radius + 42);
|
|
const labelY = cy - Math.cos(p.angle) * (radius + 42);
|
|
ctx.fillStyle = 'rgba(232, 232, 240, 0.7)';
|
|
ctx.font = '11px system-ui';
|
|
ctx.fillText(String(p.label), labelX, labelY);
|
|
|
|
ctx.fillStyle = 'rgba(129, 140, 248, 0.7)';
|
|
ctx.font = '9px system-ui';
|
|
ctx.fillText(p.illum, labelX, labelY + 14);
|
|
});
|
|
|
|
// Title
|
|
ctx.fillStyle = '#818cf8';
|
|
ctx.font = 'bold 14px system-ui';
|
|
ctx.textAlign = 'center';
|
|
ctx.fillText('29.53 days', cx, cy + 45);
|
|
ctx.fillStyle = 'rgba(255,255,255,0.4)';
|
|
ctx.font = '10px system-ui';
|
|
ctx.fillText('Synodic Month', cx, cy + 58);
|
|
}, [locale]);
|
|
|
|
const drawTidesChart = useCallback(() => {
|
|
const canvas = tidesCanvasRef.current;
|
|
if (!canvas) return;
|
|
const ctx = canvas.getContext('2d');
|
|
if (!ctx) return;
|
|
|
|
const w = canvas.width;
|
|
const h = canvas.height;
|
|
const pad = { top: 40, right: 20, bottom: 50, left: 50 };
|
|
const chartW = w - pad.left - pad.right;
|
|
const chartH = h - pad.top - pad.bottom;
|
|
|
|
ctx.clearRect(0, 0, w, h);
|
|
ctx.fillStyle = '#0a0a1a';
|
|
ctx.fillRect(0, 0, w, h);
|
|
|
|
// Title
|
|
ctx.fillStyle = '#818cf8';
|
|
ctx.font = 'bold 14px system-ui';
|
|
ctx.textAlign = 'center';
|
|
ctx.fillText(locale === 'fr' ? 'Influence lunaire sur les marées' : 'Lunar Influence on Tides', w / 2, 25);
|
|
|
|
// Axes
|
|
ctx.strokeStyle = 'rgba(255,255,255,0.2)';
|
|
ctx.lineWidth = 1;
|
|
ctx.beginPath();
|
|
ctx.moveTo(pad.left, pad.top);
|
|
ctx.lineTo(pad.left, h - pad.bottom);
|
|
ctx.lineTo(w - pad.right, h - pad.bottom);
|
|
ctx.stroke();
|
|
|
|
// Y-axis label
|
|
ctx.save();
|
|
ctx.translate(15, h / 2);
|
|
ctx.rotate(-Math.PI / 2);
|
|
ctx.fillStyle = 'rgba(255,255,255,0.4)';
|
|
ctx.font = '11px system-ui';
|
|
ctx.textAlign = 'center';
|
|
ctx.fillText(locale === 'fr' ? 'Hauteur des marées (m)' : 'Tide Height (m)', 0, 0);
|
|
ctx.restore();
|
|
|
|
// Generate tide data (29.53 days cycle)
|
|
const days = 30;
|
|
const points: { x: number; y: number }[] = [];
|
|
|
|
for (let d = 0; d <= days; d += 0.5) {
|
|
const phaseAngle = (d / 29.53) * Math.PI * 2;
|
|
// Spring tides at new/full moon (0 and π), neap tides at quarters
|
|
const springNeap = Math.cos(2 * phaseAngle);
|
|
// Semi-diurnal tide variation
|
|
const semiDiurnal = Math.sin(d * Math.PI * 2 * 2);
|
|
const tideHeight = 1.0 + springNeap * 0.8 + semiDiurnal * 0.3;
|
|
|
|
const x = pad.left + (d / days) * chartW;
|
|
const y = pad.top + chartH - (tideHeight / 2.5) * chartH;
|
|
points.push({ x, y });
|
|
}
|
|
|
|
// Fill area under curve
|
|
ctx.beginPath();
|
|
ctx.moveTo(points[0].x, h - pad.bottom);
|
|
points.forEach(p => ctx.lineTo(p.x, p.y));
|
|
ctx.lineTo(points[points.length - 1].x, h - pad.bottom);
|
|
ctx.closePath();
|
|
const fillGrad = ctx.createLinearGradient(0, pad.top, 0, h - pad.bottom);
|
|
fillGrad.addColorStop(0, 'rgba(99, 102, 241, 0.3)');
|
|
fillGrad.addColorStop(1, 'rgba(99, 102, 241, 0.02)');
|
|
ctx.fillStyle = fillGrad;
|
|
ctx.fill();
|
|
|
|
// Line
|
|
ctx.beginPath();
|
|
points.forEach((p, i) => {
|
|
if (i === 0) ctx.moveTo(p.x, p.y);
|
|
else ctx.lineTo(p.x, p.y);
|
|
});
|
|
ctx.strokeStyle = '#818cf8';
|
|
ctx.lineWidth = 2;
|
|
ctx.stroke();
|
|
|
|
// Phase markers
|
|
const phaseMarkers = [
|
|
{ day: 0, emoji: '🌑', label: ts('new_moon', locale) },
|
|
{ day: 7.38, emoji: '🌓', label: ts('first_quarter', locale) },
|
|
{ day: 14.77, emoji: '🌕', label: ts('full_moon', locale) },
|
|
{ day: 22.15, emoji: '🌗', label: ts('last_quarter', locale) },
|
|
{ day: 29.53, emoji: '🌑', label: ts('new_moon', locale) },
|
|
];
|
|
|
|
phaseMarkers.forEach(m => {
|
|
const x = pad.left + (m.day / days) * chartW;
|
|
|
|
// Vertical line
|
|
ctx.strokeStyle = 'rgba(255,255,255,0.1)';
|
|
ctx.setLineDash([3, 3]);
|
|
ctx.beginPath();
|
|
ctx.moveTo(x, pad.top);
|
|
ctx.lineTo(x, h - pad.bottom);
|
|
ctx.stroke();
|
|
ctx.setLineDash([]);
|
|
|
|
// Emoji
|
|
ctx.font = '16px system-ui';
|
|
ctx.textAlign = 'center';
|
|
ctx.fillText(m.emoji, x, h - pad.bottom + 20);
|
|
|
|
// Label
|
|
ctx.fillStyle = 'rgba(255,255,255,0.4)';
|
|
ctx.font = '9px system-ui';
|
|
ctx.fillText(String(m.label).substring(0, 12), x, h - pad.bottom + 38);
|
|
});
|
|
|
|
// Spring/Neap labels
|
|
ctx.fillStyle = 'rgba(251, 191, 36, 0.7)';
|
|
ctx.font = 'bold 10px system-ui';
|
|
const springX1 = pad.left + (0 / days) * chartW + 20;
|
|
ctx.fillText(locale === 'fr' ? 'Vives-eaux' : 'Spring Tide', springX1 + 30, pad.top + 15);
|
|
const springX2 = pad.left + (14.77 / days) * chartW;
|
|
ctx.fillText(locale === 'fr' ? 'Vives-eaux' : 'Spring Tide', springX2, pad.top + 15);
|
|
|
|
ctx.fillStyle = 'rgba(34, 197, 94, 0.7)';
|
|
const neapX1 = pad.left + (7.38 / days) * chartW;
|
|
ctx.fillText(locale === 'fr' ? 'Mortes-eaux' : 'Neap Tide', neapX1, pad.top + 15);
|
|
const neapX2 = pad.left + (22.15 / days) * chartW;
|
|
ctx.fillText(locale === 'fr' ? 'Mortes-eaux' : 'Neap Tide', neapX2, pad.top + 15);
|
|
}, [locale]);
|
|
|
|
useEffect(() => {
|
|
drawPhasesCycle();
|
|
drawTidesChart();
|
|
}, [drawPhasesCycle, drawTidesChart]);
|
|
|
|
return (
|
|
<section id="infographics" aria-label="Lunar Infographics and Charts" className="section-container">
|
|
<div className="text-center mb-10">
|
|
<h2 className="section-title">{ts('infographics_title', locale)}</h2>
|
|
<p className="section-subtitle mx-auto">{ts('infographics_subtitle', locale)}</p>
|
|
</div>
|
|
|
|
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6 max-w-6xl mx-auto">
|
|
<div className="glass-card overflow-hidden">
|
|
<canvas ref={phasesCanvasRef} width={500} height={500} className="w-full h-auto" />
|
|
</div>
|
|
<div className="glass-card overflow-hidden">
|
|
<canvas ref={tidesCanvasRef} width={500} height={350} className="w-full h-auto" />
|
|
</div>
|
|
</div>
|
|
</section>
|
|
);
|
|
}
|