7 changed files with 455 additions and 28 deletions
-
2components/sections/AiCopilotSection.tsx
-
2components/sections/HeroDotCanvas.tsx
-
5components/sections/HeroSection.tsx
-
447components/sections/HeroWaves.tsx
-
1components/sections/heroStarlightDots.ts
-
1next.config.ts
-
25public/assets/images/hero_back.svg
@ -0,0 +1,447 @@ |
|||||
|
"use client"; |
||||
|
|
||||
|
import React, { useEffect, useRef } from "react"; |
||||
|
import { STARLIGHT_DOTS_MARKUP } from "./heroStarlightDots"; |
||||
|
|
||||
|
interface HeroWavesProps { |
||||
|
sectionRef?: React.RefObject<HTMLElement | null>; |
||||
|
} |
||||
|
|
||||
|
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<SVGSVGElement>(null); |
||||
|
const pathRefs = [ |
||||
|
useRef<SVGPathElement>(null), |
||||
|
useRef<SVGPathElement>(null), |
||||
|
useRef<SVGPathElement>(null), |
||||
|
useRef<SVGPathElement>(null), |
||||
|
useRef<SVGPathElement>(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 ( |
||||
|
<svg |
||||
|
ref={svgRef} |
||||
|
viewBox="0 0 1856 566" |
||||
|
preserveAspectRatio="xMidYMin slice" |
||||
|
className="absolute inset-0 w-full h-full pointer-events-none select-none z-[1]" |
||||
|
> |
||||
|
<defs> |
||||
|
{/* GPU Gaussian Blur Filters matching original Figma specifications */} |
||||
|
<filter |
||||
|
id="hw_filter1" |
||||
|
x="-300" |
||||
|
y="-50" |
||||
|
width="2500" |
||||
|
height="1000" |
||||
|
filterUnits="userSpaceOnUse" |
||||
|
colorInterpolationFilters="sRGB" |
||||
|
> |
||||
|
<feFlood floodOpacity="0" result="BackgroundImageFix" /> |
||||
|
<feBlend mode="normal" in="SourceGraphic" in2="BackgroundImageFix" result="shape" /> |
||||
|
<feGaussianBlur stdDeviation="55" /> |
||||
|
</filter> |
||||
|
|
||||
|
<filter |
||||
|
id="hw_filter2" |
||||
|
x="-300" |
||||
|
y="-50" |
||||
|
width="2500" |
||||
|
height="1000" |
||||
|
filterUnits="userSpaceOnUse" |
||||
|
colorInterpolationFilters="sRGB" |
||||
|
> |
||||
|
<feFlood floodOpacity="0" result="BackgroundImageFix" /> |
||||
|
<feBlend mode="normal" in="SourceGraphic" in2="BackgroundImageFix" result="shape" /> |
||||
|
<feGaussianBlur stdDeviation="30" /> |
||||
|
</filter> |
||||
|
|
||||
|
<filter |
||||
|
id="hw_filter3" |
||||
|
x="-152" |
||||
|
y="235" |
||||
|
width="2160" |
||||
|
height="640" |
||||
|
filterUnits="userSpaceOnUse" |
||||
|
colorInterpolationFilters="sRGB" |
||||
|
> |
||||
|
<feFlood floodOpacity="0" result="BackgroundImageFix" /> |
||||
|
<feBlend mode="normal" in="SourceGraphic" in2="BackgroundImageFix" result="shape" /> |
||||
|
<feGaussianBlur stdDeviation="60" /> |
||||
|
</filter> |
||||
|
|
||||
|
<filter |
||||
|
id="hw_filter4" |
||||
|
x="-152" |
||||
|
y="285" |
||||
|
width="2160" |
||||
|
height="540" |
||||
|
filterUnits="userSpaceOnUse" |
||||
|
colorInterpolationFilters="sRGB" |
||||
|
> |
||||
|
<feFlood floodOpacity="0" result="BackgroundImageFix" /> |
||||
|
<feBlend mode="normal" in="SourceGraphic" in2="BackgroundImageFix" result="shape" /> |
||||
|
<feGaussianBlur stdDeviation="60" /> |
||||
|
</filter> |
||||
|
|
||||
|
<filter |
||||
|
id="hw_filter5" |
||||
|
x="-152" |
||||
|
y="335" |
||||
|
width="2160" |
||||
|
height="440" |
||||
|
filterUnits="userSpaceOnUse" |
||||
|
colorInterpolationFilters="sRGB" |
||||
|
> |
||||
|
<feFlood floodOpacity="0" result="BackgroundImageFix" /> |
||||
|
<feBlend mode="normal" in="SourceGraphic" in2="BackgroundImageFix" result="shape" /> |
||||
|
<feGaussianBlur stdDeviation="60" /> |
||||
|
</filter> |
||||
|
|
||||
|
{/* Top Fade Gradient Masks for depth blending */} |
||||
|
<linearGradient id="hw_wave2Grad" x1="0" y1="600" x2="0" y2="150" gradientUnits="userSpaceOnUse"> |
||||
|
<stop offset="0" stopColor="white" stopOpacity="1" /> |
||||
|
<stop offset="0.75" stopColor="white" stopOpacity="1" /> |
||||
|
<stop offset="1" stopColor="white" stopOpacity="0" /> |
||||
|
</linearGradient> |
||||
|
<mask id="hw_mask2" maskUnits="userSpaceOnUse" x="-300" y="0" width="2500" height="900"> |
||||
|
<rect x="-300" y="0" width="2500" height="900" fill="url(#hw_wave2Grad)" /> |
||||
|
</mask> |
||||
|
|
||||
|
<linearGradient id="hw_wave3Grad" x1="0" y1="755" x2="0" y2="270" gradientUnits="userSpaceOnUse"> |
||||
|
<stop offset="0" stopColor="white" stopOpacity="1" /> |
||||
|
<stop offset="0.7" stopColor="white" stopOpacity="1" /> |
||||
|
<stop offset="1" stopColor="white" stopOpacity="0" /> |
||||
|
</linearGradient> |
||||
|
<mask id="hw_mask3" maskUnits="userSpaceOnUse" x="-300" y="0" width="2500" height="900"> |
||||
|
<rect x="-300" y="0" width="2500" height="900" fill="url(#hw_wave3Grad)" /> |
||||
|
</mask> |
||||
|
|
||||
|
<linearGradient id="hw_wave4Grad" x1="0" y1="705" x2="0" y2="320" gradientUnits="userSpaceOnUse"> |
||||
|
<stop offset="0" stopColor="white" stopOpacity="1" /> |
||||
|
<stop offset="0.7" stopColor="white" stopOpacity="1" /> |
||||
|
<stop offset="1" stopColor="white" stopOpacity="0" /> |
||||
|
</linearGradient> |
||||
|
<mask id="hw_mask4" maskUnits="userSpaceOnUse" x="-300" y="0" width="2500" height="900"> |
||||
|
<rect x="-300" y="0" width="2500" height="900" fill="url(#hw_wave4Grad)" /> |
||||
|
</mask> |
||||
|
|
||||
|
<linearGradient id="hw_wave5Grad" x1="0" y1="655" x2="0" y2="370" gradientUnits="userSpaceOnUse"> |
||||
|
<stop offset="0" stopColor="white" stopOpacity="1" /> |
||||
|
<stop offset="0.7" stopColor="white" stopOpacity="1" /> |
||||
|
<stop offset="1" stopColor="white" stopOpacity="0" /> |
||||
|
</linearGradient> |
||||
|
<mask id="hw_mask5" maskUnits="userSpaceOnUse" x="-300" y="0" width="2500" height="900"> |
||||
|
<rect x="-300" y="0" width="2500" height="900" fill="url(#hw_wave5Grad)" /> |
||||
|
</mask> |
||||
|
</defs> |
||||
|
|
||||
|
{/* 5 Dynamic Wave Layers */} |
||||
|
{/* Layer 1: Deep Blue Base */} |
||||
|
<g filter="url(#hw_filter1)"> |
||||
|
<path ref={pathRefs[0]} fill="#0029B2" /> |
||||
|
</g> |
||||
|
|
||||
|
{/* Layer 2: Mid-Ocean Blue */} |
||||
|
<g filter="url(#hw_filter2)" mask="url(#hw_mask2)"> |
||||
|
<path ref={pathRefs[1]} fill="#3D6FE5" /> |
||||
|
</g> |
||||
|
|
||||
|
{/* Layer 3: Sky Blue Swell */} |
||||
|
<g filter="url(#hw_filter3)" mask="url(#hw_mask3)"> |
||||
|
<path ref={pathRefs[2]} fill="#4B80FF" /> |
||||
|
</g> |
||||
|
|
||||
|
{/* Layer 4: Atmospheric Luminous Indigo */} |
||||
|
<g |
||||
|
filter="url(#hw_filter4)" |
||||
|
mask="url(#hw_mask4)" |
||||
|
style={{ mixBlendMode: "plus-lighter" }} |
||||
|
> |
||||
|
<path ref={pathRefs[3]} fill="#2D4D99" /> |
||||
|
</g> |
||||
|
|
||||
|
{/* Layer 5: White Core Glow */} |
||||
|
<g filter="url(#hw_filter5)" mask="url(#hw_mask5)"> |
||||
|
<path ref={pathRefs[4]} fill="#FFFFFF" /> |
||||
|
</g> |
||||
|
|
||||
|
{/* Floating Starlight Sparkle Dots */} |
||||
|
<g |
||||
|
className="pointer-events-none select-none" |
||||
|
dangerouslySetInnerHTML={{ __html: STARLIGHT_DOTS_MARKUP }} |
||||
|
/> |
||||
|
</svg> |
||||
|
); |
||||
|
} |
||||
1
components/sections/heroStarlightDots.ts
File diff suppressed because it is too large
View File
File diff suppressed because it is too large
View File
25
public/assets/images/hero_back.svg
File diff suppressed because it is too large
View File
File diff suppressed because it is too large
View File
Write
Preview
Loading…
Cancel
Save
Reference in new issue