"use client"; import React, { useEffect, useRef } from "react"; import { STARLIGHT_DOTS_MARKUP } from "./heroStarlightDots"; interface HeroWavesProps { sectionRef?: React.RefObject; } interface LayerConfig { baseY: number; maxPull: number; sigma: number; baseline: number; freq1: number; speed1: number; amp1: number; freq2: number; speed2: number; amp2: number; phase: number; blur: number; isAdditive?: boolean; } const LAYERS: LayerConfig[] = [ // Layer 1: Deep Blue Base (#0029B2, blur 55px) { baseY: 255, maxPull: 85, sigma: 290, baseline: 0.22, freq1: 0.0028, speed1: 0.85, amp1: 28, freq2: 0.0055, speed2: -1.1, amp2: 16, phase: 0, blur: 32, }, // Layer 2: Mid-Ocean Blue (#3D6FE5, blur 18px, mask 2: y1=600, y2=150) { baseY: 295, maxPull: 95, sigma: 250, baseline: 0.24, freq1: 0.0033, speed1: 1.15, amp1: 32, freq2: 0.0065, speed2: -1.4, amp2: 18, phase: 1.4, blur: 18, }, // Layer 3: Sky Blue Swell (#4B80FF, blur 34px, mask 3: y1=755, y2=270) { baseY: 350, maxPull: 90, sigma: 270, baseline: 0.23, freq1: 0.0036, speed1: 0.95, amp1: 34, freq2: 0.007, speed2: -1.25, amp2: 18, phase: 2.7, blur: 34, }, // Layer 4: Luminous Atmosphere Indigo (#2D4D99, plus-lighter, blur 34px, mask 4: y1=705, y2=320) { baseY: 410, maxPull: 75, sigma: 280, baseline: 0.2, freq1: 0.003, speed1: 0.75, amp1: 30, freq2: 0.0058, speed2: -0.95, amp2: 16, phase: 3.9, blur: 34, isAdditive: true, }, // Layer 5: Pure White Core Light (#FFFFFF, blur 34px, mask 5: y1=655, y2=370) { baseY: 485, maxPull: 55, sigma: 230, baseline: 0.22, freq1: 0.0034, speed1: 0.8, amp1: 24, freq2: 0.0062, speed2: -1.05, amp2: 12, phase: 0.8, blur: 34, }, ]; const SVG_WIDTH = 1856; const SVG_HEIGHT = 566; const NUM_POINTS = 64; const X_MIN = -32; const X_MAX = 1888; const DX = (X_MAX - X_MIN) / (NUM_POINTS - 1); // Pre-calculated X coordinates const X_COORDS = new Float64Array(NUM_POINTS); for (let i = 0; i < NUM_POINTS; i++) { X_COORDS[i] = X_MIN + i * DX; } // Pre-allocated buffers for Y coordinates const Y_BUFFERS = Array.from({ length: 5 }, () => new Float64Array(NUM_POINTS)); function drawSplinePath(ctx: CanvasRenderingContext2D, xArr: Float64Array, yArr: Float64Array) { const n = NUM_POINTS; ctx.beginPath(); ctx.moveTo(xArr[0], yArr[0]); for (let i = 0; i < n - 1; i++) { const i0 = i === 0 ? 0 : i - 1; const i1 = i; const i2 = i + 1; const i3 = i + 2 >= n ? n - 1 : i + 2; const cp1x = xArr[i1] + (xArr[i2] - xArr[i0]) / 6; const cp1y = yArr[i1] + (yArr[i2] - yArr[i0]) / 6; const cp2x = xArr[i2] - (xArr[i3] - xArr[i1]) / 6; const cp2y = yArr[i2] - (yArr[i3] - yArr[i1]) / 6; ctx.bezierCurveTo(cp1x, cp1y, cp2x, cp2y, xArr[i2], yArr[i2]); } ctx.lineTo(1888, 855); ctx.lineTo(-32, 855); ctx.closePath(); } export function HeroWaves({ sectionRef }: HeroWavesProps) { const containerRef = useRef(null); const canvasRef = useRef(null); useEffect(() => { const container = containerRef.current; const canvas = canvasRef.current; if (!container || !canvas) return; const ctx = canvas.getContext("2d"); if (!ctx) return; let width = 0; let height = 0; let scale = 1; let offsetX = 0; const DPR_SCALE = 0.35; // 0.35x internal resolution eliminates 88% GPU raster cost while blur remains identical let cachedFills: (string | CanvasGradient)[] = []; const updateGradients = () => { // Layer 0: solid deep blue #0029B2 // Layer 1: mid-ocean blue gradient y1=600 to y2=150 const g1 = ctx.createLinearGradient(0, 600, 0, 150); g1.addColorStop(0, "rgba(61, 111, 229, 1)"); g1.addColorStop(0.75, "rgba(61, 111, 229, 1)"); g1.addColorStop(1, "rgba(61, 111, 229, 0)"); // Layer 2: sky blue swell gradient y1=755 to y2=270 const g2 = ctx.createLinearGradient(0, 755, 0, 270); g2.addColorStop(0, "rgba(75, 128, 255, 1)"); g2.addColorStop(0.7, "rgba(75, 128, 255, 1)"); g2.addColorStop(1, "rgba(75, 128, 255, 0)"); // Layer 3: atmospheric indigo gradient y1=705 to y2=320 (lighter blend) const g3 = ctx.createLinearGradient(0, 705, 0, 320); g3.addColorStop(0, "rgba(45, 77, 153, 1)"); g3.addColorStop(0.7, "rgba(45, 77, 153, 1)"); g3.addColorStop(1, "rgba(45, 77, 153, 0)"); // Layer 4: white core light gradient y1=655 to y2=370 const g4 = ctx.createLinearGradient(0, 655, 0, 370); g4.addColorStop(0, "rgba(255, 255, 255, 1)"); g4.addColorStop(0.7, "rgba(255, 255, 255, 1)"); g4.addColorStop(1, "rgba(255, 255, 255, 0)"); cachedFills = ["#0029B2", g1, g2, g3, g4]; }; const resize = () => { const rect = container.getBoundingClientRect(); if (rect.width === 0 || rect.height === 0) return; width = rect.width; height = rect.height; scale = Math.max(width / SVG_WIDTH, height / SVG_HEIGHT); offsetX = (width - SVG_WIDTH * scale) / 2; const targetW = Math.max(1, Math.round(width * DPR_SCALE)); const targetH = Math.max(1, Math.round(height * DPR_SCALE)); if (canvas.width !== targetW || canvas.height !== targetH) { canvas.width = targetW; canvas.height = targetH; } ctx.setTransform(scale * DPR_SCALE, 0, 0, scale * DPR_SCALE, offsetX * DPR_SCALE, 0); updateGradients(); }; let targetX = -1000; let targetY = -1000; let currentX = -1000; let currentY = -1000; let targetStrength = 0; let currentStrength = 0; let isHovering = false; let animFrameId: number | null = null; let isInView = true; let lastDrawTime = 0; const LERP_LAG = 0.08; const FADE_IN_LAG = 0.06; const FADE_OUT_LAG = 0.04; const updateWaves = (t: number) => { for (let layerIdx = 0; layerIdx < LAYERS.length; layerIdx++) { const cfg = LAYERS[layerIdx]; const yBuf = Y_BUFFERS[layerIdx]; // Gravitational attraction proximity: // Strongest when cursor is near the layer's waterline, fading smoothly when far above const distY = Math.abs(currentY - cfg.baseY); const verticalProximity = Math.pow( Math.max(0, Math.min(1, 1 - distY / 380)), 1.3 ); const pull = cfg.maxPull * verticalProximity * currentStrength; for (let i = 0; i < NUM_POINTS; i++) { const x = X_COORDS[i]; const dx = x - currentX; // Gaussian tidal crest centered at cursor X const bell = Math.exp(-(dx * dx) / (2 * cfg.sigma * cfg.sigma)); // Volume conservation: water pulled towards cursor causes surrounding water to recede const tidalFactor = bell - cfg.baseline; const tidalDisplacement = tidalFactor * pull; // Harmonic ambient sea wave motion const yAmbient = cfg.baseY + Math.sin(x * cfg.freq1 + t * cfg.speed1 + cfg.phase) * cfg.amp1 + Math.cos(x * cfg.freq2 + t * cfg.speed2) * cfg.amp2; // Dynamic ripple emanating from the gravitational center const ripple = Math.sin(dx * 0.022 - t * 3.2) * Math.exp(-(dx * dx) / (2 * 160 * 160)) * 5 * verticalProximity * currentStrength; // Pulling upward reduces SVG Y coordinate yBuf[i] = yAmbient - tidalDisplacement - ripple; } } }; const render = () => { // Clear canvas buffer completely ctx.save(); ctx.setTransform(1, 0, 0, 1, 0, 0); ctx.filter = "none"; ctx.clearRect(0, 0, canvas.width, canvas.height); ctx.restore(); for (let i = 0; i < LAYERS.length; i++) { const cfg = LAYERS[i]; const yBuf = Y_BUFFERS[i]; ctx.filter = `blur(${cfg.blur * scale * DPR_SCALE}px)`; if (cfg.isAdditive) { ctx.globalCompositeOperation = "lighter"; } drawSplinePath(ctx, X_COORDS, yBuf); ctx.fillStyle = cachedFills[i]; ctx.fill(); if (cfg.isAdditive) { ctx.globalCompositeOperation = "source-over"; } } ctx.filter = "none"; }; const loop = (now: number) => { animFrameId = requestAnimationFrame(loop); if (!isInView || document.hidden) return; const isActive = isHovering || currentStrength > 0.0005; const minInterval = isActive ? 1000 / 30 : 1000 / 15; // 30fps when active, 15fps when idle ambient const elapsed = now - lastDrawTime; if (elapsed < minInterval) return; lastDrawTime = now - (elapsed % minInterval); if (isHovering) { currentX += (targetX - currentX) * LERP_LAG; currentY += (targetY - currentY) * LERP_LAG; currentStrength += (targetStrength - currentStrength) * FADE_IN_LAG; } else if (currentStrength > 0.0005) { currentStrength += (0 - currentStrength) * FADE_OUT_LAG; } else { currentStrength = 0; } const t = now * 0.001; updateWaves(t); render(); }; const handleMouseMove = (e: MouseEvent) => { const section = sectionRef?.current; const targetArea = section || container; if (!targetArea) return; const sRect = targetArea.getBoundingClientRect(); if ( e.clientX >= sRect.left && e.clientX <= sRect.right && e.clientY >= sRect.top && e.clientY <= sRect.bottom ) { targetX = (e.clientX - sRect.left - offsetX) / scale; targetY = (e.clientY - sRect.top) / scale; targetStrength = 1; isHovering = true; if (currentX < -500) { currentX = targetX; currentY = targetY; } } else if (isHovering) { targetStrength = 0; isHovering = false; } }; const handleMouseLeave = () => { targetStrength = 0; isHovering = false; }; resize(); const ro = new ResizeObserver(() => resize()); ro.observe(container); const io = new IntersectionObserver( ([entry]) => { isInView = entry.isIntersecting; }, { threshold: 0 } ); io.observe(container); animFrameId = requestAnimationFrame(loop); window.addEventListener("mousemove", handleMouseMove, { passive: true }); window.addEventListener("mouseleave", handleMouseLeave); const handleVisibility = () => { if (document.hidden) { lastDrawTime = performance.now(); } }; document.addEventListener("visibilitychange", handleVisibility); return () => { ro.disconnect(); io.disconnect(); window.removeEventListener("mousemove", handleMouseMove); window.removeEventListener("mouseleave", handleMouseLeave); document.removeEventListener("visibilitychange", handleVisibility); if (animFrameId) cancelAnimationFrame(animFrameId); }; }, [sectionRef]); return (
{/* High-Performance Hardware-Accelerated Canvas Waves */} {/* Floating Starlight Sparkle Dots (Static Zero-GPU SVG Overlay) */}
); }