diff --git a/components/sections/AiCopilotSection.tsx b/components/sections/AiCopilotSection.tsx
index faf8cfa..974ce43 100644
--- a/components/sections/AiCopilotSection.tsx
+++ b/components/sections/AiCopilotSection.tsx
@@ -98,7 +98,7 @@ export function AiCopilotSection() {
{/* Middle Luminous Orb (Figma: 437x437 centered at x=608, y=218.5) */}
diff --git a/components/sections/HeroDotCanvas.tsx b/components/sections/HeroDotCanvas.tsx
index bc1d22c..83aaa93 100644
--- a/components/sections/HeroDotCanvas.tsx
+++ b/components/sections/HeroDotCanvas.tsx
@@ -328,7 +328,7 @@ export function HeroDotCanvas({ sectionRef }: HeroDotCanvasProps) {
return (
);
}
diff --git a/components/sections/HeroSection.tsx b/components/sections/HeroSection.tsx
index 580d7a2..394b3c5 100644
--- a/components/sections/HeroSection.tsx
+++ b/components/sections/HeroSection.tsx
@@ -4,6 +4,7 @@ import Image from "next/image";
import Link from "next/link";
import { useRef } from "react";
import { HeroDotCanvas } from "./HeroDotCanvas";
+import { HeroWaves } from "./HeroWaves";
export function HeroSection() {
const sectionRef = useRef(null);
@@ -17,13 +18,15 @@ export function HeroSection() {
{/* Background Graphic & Curve (back_4178_60221: x: 32, y: 230, w: 1856, h: 566) */}
+ {/* Interactive Gravitational Water Waves */}
+ {/* */}
{/* Interactive Canvas Dots */}
diff --git a/components/sections/HeroWaves.tsx b/components/sections/HeroWaves.tsx
new file mode 100644
index 0000000..9f1296c
--- /dev/null
+++ b/components/sections/HeroWaves.tsx
@@ -0,0 +1,447 @@
+"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 (
+
+ );
+}
diff --git a/components/sections/heroStarlightDots.ts b/components/sections/heroStarlightDots.ts
new file mode 100644
index 0000000..f037c14
--- /dev/null
+++ b/components/sections/heroStarlightDots.ts
@@ -0,0 +1 @@
+export const STARLIGHT_DOTS_MARKUP = "\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r";
diff --git a/next.config.ts b/next.config.ts
index 68a6c64..963de36 100644
--- a/next.config.ts
+++ b/next.config.ts
@@ -2,6 +2,7 @@ import type { NextConfig } from "next";
const nextConfig: NextConfig = {
output: "standalone",
+ allowedDevOrigins: ["192.168.1.64"],
};
export default nextConfig;
diff --git a/public/assets/images/hero_back.svg b/public/assets/images/hero_back.svg
index 1f9ab13..56ab682 100644
--- a/public/assets/images/hero_back.svg
+++ b/public/assets/images/hero_back.svg
@@ -1015,31 +1015,6 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-