diff --git a/app/globals.css b/app/globals.css index 28d95a8..e0ac327 100644 --- a/app/globals.css +++ b/app/globals.css @@ -164,10 +164,10 @@ body { /* 4. Refracting Starlight: Unbroken continuous circular orbit inside star */ @keyframes starSpotlightMotion { 0% { - transform: rotate(0deg) translate(38px, 0) rotate(0deg); + transform: rotate(0deg) translate(15px, 0) rotate(0deg); } 100% { - transform: rotate(360deg) translate(38px, 0) rotate(-360deg); + transform: rotate(360deg) translate(15px, 0) rotate(-360deg); } } @@ -196,10 +196,10 @@ body { /* 7. Liquid Blob continuous drift */ @keyframes blobDriftMotion { 0% { - transform: rotate(0deg) translate(22px, 0) rotate(0deg); + transform: rotate(0deg) translate(15px, 0) rotate(0deg); } 100% { - transform: rotate(360deg) translate(22px, 0) rotate(-360deg); + transform: rotate(360deg) translate(15px, 0) rotate(-360deg); } } @@ -207,11 +207,9 @@ body { @keyframes ambientGlowMotion { 0%, 100% { opacity: 0.75; - transform: scale(1); } 50% { opacity: 0.95; - transform: scale(1.025); } } @@ -260,7 +258,6 @@ body { .animate-orb-color-cycle { animation: orbColorCycleMotion 12s linear infinite; - will-change: filter; } .animate-star-spotlight { @@ -287,8 +284,7 @@ body { .animate-ambient-glow { animation: ambientGlowMotion 6s ease-in-out infinite; - transform-origin: 637.492px 421.5px; - will-change: transform, opacity; + will-change: opacity; } .animate-particles-a { diff --git a/components/copilot/AnimatedCopilotOrb.tsx b/components/copilot/AnimatedCopilotOrb.tsx index a4510f8..fa8bbb7 100644 --- a/components/copilot/AnimatedCopilotOrb.tsx +++ b/components/copilot/AnimatedCopilotOrb.tsx @@ -2,403 +2,319 @@ export function AnimatedCopilotOrb() { return ( - - {/* Ambient Background Glow */} - - - - - - - - - - - +
+ {/* ========================================================================= + LAYER 1: Ambient Background Glow & Core Base Volume (Cached Layer) + Isolated in its own SVG so the 125px Gaussian blurs and inner shadows + are rasterized ONCE into GPU VRAM and never re-evaluated during animation. + ========================================================================= */} + + + {/* Unified ambient background blur filter */} + + + - {/* Main Orb Color-Morphing Entity */} - - {/* Core Sphere (Base Glow & Dark Volume) */} - - - - - - - + {/* Sphere base outer glow */} + + + + + {/* Core sphere inner shadow */} + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + {/* Ambient Glow Paths */} + + + + - {/* Orbiting Energy Ring (Ellipse 760) */} + {/* Base Sphere Glow Rim & Dark Volume */} + + + + + + + + + {/* ========================================================================= + LAYER 2: Dynamic Live Vector Animations + Contains the continuous 360° orbiting ring, breathing energy waves, + drifting liquid blob, rotating star spotlight, and twin contour rings. + ========================================================================= */} + + + {/* Orbiting Ring Drop Shadow Filter */} + + + + + + + + + + + + {/* Liquid Blob Glow Filter - Generous bounds so it never clips during drift */} + + + + + {/* Star Spotlight Blur Filter - Generous bounds so it never clips during orbit */} + + + + + {/* Energy Waves Shared Blur Filter - Generous bounds so it never clips during breathing */} + + + + + {/* Sphere ClipPath: Hardware Stencil/Scissor Buffer (Fast!) */} + + + + + {/* Star Alpha Mask */} + + + + + {/* Linear Gradients */} + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + {/* 1. Orbiting Energy Ring (Smooth 360° Spin on GPU) */} - - - + + + - {/* Masked Spherical Interior */} - - - - - {/* Liquid Internal Glow Blob */} + {/* 2. Masked Spherical Interior (Clipped cleanly to sphere radius 218.5px) */} + + {/* 2A. Liquid Internal Glow Blob (Smooth Circular Drift) */} - - - + + + + + + + + {/* Dynamic Internal Particles (Inside the Orb) */} + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - {/* Central 4-Pointed Star with Refracting Spotlight */} + {/* 2B. Central 4-Pointed Star Body */} + + + {/* 2C. Refracting Spotlight inside the Star (Smooth Radial Shimmer) */} - - - - - - - - - - - - - + + + + + + + - {/* Radiating Flare Energy Waves */} + {/* 2D. Radiating Flare Energy Waves (Smooth Vertical Breathing) */} - - - - - - - - - - - - - - - - - + + + + + + + - {/* Twin Contour Rings */} + {/* 2E. Twin Contour Rings (Crisp Vector 360° Spin) */} - - - - - + + + + + - - - {/* Constellation of 98 Dynamic Particles */} - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - {/* SVG Filters & Linear Gradients */} - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + +
); } diff --git a/components/sections/AiCopilotSection.tsx b/components/sections/AiCopilotSection.tsx index 974ce43..faf8cfa 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 83aaa93..f951b03 100644 --- a/components/sections/HeroDotCanvas.tsx +++ b/components/sections/HeroDotCanvas.tsx @@ -22,7 +22,7 @@ export function HeroDotCanvas({ sectionRef }: HeroDotCanvasProps) { const rowsize = 12; const minX = 6; const maxX = 1854; - const minY = 3; // Grid extends to the top of the card + const minY = 3; const maxY = 555; // Scaling constants per original reference: @@ -56,37 +56,32 @@ export function HeroDotCanvas({ sectionRef }: HeroDotCanvasProps) { let currentStrength = 0; let isHovering = false; let animFrameId: number | null = null; + let isRunning = false; // Sizing & scaling state let width = 0; let height = 0; let scale = 1; let offsetX = 0; + let dpr = 1; - const resize = () => { - const rect = canvas.getBoundingClientRect(); - if (rect.width === 0 || rect.height === 0) return; - - const dpr = window.devicePixelRatio || 1; - width = rect.width; - height = rect.height; - - canvas.width = Math.round(width * dpr); - canvas.height = Math.round(height * dpr); - ctx.setTransform(dpr, 0, 0, dpr, 0, 0); + // Cache rects to prevent layout thrashing on mousemove + let canvasRect: DOMRect | null = null; + let sectionRect: DOMRect | null = null; - // Sizing matches Next.js Image object-cover object-top - scale = height / SVG_HEIGHT; - offsetX = (width - SVG_WIDTH * scale) / 2; + const updateRects = () => { + canvasRect = canvas.getBoundingClientRect(); + const section = sectionRef?.current; + sectionRect = (section || canvas).getBoundingClientRect(); }; - // Load the illuminated bright logo patterns for cursor reaction - const brightLogoImg = new Image(); - let brightLogoLoaded = false; - brightLogoImg.onload = () => { - brightLogoLoaded = true; - }; - brightLogoImg.src = "/assets/images/hero_logo_patterns.svg"; + // Dedicated offscreen canvas for pre-rasterized logo pattern bitmap + let logoCanvas: HTMLCanvasElement | null = null; + let logoCtx: CanvasRenderingContext2D | null = null; + if (typeof document !== "undefined") { + logoCanvas = document.createElement("canvas"); + logoCtx = logoCanvas.getContext("2d"); + } // Dedicated offscreen canvas for pixel-precise radial spotlight on logos let spotCanvas: HTMLCanvasElement | null = null; @@ -104,31 +99,79 @@ export function HeroDotCanvas({ sectionRef }: HeroDotCanvasProps) { ) : null; - const draw = (t: number) => { + // Pre-rasterize the logo pattern once into logoCanvas as GPU bitmap + const renderLogoCanvas = () => { + if (!logoCanvas || !logoCtx || !brightLogoLoaded || width === 0 || height === 0) return; + + logoCanvas.width = Math.round(width * dpr); + logoCanvas.height = Math.round(height * dpr); + logoCtx.setTransform(dpr, 0, 0, dpr, 0, 0); + + logoCtx.clearRect(0, 0, width, height); + logoCtx.drawImage( + brightLogoImg, + offsetX, + 0, + SVG_WIDTH * scale, + SVG_HEIGHT * scale + ); + }; + + // Load the illuminated bright logo patterns for cursor reaction + const brightLogoImg = new Image(); + let brightLogoLoaded = false; + brightLogoImg.onload = () => { + brightLogoLoaded = true; + renderLogoCanvas(); + }; + brightLogoImg.src = "/assets/images/hero_logo_patterns.svg"; + + const resize = () => { + const rect = canvas.getBoundingClientRect(); + if (rect.width === 0 || rect.height === 0) return; + + // Cap DPR at 1.5 to prevent massive 6.5MP buffer overhead on high-DPI screens while keeping crispness + dpr = Math.min(window.devicePixelRatio || 1, 1.5); + width = rect.width; + height = rect.height; + + canvas.width = Math.round(width * dpr); + canvas.height = Math.round(height * dpr); + ctx.setTransform(dpr, 0, 0, dpr, 0, 0); + + scale = height / SVG_HEIGHT; + offsetX = (width - SVG_WIDTH * scale) / 2; + + updateRects(); + renderLogoCanvas(); + draw(); + }; + + const draw = () => { ctx.clearRect(0, 0, width, height); - // 1. Single-pass dot rendering: - // Preserves original look, with dynamic, fast acoustic sound-wave undulation + // 1. Single-pass dot rendering with fast bounding-box skip (avoids 7,000 Math.hypot calls) ctx.fillStyle = "rgba(255, 255, 255, 0.22)"; ctx.beginPath(); + const hasHover = currentStrength > 0.005; + for (let i = 0; i < dots.length; i++) { const dot = dots[i]; - - // Static dot grid (no wave animation on dots per user request) const screenX = dot.x * scale + offsetX; const screenY = dot.y * scale; if (screenX < -10 || screenX > width + 10) continue; let r = dotmin; - if (currentStrength > 0.005) { - const scaler = Math.hypot( - (currentX - dot.x) / rowsize, - (currentY - dot.y) / rowsize - ); - const addedSize = Math.max(0, dotsizebase - dotmin - scaler * decay); - r += addedSize * currentStrength; + if (hasHover) { + const dx = Math.abs(currentX - dot.x); + const dy = Math.abs(currentY - dot.y); + if (dx < maxRadius && dy < maxRadius) { + const scaler = Math.hypot(dx / rowsize, dy / rowsize); + const addedSize = Math.max(0, dotsizebase - dotmin - scaler * decay); + r += addedSize * currentStrength; + } } ctx.moveTo(screenX + r, screenY); @@ -158,8 +201,8 @@ export function HeroDotCanvas({ sectionRef }: HeroDotCanvasProps) { ctx.globalCompositeOperation = "source-over"; - // 4. Subtle hover illumination on dots: - if (DOT_HOVER_BRIGHTNESS > 0.001 && currentStrength > 0.005) { + // 4. Subtle hover illumination on dots + if (DOT_HOVER_BRIGHTNESS > 0.001 && hasHover) { ctx.globalCompositeOperation = "source-atop"; const scX = currentX * scale + offsetX; const scY = currentY * scale; @@ -179,13 +222,12 @@ export function HeroDotCanvas({ sectionRef }: HeroDotCanvasProps) { ctx.globalCompositeOperation = "source-over"; } - // 5. Pixel-precise radial spotlight for TeamBy logo patterns: - if (currentStrength > 0.005 && brightLogoLoaded && spotCanvas && spotCtx) { + // 5. Pixel-precise radial spotlight for TeamBy logo patterns using pre-rasterized bitmap + if (hasHover && brightLogoLoaded && spotCanvas && spotCtx && logoCanvas) { const spotRadiusSvg = maxRadius; const spotRadiusScreen = spotRadiusSvg * scale; const spotSize = Math.ceil(spotRadiusScreen * 2); - const dpr = window.devicePixelRatio || 1; const targetW = Math.round(spotSize * dpr); const targetH = Math.round(spotSize * dpr); @@ -201,13 +243,8 @@ export function HeroDotCanvas({ sectionRef }: HeroDotCanvasProps) { spotCtx.clearRect(0, 0, spotSize, spotSize); spotCtx.save(); spotCtx.translate(spotRadiusScreen - scX, spotRadiusScreen - scY); - spotCtx.drawImage( - brightLogoImg, - offsetX, - 0, - SVG_WIDTH * scale, - SVG_HEIGHT * scale - ); + // Blit from pre-rendered logo bitmap texture (instant GPU copy, no vector SVG re-parsing) + spotCtx.drawImage(logoCanvas, 0, 0, width, height); spotCtx.restore(); const dLeft = Math.hypot((currentX - 531) / 220, (currentY - 194) / 180); @@ -258,7 +295,7 @@ export function HeroDotCanvas({ sectionRef }: HeroDotCanvasProps) { } }; - // Smooth animation loop for subtle acoustic dot waviness and hover reaction + // Demand-driven animation loop: Smoothly animates LERP, fades down to 0, then stops const loop = () => { if (isHovering) { currentX += (targetX - currentX) * LERP_LAG; @@ -270,16 +307,48 @@ export function HeroDotCanvas({ sectionRef }: HeroDotCanvasProps) { currentStrength = 0; } - const t = performance.now() * 0.001; - draw(t); + draw(); + + // When cursor leaves and fade-out is complete: draw clean default state once and SLEEP + if (!isHovering && currentStrength <= 0.001) { + currentStrength = 0; + draw(); + isRunning = false; + animFrameId = null; + return; + } + + // When cursor rests motionless inside section: settle and SLEEP until next movement + if ( + isHovering && + Math.abs(targetX - currentX) < 0.05 && + Math.abs(targetY - currentY) < 0.05 && + Math.abs(targetStrength - currentStrength) < 0.001 + ) { + currentX = targetX; + currentY = targetY; + currentStrength = targetStrength; + draw(); + isRunning = false; + animFrameId = null; + return; + } animFrameId = requestAnimationFrame(loop); }; + const startLoop = () => { + if (!isRunning) { + isRunning = true; + animFrameId = requestAnimationFrame(loop); + } + }; + const handleMouseMove = (e: MouseEvent) => { - const section = sectionRef?.current; - const targetArea = section || canvas; - const sRect = targetArea.getBoundingClientRect(); + if (!sectionRect || !canvasRect) { + updateRects(); + } + const sRect = sectionRect!; if ( e.clientX >= sRect.left && @@ -287,7 +356,7 @@ export function HeroDotCanvas({ sectionRef }: HeroDotCanvasProps) { e.clientY >= sRect.top && e.clientY <= sRect.bottom ) { - const cRect = canvas.getBoundingClientRect(); + const cRect = canvasRect!; targetX = (e.clientX - cRect.left - offsetX) / scale; targetY = (e.clientY - cRect.top) / scale; targetStrength = 1; @@ -297,30 +366,44 @@ export function HeroDotCanvas({ sectionRef }: HeroDotCanvasProps) { currentX = targetX; currentY = targetY; } + + startLoop(); } else if (isHovering) { targetStrength = 0; isHovering = false; + startLoop(); } }; const handleMouseLeave = () => { - targetStrength = 0; - isHovering = false; + if (isHovering) { + targetStrength = 0; + isHovering = false; + startLoop(); + } + }; + + const handleScrollOrResize = () => { + updateRects(); }; resize(); - const ro = new ResizeObserver(() => resize()); + const ro = new ResizeObserver(() => { + resize(); + }); ro.observe(canvas); - animFrameId = requestAnimationFrame(loop); - window.addEventListener("mousemove", handleMouseMove, { passive: true }); window.addEventListener("mouseleave", handleMouseLeave); + window.addEventListener("scroll", handleScrollOrResize, { passive: true }); + window.addEventListener("resize", handleScrollOrResize, { passive: true }); return () => { ro.disconnect(); window.removeEventListener("mousemove", handleMouseMove); window.removeEventListener("mouseleave", handleMouseLeave); + window.removeEventListener("scroll", handleScrollOrResize); + window.removeEventListener("resize", handleScrollOrResize); if (animFrameId) cancelAnimationFrame(animFrameId); }; }, [sectionRef]); diff --git a/components/sections/HeroSection.tsx b/components/sections/HeroSection.tsx index 394b3c5..d40c026 100644 --- a/components/sections/HeroSection.tsx +++ b/components/sections/HeroSection.tsx @@ -26,7 +26,7 @@ export function HeroSection() { className="object-cover object-top select-none pointer-events-none" /> {/* Interactive Gravitational Water Waves */} - {/* */} + {/* Interactive Canvas Dots */} diff --git a/components/sections/HeroWaves.tsx b/components/sections/HeroWaves.tsx index 9f1296c..55f4ea9 100644 --- a/components/sections/HeroWaves.tsx +++ b/components/sections/HeroWaves.tsx @@ -19,6 +19,8 @@ interface LayerConfig { speed2: number; amp2: number; phase: number; + blur: number; + isAdditive?: boolean; } const LAYERS: LayerConfig[] = [ @@ -35,8 +37,9 @@ const LAYERS: LayerConfig[] = [ speed2: -1.1, amp2: 16, phase: 0, + blur: 32, }, - // Layer 2: Mid-Ocean Blue (#3D6FE5, blur 30px, mask 2) + // Layer 2: Mid-Ocean Blue (#3D6FE5, blur 18px, mask 2: y1=600, y2=150) { baseY: 295, maxPull: 95, @@ -49,8 +52,9 @@ const LAYERS: LayerConfig[] = [ speed2: -1.4, amp2: 18, phase: 1.4, + blur: 18, }, - // Layer 3: Sky Blue Swell (#4B80FF, blur 60px, mask 3) + // Layer 3: Sky Blue Swell (#4B80FF, blur 34px, mask 3: y1=755, y2=270) { baseY: 350, maxPull: 90, @@ -63,8 +67,9 @@ const LAYERS: LayerConfig[] = [ speed2: -1.25, amp2: 18, phase: 2.7, + blur: 34, }, - // Layer 4: Luminous Atmosphere Indigo (#2D4D99, plus-lighter, blur 60px, mask 4) + // Layer 4: Luminous Atmosphere Indigo (#2D4D99, plus-lighter, blur 34px, mask 4: y1=705, y2=320) { baseY: 410, maxPull: 75, @@ -77,8 +82,10 @@ const LAYERS: LayerConfig[] = [ speed2: -0.95, amp2: 16, phase: 3.9, + blur: 34, + isAdditive: true, }, - // Layer 5: Pure White Core Light (#FFFFFF, blur 60px, mask 5) + // Layer 5: Pure White Core Light (#FFFFFF, blur 34px, mask 5: y1=655, y2=370) { baseY: 485, maxPull: 55, @@ -91,15 +98,19 @@ const LAYERS: LayerConfig[] = [ 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-calculate X coordinates +// 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; @@ -108,9 +119,10 @@ for (let i = 0; i < NUM_POINTS; i++) { // Pre-allocated buffers for Y coordinates const Y_BUFFERS = Array.from({ length: 5 }, () => new Float64Array(NUM_POINTS)); -function buildSplinePath(xArr: Float64Array, yArr: Float64Array): string { +function drawSplinePath(ctx: CanvasRenderingContext2D, xArr: Float64Array, yArr: Float64Array) { const n = NUM_POINTS; - let d = `M ${xArr[0].toFixed(1)} ${yArr[0].toFixed(1)}`; + ctx.beginPath(); + ctx.moveTo(xArr[0], yArr[0]); for (let i = 0; i < n - 1; i++) { const i0 = i === 0 ? 0 : i - 1; @@ -123,41 +135,82 @@ function buildSplinePath(xArr: Float64Array, yArr: Float64Array): string { 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)}`; + ctx.bezierCurveTo(cp1x, cp1y, cp2x, cp2y, xArr[i2], yArr[i2]); } - d += " L 1888 855 L -32 855 Z"; - return d; + ctx.lineTo(1888, 855); + ctx.lineTo(-32, 855); + ctx.closePath(); } export function HeroWaves({ sectionRef }: HeroWavesProps) { - const svgRef = useRef(null); - const pathRefs = [ - useRef(null), - useRef(null), - useRef(null), - useRef(null), - useRef(null), - ]; + const containerRef = useRef(null); + const canvasRef = useRef(null); useEffect(() => { - const SVG_WIDTH = 1856; - const SVG_HEIGHT = 566; + 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 svg = svgRef.current; - if (!svg) return; - const rect = svg.getBoundingClientRect(); + const rect = container.getBoundingClientRect(); if (rect.width === 0 || rect.height === 0) return; width = rect.width; height = rect.height; - scale = height / SVG_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; @@ -168,6 +221,8 @@ export function HeroWaves({ sectionRef }: HeroWavesProps) { 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; @@ -214,36 +269,69 @@ export function HeroWaves({ sectionRef }: HeroWavesProps) { // 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"; + } - // 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)); + drawSplinePath(ctx, X_COORDS, yBuf); + ctx.fillStyle = cachedFills[i]; + ctx.fill(); + + if (cfg.isAdditive) { + ctx.globalCompositeOperation = "source-over"; } } + + ctx.filter = "none"; }; - const loop = () => { + 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.001) { + } else if (currentStrength > 0.0005) { currentStrength += (0 - currentStrength) * FADE_OUT_LAG; } else { currentStrength = 0; } - const t = performance.now() * 0.001; + const t = now * 0.001; updateWaves(t); - - animFrameId = requestAnimationFrame(loop); + render(); }; const handleMouseMove = (e: MouseEvent) => { const section = sectionRef?.current; - const svg = svgRef.current; - const targetArea = section || svg; + const targetArea = section || container; if (!targetArea) return; const sRect = targetArea.getBoundingClientRect(); @@ -275,173 +363,56 @@ export function HeroWaves({ sectionRef }: HeroWavesProps) { resize(); const ro = new ResizeObserver(() => resize()); - if (svgRef.current) ro.observe(svgRef.current); + 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 ( - - - {/* 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 */} - + + {/* Floating Starlight Sparkle Dots (Static Zero-GPU SVG Overlay) */} + - + ); } diff --git a/public/assets/images/orb/contour1.png b/public/assets/images/orb/contour1.png new file mode 100644 index 0000000..c0bffcd Binary files /dev/null and b/public/assets/images/orb/contour1.png differ diff --git a/public/assets/images/orb/contour2.png b/public/assets/images/orb/contour2.png new file mode 100644 index 0000000..c0bffcd Binary files /dev/null and b/public/assets/images/orb/contour2.png differ diff --git a/public/assets/images/orb/energy_waves.png b/public/assets/images/orb/energy_waves.png new file mode 100644 index 0000000..d7733e4 Binary files /dev/null and b/public/assets/images/orb/energy_waves.png differ diff --git a/public/assets/images/orb/liquid_blob.png b/public/assets/images/orb/liquid_blob.png new file mode 100644 index 0000000..55122a1 Binary files /dev/null and b/public/assets/images/orb/liquid_blob.png differ diff --git a/public/assets/images/orb/ring.png b/public/assets/images/orb/ring.png new file mode 100644 index 0000000..54edc49 Binary files /dev/null and b/public/assets/images/orb/ring.png differ diff --git a/public/assets/images/orb/sphere_base.png b/public/assets/images/orb/sphere_base.png new file mode 100644 index 0000000..a7ca528 Binary files /dev/null and b/public/assets/images/orb/sphere_base.png differ diff --git a/public/assets/images/orb/star.png b/public/assets/images/orb/star.png new file mode 100644 index 0000000..1108e7d Binary files /dev/null and b/public/assets/images/orb/star.png differ