/** * Wave Hero Section Template * * A hero section with animated wave bars using Three.js instanced rendering. * Features bloom post-processing, film grain, and mouse-reactive glow effects. * * Dependencies: * - three * - gsap */ 'use client'; import React, { useEffect, useRef, useState } from 'react'; import * as THREE from 'three'; import { EffectComposer } from 'three/addons/postprocessing/EffectComposer.js'; import { RenderPass } from 'three/addons/postprocessing/RenderPass.js'; import { UnrealBloomPass } from 'three/addons/postprocessing/UnrealBloomPass.js'; import { ShaderPass } from 'three/addons/postprocessing/ShaderPass.js'; import gsap from 'gsap'; // ============================================ // TYPES // ============================================ export interface WaveHeroProps { className?: string; style?: React.CSSProperties; extendLeftPx?: number; title?: string; subtitle?: string; placeholder?: string; buttonText?: string; onPromptSubmit?: (value: string) => void; children?: React.ReactNode; } // ============================================ // FILM GRAIN SHADER // ============================================ const FilmGrainShader = { uniforms: { tDiffuse: { value: null as THREE.Texture | null }, time: { value: 0 }, intensity: { value: 1.1 }, grainScale: { value: 0.5 }, }, vertexShader: ` varying vec2 vUv; void main() { vUv = uv; gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0); } `, fragmentShader: ` #ifdef GL_ES precision highp int; precision mediump float; #else precision mediump float; #endif uniform sampler2D tDiffuse; uniform float time; uniform float intensity; uniform float grainScale; varying vec2 vUv; float sparkleNoise(vec2 p) { vec2 jPos = p + vec2(37.0, 17.0) * fract(time * 0.07); vec3 p3 = fract(vec3(jPos.xyx) * vec3(.1031, .1030, .0973) + time * 0.1); p3 += dot(p3, p3.yxz + 19.19); return fract((p3.x + p3.y) * p3.z); } void main() { vec4 color = texture2D(tDiffuse, vUv); vec2 pos = gl_FragCoord.xy * 0.5 * grainScale; float noise = sparkleNoise(pos); noise = noise * 2.0 - 1.0; vec3 result = color.rgb + noise * intensity * 0.1; gl_FragColor = vec4(result, color.a); } `, }; function createFilmGrainPass(intensity = 0.9, grainScale = 0.3) { const pass = new ShaderPass(FilmGrainShader as any); (pass.uniforms as any).intensity.value = intensity; (pass.uniforms as any).grainScale.value = grainScale; return pass; } // ============================================ // BAR MATERIAL SHADER // ============================================ function createInstancedMaterial() { const baseCol = new THREE.Color('hsl(220,100%,50%)'); const emisCol = new THREE.Color('#1f3dbc'); return new THREE.ShaderMaterial({ defines: { USE_INSTANCING: '' }, uniforms: { uMouseClipX: { value: 0 }, uHalfW: { value: 0 }, uMaxGlowDist: { value: 690 }, uGlowFalloff: { value: 0.6 }, uSmoothSpeed: { value: 0 }, uGainMul: { value: 1 }, uBaseY: { value: 0 }, w1Gain: { value: 10 }, w1Len: { value: 0.5 }, w1Phase: { value: 0 }, w2Gain: { value: 0 }, w2Len: { value: 0.5 }, w2Phase: { value: 0 }, uFixedTipPx: { value: 10 }, uMinBottomWidthPx: { value: 0 }, uColor: { value: baseCol }, uEmissive: { value: emisCol }, uBaseEmissive: { value: 0.05 }, uRotationAngle: { value: THREE.MathUtils.degToRad(23.4) }, }, vertexShader: ` attribute float aXPos, aPosNorm, aGroup, aGlow; uniform float uMouseClipX, uHalfW, uMaxGlowDist, uGlowFalloff; uniform float uGainMul, uBaseY; uniform float w1Gain, w1Len, w1Phase; uniform float w2Gain, w2Len, w2Phase; uniform float uRotationAngle; varying float vGlow, vPulse, vHeight; varying vec2 vUv; float sineH(float g, float len, float ph, float t){ return max(20.0, (sin(ph + t * len) * 0.5 + 0.6) * g * uGainMul); } void main(){ vUv = uv; float h1 = sineH(w1Gain, w1Len, w1Phase, aPosNorm); float h2 = sineH(w2Gain, w2Len, w2Phase, aPosNorm); vHeight = mix(h1, h2, aGroup); vec3 pos = position; pos.x += aXPos; pos.y = 0.0; float height = vHeight * uv.y; pos.x += height * tan(uRotationAngle); pos.y += height; pos.y += uBaseY; vec4 clip = projectionMatrix * modelViewMatrix * vec4(pos,1.0); float dxPx = abs(uMouseClipX - clip.x/clip.w) * uHalfW; float prox = clamp(1.0 - pow(dxPx / uMaxGlowDist, uGlowFalloff), 0.0, 1.0); vGlow = aGlow; vPulse = prox; gl_Position = clip; } `, fragmentShader: ` #ifdef GL_ES precision highp int; precision mediump float; #else precision mediump float; #endif uniform vec3 uColor, uEmissive; uniform float uBaseEmissive; uniform float uFixedTipPx, uMinBottomWidthPx; varying float vGlow, vPulse, vHeight; varying vec2 vUv; void main(){ float tipProp = clamp(uFixedTipPx / vHeight, 0.0, 0.95); float transitionY = 1.0 - tipProp; float xFromCenter = abs(vUv.x - 0.5) * 2.0; float px = fwidth(vUv.x); float allowedWidth; if (vUv.y >= transitionY){ float topPos = (vUv.y - transitionY) / tipProp; allowedWidth = 1.0 - pow(topPos, 0.9); } else { float bottomPos = vUv.y / transitionY; allowedWidth = max(uMinBottomWidthPx * px * 10.0, pow(bottomPos, 0.5)); } float alpha = smoothstep(-px, px, allowedWidth - xFromCenter); if (alpha < 0.01) discard; float emissiveStrength = uBaseEmissive + vGlow * 0.9 + vPulse * 0.15; vec3 finalColor = uColor + uEmissive * emissiveStrength; gl_FragColor = vec4(finalColor, 0.35 * alpha); } `, side: THREE.FrontSide, transparent: true, depthWrite: false, blending: THREE.AdditiveBlending, }); } // ============================================ // WAVE KEYFRAMES // ============================================ const waveKeyframes1 = [ { time: 0, gain: 10, frequency: 0, waveLength: 0.5 }, { time: 4, gain: 300, frequency: 1, waveLength: 0.5 }, { time: 6, gain: 300, frequency: 4, waveLength: Math.PI * 1.5 }, { time: 8, gain: 225, frequency: 4, waveLength: Math.PI * 1.5 }, { time: 10, gain: 500, frequency: 1, waveLength: Math.PI * 1.5 }, { time: 14, gain: 225, frequency: 3, waveLength: Math.PI * 1.5 }, { time: 22, gain: 100, frequency: 6, waveLength: Math.PI * 1.5 }, { time: 28, gain: 0, frequency: 0.9, waveLength: 0.5 }, { time: 30, gain: 128, frequency: 0.9, waveLength: 0.5 }, { time: 32, gain: 190, frequency: 1.42, waveLength: 0.5 }, { time: 39, gain: 499, frequency: 4.0, waveLength: Math.PI * 1.5 }, { time: 40, gain: 500, frequency: 4.0, waveLength: Math.PI * 1.5 }, { time: 42, gain: 400, frequency: 2.82, waveLength: Math.PI * 1.5 }, { time: 44, gain: 327, frequency: 2.56, waveLength: Math.PI * 1.5 }, { time: 48, gain: 188, frequency: 5.4, waveLength: 0.5 }, { time: 52, gain: 32, frequency: 0.1, waveLength: 0.5 }, { time: 55, gain: 10, frequency: 0, waveLength: 0.5 }, ]; const waveKeyframes2 = [ { time: 0, gain: 0, frequency: 0, waveLength: 0.5 }, { time: 9, gain: 0, frequency: 0, waveLength: 0.5 }, { time: 10, gain: 400, frequency: 1, waveLength: 0.5 }, { time: 13, gain: 300, frequency: 4, waveLength: Math.PI * 1.5 }, { time: 24, gain: 96, frequency: 2, waveLength: 0.5 }, { time: 28, gain: 0, frequency: 0.9, waveLength: 0.5 }, { time: 30, gain: 142, frequency: 0.9, waveLength: 0.5 }, { time: 36, gain: 374, frequency: 4.0, waveLength: Math.PI * 1.5 }, { time: 38, gain: 375, frequency: 4.0, waveLength: Math.PI * 1.5 }, { time: 40, gain: 300, frequency: 2.26, waveLength: Math.PI * 1.5 }, { time: 44, gain: 245, frequency: 2.05, waveLength: Math.PI * 1.5 }, { time: 48, gain: 141, frequency: 5.12, waveLength: 0.5 }, { time: 52, gain: 24, frequency: 0.08, waveLength: 0.5 }, { time: 55, gain: 8, frequency: 0, waveLength: 0.5 }, ]; // ============================================ // TYPING PLACEHOLDER HOOK // ============================================ function useTypingPlaceholder(prompt: string, basePlaceholder: string) { const suggestionsRef = useRef([ ' fitness app', ' recipe generator', ' marketing landing page', ' travel itinerary planner', ' blog engine', ' customer support chatbot', ' personal finance dashboard', ]); const [animatedPlaceholder, setAnimatedPlaceholder] = useState(basePlaceholder); const typingStateRef = useRef({ suggestionIndex: 0, charIndex: 0, deleting: false, running: true, }); const timersRef = useRef([]); useEffect(() => { typingStateRef.current.running = true; const typeSpeed = 70; const deleteSpeed = 40; const pauseAtEnd = 1200; const pauseBetween = 500; function schedule(fn: () => void, delay: number) { const id = window.setTimeout(fn, delay); timersRef.current.push(id); } function clearTimers() { for (const id of timersRef.current) window.clearTimeout(id); timersRef.current = []; } function step() { if (!typingStateRef.current.running) return; if (prompt !== '') { setAnimatedPlaceholder(basePlaceholder); schedule(step, 300); return; } const state = typingStateRef.current; const suggestions = suggestionsRef.current; const current = suggestions[state.suggestionIndex % suggestions.length] || ''; if (!state.deleting) { const nextIndex = state.charIndex + 1; const next = current.slice(0, nextIndex); setAnimatedPlaceholder(basePlaceholder + next); state.charIndex = nextIndex; if (nextIndex >= current.length) { schedule(() => { state.deleting = true; step(); }, pauseAtEnd); } else { schedule(step, typeSpeed); } } else { const nextIndex = Math.max(0, state.charIndex - 1); const next = current.slice(0, nextIndex); setAnimatedPlaceholder(basePlaceholder + next); state.charIndex = nextIndex; if (nextIndex <= 0) { state.deleting = false; state.suggestionIndex = (state.suggestionIndex + 1) % suggestions.length; schedule(step, pauseBetween); } else { schedule(step, deleteSpeed); } } } clearTimers(); schedule(step, 400); return () => { typingStateRef.current.running = false; clearTimers(); }; }, [prompt, basePlaceholder]); return animatedPlaceholder; } // ============================================ // COMPONENT // ============================================ export function WaveHero({ className, style, extendLeftPx = 320, title = 'Build with AI.', subtitle = 'The AI Fullstack Engineer. Build prototypes, apps, and websites', placeholder = 'Describe what you want to create...', buttonText = 'Generate', onPromptSubmit, children, }: WaveHeroProps) { const [prompt, setPrompt] = useState(''); const containerRef = useRef(null); const waveRef = useRef(null); const basePlaceholder = 'Make me a'; const animatedPlaceholder = useTypingPlaceholder(prompt, basePlaceholder); useEffect(() => { if (!containerRef.current || !waveRef.current) return; // Wave state const wave1 = { gain: 10, frequency: 0, waveLength: 0.5, currentAngle: 0 }; const wave2 = { gain: 0, frequency: 0, waveLength: 0.5, currentAngle: 0 }; // Mouse & glow config const mouse = { x: 0, y: 0, active: false }; let proxyMouseX = 0, proxyMouseY = 0, proxyInitialized = false; const glowConfig = { maxGlowDistance: 690, speedScale: 0.52, fadeSpeed: 4.4, glowFalloff: 0.6, mouseSmoothing: 30.0, }; const glowDynamics = { accumulation: 1.2, decay: 3.3, max: 40.0, accumEase: 1.5, speedEase: 8.5, }; // THREE.js setup let DPR_CAP = 2; const mm = gsap.matchMedia(); mm.add('(max-resolution: 180dpi)', () => { DPR_CAP = 1.5; }); const EFFECT_PR = Math.min(window.devicePixelRatio, DPR_CAP) * 0.5; const waveContainer = waveRef.current!; while (waveContainer.firstChild) { waveContainer.removeChild(waveContainer.firstChild); } const waveRenderer = new THREE.WebGLRenderer({ antialias: false, alpha: true }); waveRenderer.setPixelRatio(EFFECT_PR); waveRenderer.toneMapping = THREE.ACESFilmicToneMapping; waveRenderer.toneMappingExposure = 1.0; waveRenderer.autoClear = false; waveContainer.appendChild(waveRenderer.domElement); const waveScene = new THREE.Scene(); waveScene.fog = null as any; waveScene.add(new THREE.AmbientLight(0xffffff, 0.2)); let waveCamera: THREE.OrthographicCamera; let waveComposer: EffectComposer; let waveBloomPass: UnrealBloomPass; let grainPass: ShaderPass; let cameraWidth = 0, cameraHeight = 0, waveCameraInitialized = false; let setMouseNDC: (v: number) => void, setSmoothSpeed: (v: number) => void, setPhase1: (v: number) => void, setPhase2: (v: number) => void; // Bar instancing const MAX_BARS = 256; const FIXED_BAR_WIDTH = 14; const FIXED_BAR_GAP = 10; const EXTEND_LEFT_PX = extendLeftPx; let instancedBars: THREE.InstancedMesh | null = null; let currentBarCount = 0; let barMaterial: THREE.ShaderMaterial; let barCenters: Float32Array | null = null; function updateGlowDistance() { if (!barMaterial) return; const totalWidth = currentBarCount * (FIXED_BAR_WIDTH + FIXED_BAR_GAP) - FIXED_BAR_GAP; const spanPx = totalWidth * 0.3; glowConfig.maxGlowDistance = spanPx; (barMaterial.uniforms as any).uMaxGlowDist.value = spanPx; } function setupQuickSetters() { const u = (instancedBars!.material as THREE.ShaderMaterial).uniforms as any; setMouseNDC = gsap.quickSetter(u.uMouseClipX, 'value') as any; setSmoothSpeed = gsap.quickSetter(u.uSmoothSpeed, 'value') as any; setPhase1 = gsap.quickSetter(u.w1Phase, 'value') as any; setPhase2 = gsap.quickSetter(u.w2Phase, 'value') as any; } const MAX_KEYFRAME_GAIN = 500; const SCREEN_COVERAGE = 0.6; function updateGainMultiplier() { if (!barMaterial) return; const targetPx = cameraHeight * SCREEN_COVERAGE; (barMaterial.uniforms as any).uGainMul.value = targetPx / MAX_KEYFRAME_GAIN; } let rect = { left: 0, top: 0 }; const listeners: Array<() => void> = []; function setupPointerTracking() { const el = waveRenderer.domElement; const readCoords = (e: PointerEvent | TouchEvent): { x: number; y: number } => { if ('clientX' in e) return { x: (e as PointerEvent).clientX, y: (e as PointerEvent).clientY }; const t = (e as TouchEvent).touches?.[0] || (e as TouchEvent).changedTouches?.[0]; return t ? { x: t.clientX, y: t.clientY } : { x: mouse.x, y: mouse.y }; }; const updatePos = (e: any, active: boolean) => { const { x, y } = readCoords(e); const r = rect; mouse.x = x - r.left; mouse.y = y - r.top; mouse.active = active; if (!proxyInitialized) { proxyMouseX = mouse.x; proxyMouseY = mouse.y; proxyInitialized = true; } }; const activate = (e: any) => updatePos(e, true); const move = (e: any) => updatePos(e, true); const deactivate = () => { mouse.active = false; }; el.addEventListener('pointerdown', activate, { passive: true }); el.addEventListener('pointermove', move, { passive: true }); window.addEventListener('pointerup', deactivate as any, { passive: true }); el.addEventListener('pointerleave', deactivate as any, { passive: true }); el.addEventListener('touchstart', activate as any, { passive: true }); el.addEventListener('touchmove', move as any, { passive: true }); window.addEventListener('touchend', deactivate as any, { passive: true }); window.addEventListener('touchcancel', deactivate as any, { passive: true }); listeners.push(() => { el.removeEventListener('pointerdown', activate as any); el.removeEventListener('pointermove', move as any); window.removeEventListener('pointerup', deactivate as any); el.removeEventListener('pointerleave', deactivate as any); el.removeEventListener('touchstart', activate as any); el.removeEventListener('touchmove', move as any); window.removeEventListener('touchend', deactivate as any); window.removeEventListener('touchcancel', deactivate as any); }); } let smoothSpeed = 0; function accumulateGlow(dt: number) { if (!instancedBars) return; const attr = instancedBars.geometry.getAttribute('aGlow') as THREE.InstancedBufferAttribute; const arr = attr.array as Float32Array; const mouseWorldX = proxyMouseX - cameraWidth * 0.5; const mDist = glowConfig.maxGlowDistance; const fall = glowConfig.glowFalloff; const decayLerp = 1.0 - Math.exp(-glowDynamics.decay * dt); const addEase = 1.0 - Math.exp(-glowDynamics.accumEase * dt); const vmax = glowDynamics.max; for (let i = 0; i < currentBarCount; i++) { const dx = Math.abs(mouseWorldX - (barCenters as Float32Array)[i]); const hit = dx < mDist ? 1.0 - Math.pow(dx / mDist, fall) : 0.0; const targetAdd = hit * smoothSpeed; const add = targetAdd * addEase; let g = (arr as any)[i] + add - (arr as any)[i] * decayLerp; if (g > vmax) g = vmax; (arr as any)[i] = (arr as any)[i + currentBarCount] = g; } (attr as any).needsUpdate = true; } function createInstancedBars() { if (instancedBars) { waveScene.remove(instancedBars); instancedBars.geometry.dispose(); (instancedBars.material as any).dispose(); instancedBars = null; } const waveWidth = cameraWidth; const span = waveWidth + EXTEND_LEFT_PX; let barCount = Math.min(MAX_BARS, Math.max(1, Math.floor((span + FIXED_BAR_GAP) / (FIXED_BAR_WIDTH + FIXED_BAR_GAP)))); const gap = barCount > 1 ? (span - barCount * FIXED_BAR_WIDTH) / (barCount - 1) : 0; currentBarCount = barCount; const startX = -waveWidth / 2 - EXTEND_LEFT_PX; const instCnt = barCount * 2; barCenters = new Float32Array(barCount); const aXPos = new Float32Array(instCnt); const aPosNorm = new Float32Array(instCnt); const aGroup = new Float32Array(instCnt); const aGlow = new Float32Array(instCnt).fill(0); for (let i = 0; i < barCount; i++) { const x = startX + FIXED_BAR_WIDTH / 2 + i * (FIXED_BAR_WIDTH + gap); (barCenters as Float32Array)[i] = x; const t = barCount > 1 ? i / (barCount - 1) : 0; aXPos[i] = x; aXPos[i + barCount] = x; aPosNorm[i] = t; aPosNorm[i + barCount] = t; aGroup[i] = 0; aGroup[i + barCount] = 1; } const geo = new THREE.PlaneGeometry(FIXED_BAR_WIDTH, 1, 1, 1); geo.translate(0, 0.5, 0); geo.setAttribute('aXPos', new THREE.InstancedBufferAttribute(aXPos, 1)); geo.setAttribute('aPosNorm', new THREE.InstancedBufferAttribute(aPosNorm, 1)); geo.setAttribute('aGroup', new THREE.InstancedBufferAttribute(aGroup, 1)); geo.setAttribute('aGlow', new THREE.InstancedBufferAttribute(aGlow, 1).setUsage(THREE.DynamicDrawUsage)); barMaterial = createInstancedMaterial(); instancedBars = new THREE.InstancedMesh(geo, barMaterial, instCnt); instancedBars.frustumCulled = false; waveScene.add(instancedBars); setupQuickSetters(); updateGlowDistance(); } function buildKeyframeTweens(target: any, keyframes: Array) { const tl = gsap.timeline(); for (let i = 0; i < keyframes.length - 1; i++) { const cur = keyframes[i]; const nxt = keyframes[i + 1]; const duration = nxt.time - cur.time; tl.to(target, { gain: nxt.gain, frequency: nxt.frequency, waveLength: nxt.waveLength, duration, ease: 'power2.inOut' }, cur.time); } return tl; } function initWaveThree() { cameraWidth = waveContainer.clientWidth; cameraHeight = waveContainer.clientHeight; waveCamera = new THREE.OrthographicCamera(-cameraWidth / 2, cameraWidth / 2, cameraHeight / 2, -cameraHeight / 2, -1000, 1000); waveCamera.position.z = 10; waveCamera.lookAt(0, 0, 0); waveRenderer.setSize(cameraWidth, cameraHeight); waveComposer = new EffectComposer(waveRenderer); (waveComposer as any).setPixelRatio(EFFECT_PR); const waveRenderPass = new RenderPass(waveScene, waveCamera); waveComposer.addPass(waveRenderPass); waveBloomPass = new UnrealBloomPass(new THREE.Vector2(cameraWidth, cameraHeight), 1.0, 0.68, 0.0); (waveBloomPass as any).resolution.set(cameraWidth * 0.5, cameraHeight * 0.5); waveComposer.addPass(waveBloomPass); grainPass = createFilmGrainPass(); waveComposer.addPass(grainPass); createInstancedBars(); setupPointerTracking(); updateGainMultiplier(); waveCameraInitialized = true; } let pendingW = 0, pendingH = 0, heavyResizeTimer: any = null; function onResize(newW: number, newH: number) { if (!waveCameraInitialized) return; pendingW = newW; pendingH = newH; cameraWidth = newW; cameraHeight = newH; waveCamera.left = -cameraWidth / 2; waveCamera.right = cameraWidth / 2; waveCamera.top = cameraHeight / 2; waveCamera.bottom = -cameraHeight / 2; waveCamera.updateProjectionMatrix(); const waveWidth = cameraWidth; const span = waveWidth + EXTEND_LEFT_PX; let barCount = Math.min(MAX_BARS, Math.max(1, Math.floor((span + FIXED_BAR_GAP) / (FIXED_BAR_WIDTH + FIXED_BAR_GAP)))); const gap = barCount > 1 ? (span - barCount * FIXED_BAR_WIDTH) / (barCount - 1) : 0; if (barCount !== currentBarCount) { currentBarCount = barCount; createInstancedBars(); } else { const startX = -waveWidth / 2 - EXTEND_LEFT_PX; const aX = instancedBars!.geometry.getAttribute('aXPos') as THREE.InstancedBufferAttribute; const aT = instancedBars!.geometry.getAttribute('aPosNorm') as THREE.InstancedBufferAttribute; for (let i = 0; i < barCount; i++) { const x = startX + FIXED_BAR_WIDTH / 2 + i * (FIXED_BAR_WIDTH + gap); const t = barCount > 1 ? i / (barCount - 1) : 0; (aX.array as any)[i] = (aX.array as any)[i + barCount] = x; (aT.array as any)[i] = (aT.array as any)[i + barCount] = t; } aX.needsUpdate = true; aT.needsUpdate = true; } (barMaterial.uniforms as any).uHalfW.value = cameraWidth * 0.5; updateGainMultiplier(); updateGlowDistance(); clearTimeout(heavyResizeTimer); heavyResizeTimer = setTimeout(applyHeavyResize, 10); rect = waveRenderer.domElement.getBoundingClientRect(); } function applyHeavyResize() { heavyResizeTimer = null; waveRenderer.setPixelRatio(EFFECT_PR); waveRenderer.setSize(pendingW, pendingH); (waveComposer as any).setSize(pendingW, pendingH); (waveBloomPass as any)?.setSize(pendingW, pendingH); (grainPass as any)?.setSize(pendingW, pendingH); (grainPass.uniforms as any).grainScale.value = 0.5; } function disposeWaveScene() { gsap.globalTimeline.clear(); waveScene.traverse((obj: any) => { if (obj.isMesh) { obj.geometry.dispose(); if (Array.isArray(obj.material)) { obj.material.forEach((m: any) => m.dispose()); } else { obj.material.dispose(); } } }); (grainPass as any)?.dispose?.(); (waveBloomPass as any)?.dispose?.(); (waveComposer as any)?.dispose?.(); (waveRenderer as any)?.dispose?.(); instancedBars = null; } const ticker = () => { if (!waveCameraInitialized || !instancedBars) return; const dt = (gsap.ticker.deltaRatio() as number) * (1 / 60); wave1.currentAngle = (wave1.currentAngle + wave1.frequency * dt) % (Math.PI * 2); wave2.currentAngle = (wave2.currentAngle + wave2.frequency * dt) % (Math.PI * 2); setPhase1(wave1.currentAngle); setPhase2(wave2.currentAngle); const kMouse = 1.0 - Math.exp(-glowConfig.mouseSmoothing * dt); proxyMouseX += (mouse.x - proxyMouseX) * kMouse; proxyMouseY += (mouse.y - proxyMouseY) * kMouse; const dx = mouse.active ? mouse.x - proxyMouseX : 0; const dy = mouse.active ? mouse.y - proxyMouseY : 0; const rawSpeed = Math.hypot(dx, dy * 0.1) * glowConfig.speedScale; const kSpeed = 1.0 - Math.exp(-glowDynamics.speedEase * dt); smoothSpeed += (rawSpeed - smoothSpeed) * kSpeed; setSmoothSpeed(smoothSpeed); const u = (instancedBars.material as THREE.ShaderMaterial).uniforms as any; u.w1Gain.value = wave1.gain; u.w1Len.value = wave1.waveLength; u.w2Gain.value = wave2.gain; u.w2Len.value = wave2.waveLength; const mouseClipX = (proxyMouseX / cameraWidth) * 2 - 1; setMouseNDC(mouseClipX); let baseOffset = 40; if (window.innerWidth < 768) baseOffset = 20; u.uBaseY.value = -cameraHeight * 0.5 + baseOffset; (grainPass.uniforms as any).time.value += dt * 0.2; accumulateGlow(dt); waveComposer.render(); }; // Init initWaveThree(); onResize(waveContainer.clientWidth, waveContainer.clientHeight); gsap.to(waveContainer.querySelector('canvas'), { opacity: 1, duration: 1, ease: 'power2.out' }); const mainTimeline = gsap.timeline({ repeat: -1 }); mainTimeline.add(buildKeyframeTweens(wave1, waveKeyframes1), 0); mainTimeline.add(buildKeyframeTweens(wave2, waveKeyframes2), 0); mainTimeline.play(0); gsap.ticker.add(ticker); const ro = new ResizeObserver((entries) => { for (const e of entries) { if (e.target === waveContainer) { onResize(e.contentRect.width, e.contentRect.height); } } }); ro.observe(waveContainer); listeners.push(() => gsap.ticker.remove(ticker)); listeners.push(() => ro.disconnect()); const onVisibility = () => { document.hidden ? gsap.globalTimeline.pause() : gsap.globalTimeline.resume(); }; document.addEventListener('visibilitychange', onVisibility); listeners.push(() => document.removeEventListener('visibilitychange', onVisibility)); return () => { listeners.forEach((fn) => fn()); try { disposeWaveScene(); } catch {} try { const canvas = waveRenderer.domElement; if (canvas && canvas.parentElement === waveContainer) { waveContainer.removeChild(canvas); } } catch {} }; }, [extendLeftPx]); return (
{children} {/* Default Content Overlay */} {!children && (

{title}

{subtitle}

{ e.preventDefault(); onPromptSubmit?.(prompt); }} >