"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; } 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, }, // Layer 2: Mid-Ocean Blue (#3D6FE5, blur 30px, mask 2) { 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, }, // Layer 3: Sky Blue Swell (#4B80FF, blur 60px, mask 3) { 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, }, // Layer 4: Luminous Atmosphere Indigo (#2D4D99, plus-lighter, blur 60px, mask 4) { 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, }, // Layer 5: Pure White Core Light (#FFFFFF, blur 60px, mask 5) { 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, }, ]; const NUM_POINTS = 64; const X_MIN = -32; const X_MAX = 1888; const DX = (X_MAX - X_MIN) / (NUM_POINTS - 1); // Pre-calculate 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 buildSplinePath(xArr: Float64Array, yArr: Float64Array): string { const n = NUM_POINTS; let d = `M ${xArr[0].toFixed(1)} ${yArr[0].toFixed(1)}`; 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; d += ` C ${cp1x.toFixed(1)} ${cp1y.toFixed(1)}, ${cp2x.toFixed(1)} ${cp2y.toFixed(1)}, ${xArr[i2].toFixed(1)} ${yArr[i2].toFixed(1)}`; } d += " L 1888 855 L -32 855 Z"; return d; } export function HeroWaves({ sectionRef }: HeroWavesProps) { const svgRef = useRef(null); const pathRefs = [ useRef(null), useRef(null), useRef(null), useRef(null), useRef(null), ]; useEffect(() => { const SVG_WIDTH = 1856; const SVG_HEIGHT = 566; let width = 0; let height = 0; let scale = 1; let offsetX = 0; const resize = () => { const svg = svgRef.current; if (!svg) return; const rect = svg.getBoundingClientRect(); if (rect.width === 0 || rect.height === 0) return; width = rect.width; height = rect.height; scale = height / SVG_HEIGHT; offsetX = (width - SVG_WIDTH * scale) / 2; }; 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; 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; } // Directly mutate DOM path 'd' attribute for maximum 60-120 FPS performance const pathEl = pathRefs[layerIdx].current; if (pathEl) { pathEl.setAttribute("d", buildSplinePath(X_COORDS, yBuf)); } } }; const loop = () => { if (isHovering) { currentX += (targetX - currentX) * LERP_LAG; currentY += (targetY - currentY) * LERP_LAG; currentStrength += (targetStrength - currentStrength) * FADE_IN_LAG; } else if (currentStrength > 0.001) { currentStrength += (0 - currentStrength) * FADE_OUT_LAG; } else { currentStrength = 0; } const t = performance.now() * 0.001; updateWaves(t); animFrameId = requestAnimationFrame(loop); }; const handleMouseMove = (e: MouseEvent) => { const section = sectionRef?.current; const svg = svgRef.current; const targetArea = section || svg; 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()); if (svgRef.current) ro.observe(svgRef.current); animFrameId = requestAnimationFrame(loop); window.addEventListener("mousemove", handleMouseMove, { passive: true }); window.addEventListener("mouseleave", handleMouseLeave); return () => { ro.disconnect(); window.removeEventListener("mousemove", handleMouseMove); window.removeEventListener("mouseleave", handleMouseLeave); if (animFrameId) cancelAnimationFrame(animFrameId); }; }, [sectionRef]); return ( {/* GPU Gaussian Blur Filters matching original Figma specifications */} {/* Top Fade Gradient Masks for depth blending */} {/* 5 Dynamic Wave Layers */} {/* Layer 1: Deep Blue Base */} {/* Layer 2: Mid-Ocean Blue */} {/* Layer 3: Sky Blue Swell */} {/* Layer 4: Atmospheric Luminous Indigo */} {/* Layer 5: White Core Glow */} {/* Floating Starlight Sparkle Dots */} ); }