"use client"; import { useThemeMode } from '@/hooks/useThemeMode' import { useEffect, useRef } from "react"; const STAR_TONES = [255, 210, 160, 110, 65]; const WAVE_TONES = [55, 90, 125, 165, 205]; const WAVE_CONFIGS = [ { amp: 26, freq: 0.011, speed: 0.35, phase: 0.0, yBase: 0.38, width: 1.6, toneIdx: 0 }, { amp: 20, freq: 0.015, speed: 0.50, phase: 1.3, yBase: 0.52, width: 1.8, toneIdx: 1 }, { amp: 30, freq: 0.008, speed: 0.28, phase: 2.6, yBase: 0.63, width: 2.2, toneIdx: 2 }, { amp: 16, freq: 0.019, speed: 0.65, phase: 0.8, yBase: 0.74, width: 2.0, toneIdx: 3 }, { amp: 22, freq: 0.013, speed: 0.42, phase: 3.2, yBase: 0.85, width: 2.6, toneIdx: 4 }, ]; interface Star { x: number; y: number; r: number; v: number; baseAlpha: number; phase: number; twinkleSpeed: number; vx: number; vy: number; } interface Wave { amp: number; freq: number; speed: number; phase: number; yBase: number; width: number; v: number; } function initStars(W: number, H: number): Star[] { return Array.from({ length: 180 }, () => { const toneIdx = Math.floor(Math.random() * STAR_TONES.length); return { x: Math.random() * W, y: Math.random() * H, r: 0.4 + Math.random() * 1.6, v: STAR_TONES[toneIdx], baseAlpha: 0.5 + toneIdx * 0.1, phase: Math.random() * Math.PI * 2, twinkleSpeed: 0.8 + Math.random() * 2.0, vx: (Math.random() - 0.5) * 0.3, vy: (Math.random() - 0.5) * 0.15, }; }); } function initWaves(): Wave[] { return WAVE_CONFIGS.map(c => ({ ...c, v: WAVE_TONES[c.toneIdx] })); } function drawStars(ctx: CanvasRenderingContext2D, stars: Star[], W: number, H: number, ga: number) { const now = Date.now() / 1000; for (const s of stars) { s.x += s.vx; s.y += s.vy; if (s.x < 0) s.x = W; if (s.x > W) s.x = 0; if (s.y < 0) s.y = H; if (s.y > H) s.y = 0; const twinkle = 0.35 + 0.65 * ((Math.sin(now * s.twinkleSpeed + s.phase) + 1) / 2); ctx.beginPath(); ctx.arc(s.x, s.y, s.r, 0, Math.PI * 2); ctx.fillStyle = `rgba(${s.v},${s.v},${s.v},${s.baseAlpha * twinkle * ga})`; ctx.fill(); } } function buildWavePts(w: Wave, W: number, H: number, now: number) { const pts: { x: number; y: number }[] = []; for (let x = 0; x <= W; x += 8) { pts.push({ x, y: H * w.yBase + Math.sin(x * w.freq + w.phase + now * w.speed) * w.amp }); } return pts; } function drawWavePath(ctx: CanvasRenderingContext2D, pts: { x: number; y: number }[], W: number, H: number) { ctx.beginPath(); ctx.moveTo(pts[0].x, pts[0].y); for (let i = 0; i < pts.length - 1; i++) { const mx = (pts[i].x + pts[i + 1].x) / 2; const my = (pts[i].y + pts[i + 1].y) / 2; ctx.quadraticCurveTo(pts[i].x, pts[i].y, mx, my); } ctx.lineTo(pts[pts.length - 1].x, pts[pts.length - 1].y); } function drawWaves(ctx: CanvasRenderingContext2D, waves: Wave[], W: number, H: number, ga: number, bgR: number) { const now = Date.now() / 1000; const bg = Math.round(bgR); for (const w of waves) { const v = w.v; const yBase = H * w.yBase; const pts = buildWavePts(w, W, H, now); const grad = ctx.createLinearGradient(0, yBase - w.amp, 0, yBase + w.amp * 3); grad.addColorStop(0, `rgba(${v},${v},${v},${0.030 * ga})`); grad.addColorStop(0.4, `rgba(${v},${v},${v},${0.012 * ga})`); grad.addColorStop(1, `rgba(${bg},${bg},${bg},0)`); drawWavePath(ctx, pts, W, H); ctx.lineTo(W, H); ctx.lineTo(0, H); ctx.closePath(); ctx.fillStyle = grad; ctx.fill(); drawWavePath(ctx, pts, W, H); ctx.strokeStyle = `rgba(${v},${v},${v},${0.85 * ga})`; ctx.lineWidth = w.width; ctx.lineJoin = "round"; ctx.lineCap = "round"; ctx.stroke(); } } // ── Component ──────────────────────────────────────────────────────────────── interface HeroBackgroundProps { /** Controlled from outside if you have a global theme toggle. */ isDark?: boolean; /** Fires when the internal toggle is clicked (omit to hide the button). */ onToggle?: (isDark: boolean) => void; /** Show the built-in toggle button. Defaults to true. */ showToggle?: boolean; } export default function HeroBackground({ isDark: externalIsDark, onToggle, showToggle = true, }: HeroBackgroundProps) { const canvasRef = useRef(null); // REPLACE WITH: const { isDark: hookDark, toggle } = useThemeMode(); // Use external control if provided, otherwise hook const isDark = externalIsDark !== undefined ? externalIsDark : hookDark; // REPLACE WITH: function handleToggle() { toggle(); onToggle?.(!isDark); } useEffect(() => { const canvas = canvasRef.current; if (!canvas) return; const ctx = canvas.getContext("2d"); if (!ctx) return; let W = 0, H = 0; let stars: Star[] = []; let waves: Wave[] = []; let rafId: number; // Animated bg colour let bgR = 0, bgG = 0, bgB = 0; let crossfade = 0; function resize() { W = canvas!.width = canvas!.offsetWidth; H = canvas!.height = canvas!.offsetHeight; stars = initStars(W, H); waves = initWaves(); } function loop() { // Read current target from closure — updated via ref below const dark = darkRef.current; const targetBg = dark ? 0 : 255; const targetCross = dark ? 0 : 1; bgR += (targetBg - bgR) * 0.04; bgG += (targetBg - bgG) * 0.04; bgB += (targetBg - bgB) * 0.04; crossfade += (targetCross - crossfade) * 0.03; ctx!.fillStyle = `rgb(${Math.round(bgR)},${Math.round(bgG)},${Math.round(bgB)})`; ctx!.fillRect(0, 0, W, H); const sa = 1 - crossfade; const wa = crossfade; if (sa > 0.01) drawStars(ctx!, stars, W, H, sa); if (wa > 0.01) drawWaves(ctx!, waves, W, H, wa, bgR); rafId = requestAnimationFrame(loop); } const ro = new ResizeObserver(resize); ro.observe(canvas); resize(); rafId = requestAnimationFrame(loop); return () => { cancelAnimationFrame(rafId); ro.disconnect(); }; }, []); // Keep a ref so the loop closure always reads the latest isDark const darkRef = useRef(isDark); useEffect(() => { darkRef.current = isDark; }, [isDark]); return (
{showToggle && ( )}
); }