diff --git a/src/components/SolarPanel.jsx b/src/components/SolarPanel.jsx index cb98a78..6667631 100644 --- a/src/components/SolarPanel.jsx +++ b/src/components/SolarPanel.jsx @@ -1,13 +1,14 @@ /** * SolarPanel Component - * Cycles between: Solar Image → Solar Indices → X-Ray Flux Chart + * Cycles between: Solar Image → Solar Indices → X-Ray Flux Chart → Lunar Phase */ -import React, { useState, useEffect, useCallback } from 'react'; +import React, { useState, useEffect, useCallback, useMemo } from 'react'; +import { getMoonPhase } from '../utils/geo.js'; -const MODES = ['image', 'indices', 'xray']; -const MODE_LABELS = { image: 'SOLAR', indices: 'SOLAR INDICES', xray: 'X-RAY FLUX' }; -const MODE_ICONS = { image: '📊', indices: '📈', xray: '🖼️' }; -const MODE_TITLES = { image: 'Show solar indices', indices: 'Show X-ray flux', xray: 'Show solar image' }; +const MODES = ['image', 'indices', 'xray', 'lunar']; +const MODE_LABELS = { image: 'SOLAR', indices: 'SOLAR INDICES', xray: 'X-RAY FLUX', lunar: 'LUNAR' }; +const MODE_ICONS = { image: '📊', indices: '📈', xray: '🌙', lunar: '☀️' }; +const MODE_TITLES = { image: 'Show solar indices', indices: 'Show X-ray flux', xray: 'Show lunar phase', lunar: 'Show solar image' }; // Flare class from flux value (W/m²) const getFlareClass = (flux) => { @@ -245,6 +246,161 @@ export const SolarPanel = ({ solarIndices }) => { ); }; + // Lunar phase renderer + const renderLunar = () => { + const now = new Date(); + const phase = getMoonPhase(now); // 0-1, 0=new, 0.5=full + const illumination = Math.round((1 - Math.cos(phase * 2 * Math.PI)) / 2 * 100); + + // Phase name + let phaseName = 'New Moon'; + if (phase >= 0.0625 && phase < 0.1875) phaseName = 'Waxing Crescent'; + else if (phase >= 0.1875 && phase < 0.3125) phaseName = 'First Quarter'; + else if (phase >= 0.3125 && phase < 0.4375) phaseName = 'Waxing Gibbous'; + else if (phase >= 0.4375 && phase < 0.5625) phaseName = 'Full Moon'; + else if (phase >= 0.5625 && phase < 0.6875) phaseName = 'Waning Gibbous'; + else if (phase >= 0.6875 && phase < 0.8125) phaseName = 'Last Quarter'; + else if (phase >= 0.8125 && phase < 0.9375) phaseName = 'Waning Crescent'; + + // Find next full moon & new moon by scanning forward + const findNextPhase = (targetPhase, label) => { + const d = new Date(now); + for (let i = 1; i <= 35; i++) { + d.setDate(d.getDate() + 1); + const p = getMoonPhase(d); + const diff = Math.abs(p - targetPhase); + if (diff < 0.018 || diff > 0.982) { + return d.toLocaleDateString(undefined, { month: 'short', day: 'numeric' }); + } + } + return '—'; + }; + const nextFull = findNextPhase(0.5, 'Full'); + const nextNew = findNextPhase(0.0, 'New'); + + // SVG moon — uses a crescent/gibbous mask technique + // phase 0=new(dark), 0.25=first quarter(right lit), 0.5=full(all lit), 0.75=last quarter(left lit) + const R = 60; // moon radius + const CX = 70, CY = 70; + + // The terminator curve is an ellipse whose x-radius varies with phase + // At new moon (0): fully dark. At full (0.5): fully lit. + // phase 0-0.5: right side lit (waxing), 0.5-1: left side lit (waning) + const angle = phase * 2 * Math.PI; + const terminatorX = R * Math.cos(angle); // ranges from R (new) through 0 (quarter) to -R (full) and back + + // Build the lit area path + // Right half arc (from top to bottom) is always an arc of radius R + // Left boundary (terminator) is an ellipse with rx = |terminatorX| + const buildMoonPath = () => { + // Lit portion: we draw two arcs — the outer limb and the terminator + // For waxing (0 < phase < 0.5): right side is lit + // For waning (0.5 < phase < 1): left side is lit + + if (phase < 0.01 || phase > 0.99) { + // New moon — no lit area + return null; + } + if (phase > 0.49 && phase < 0.51) { + // Full moon — entire circle lit + return `M${CX},${CY - R} A${R},${R} 0 1,1 ${CX},${CY + R} A${R},${R} 0 1,1 ${CX},${CY - R}`; + } + + const absTermX = Math.abs(terminatorX); + + if (phase < 0.5) { + // Waxing — right side lit + // Outer arc: top to bottom along right limb (sweep=1, clockwise) + // Terminator: bottom to top (elliptical arc) + const sweepTerminator = phase < 0.25 ? 1 : 0; // concave before quarter, convex after + return `M${CX},${CY - R} A${R},${R} 0 0,1 ${CX},${CY + R} A${absTermX},${R} 0 0,${sweepTerminator} ${CX},${CY - R}`; + } else { + // Waning — left side lit + // Outer arc: top to bottom along left limb (sweep=0, counter-clockwise) + // Terminator: bottom to top + const sweepTerminator = phase > 0.75 ? 1 : 0; + return `M${CX},${CY - R} A${R},${R} 0 0,0 ${CX},${CY + R} A${absTermX},${R} 0 0,${sweepTerminator} ${CX},${CY - R}`; + } + }; + + const litPath = buildMoonPath(); + + return ( +
+ {/* Moon SVG */} +
+ + + {/* Crater texture */} + + + + + + + + + {/* Clip to circle */} + + + + + + {/* Dark side (always full circle, dark) */} + + + {/* Lit surface with craters — clipped to lit path */} + {litPath && ( + + + {/* Mare (dark patches) */} + + + + {/* Craters */} + + + + + + + )} + + {/* Subtle glow */} + + +
+ + {/* Phase info */} +
+
{phaseName}
+
+ {illumination}% illuminated +
+
+ + {/* Next phases */} +
+
+
🌑 New
+
{nextNew}
+
+
+
🌕 Full
+
{nextFull}
+
+
+
+ ); + }; + return (
{/* Header with cycle button */} @@ -254,8 +410,8 @@ export const SolarPanel = ({ solarIndices }) => { alignItems: 'center', marginBottom: '6px' }}> - - ☀ {MODE_LABELS[mode]} + + {mode === 'lunar' ? '🌙' : '☀'} {MODE_LABELS[mode]}
{mode === 'image' && ( @@ -413,6 +569,9 @@ export const SolarPanel = ({ solarIndices }) => { ) : mode === 'xray' ? ( /* X-Ray Flux Chart View */ renderXrayChart() + ) : mode === 'lunar' ? ( + /* Lunar Phase View */ + renderLunar() ) : ( /* Solar Image View */