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

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>
);
}