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