From 536e21340c9b1f700998bc84f26ec0e30a4802cd Mon Sep 17 00:00:00 2001 From: Candy Xie <154905277+candpixie@users.noreply.github.com> Date: Tue, 5 May 2026 18:06:05 -0400 Subject: [PATCH] feat(visuals): cursor uniforms + punchier shaders Lane Cursor: pipe pointer position into all 3 presets via a single window pointermove/pointerdown listener in App.jsx, threaded through Scene.setPointer to the active module. - Glacier: cursor drives a secondary frost highlight on shard rims - Tide: click drops an Evan-Wallace-style ripple; hover leaves a foam wake on the surface - Aurora: cursor.x bends ribbon flow direction & speed (wind), cursor.y nudges vertical bias Drama pass on top: ACES filmic + gamma in all three shaders, sharper rim/specular on Glacier, hotter caustic + sparkle curves on Tide, brighter ribbon cores + denser starfield on Aurora. Bloom default raised 0.55 -> 1.1 with threshold 0.4 -> 0.55 so peaks bloom instead of mid-greys. --- src/App.jsx | 32 ++++++++++++- src/components/Visualizer.jsx | 5 ++- src/visuals/Aurora.js | 46 +++++++++++++------ src/visuals/Glacier.js | 53 ++++++++++++++++------ src/visuals/Scene.js | 16 ++++--- src/visuals/Tide.js | 84 +++++++++++++++++++++++++++++------ 6 files changed, 189 insertions(+), 47 deletions(-) diff --git a/src/App.jsx b/src/App.jsx index c7c7d4a..aa81aed 100644 --- a/src/App.jsx +++ b/src/App.jsx @@ -1,4 +1,4 @@ -import { useState, useCallback } from 'react' +import { useState, useCallback, useRef, useEffect } from 'react' import Landing from './components/Landing' import Visualizer from './components/Visualizer' import Controls from './components/Controls' @@ -12,6 +12,10 @@ function App() { const [isActive, setIsActive] = useState(false) const [inputMode, setInputMode] = useState('mic') // 'mic' | 'file' const [preset, setPreset] = useState('Glacier') + const pointerRef = useRef({ + mouse: { x: 0.5, y: 0.5 }, + click: { x: 0.5, y: 0.5, serial: 0 }, + }) const [controls, setControls] = useState({ sensitivity: 0.65, smoothing: 0.4, @@ -76,6 +80,31 @@ function App() { setControls(prev => ({ ...prev, [key]: value })) }, []) + useEffect(() => { + if (!isActive) return + const onPointerMove = (e) => { + const x = e.clientX / window.innerWidth + const y = 1 - e.clientY / window.innerHeight + pointerRef.current.mouse.x = Math.min(1, Math.max(0, x)) + pointerRef.current.mouse.y = Math.min(1, Math.max(0, y)) + } + const onPointerDown = (e) => { + const x = e.clientX / window.innerWidth + const y = 1 - e.clientY / window.innerHeight + pointerRef.current.click = { + x: Math.min(1, Math.max(0, x)), + y: Math.min(1, Math.max(0, y)), + serial: pointerRef.current.click.serial + 1, + } + } + window.addEventListener('pointermove', onPointerMove) + window.addEventListener('pointerdown', onPointerDown) + return () => { + window.removeEventListener('pointermove', onPointerMove) + window.removeEventListener('pointerdown', onPointerDown) + } + }, [isActive]) + if (!isActive) { return } @@ -88,6 +117,7 @@ function App() { analyser={analyser} preset={preset} controls={controls} + pointerRef={pointerRef} /> diff --git a/src/components/Visualizer.jsx b/src/components/Visualizer.jsx index f3a02ee..56b35aa 100644 --- a/src/components/Visualizer.jsx +++ b/src/components/Visualizer.jsx @@ -13,7 +13,7 @@ function hzToNoteName(hz) { return `${name}${octave}` } -const Visualizer = ({ analyser, preset, controls }) => { +const Visualizer = ({ analyser, preset, controls, pointerRef }) => { const canvasRef = useRef(null) const sceneRef = useRef(null) const animationFrameRef = useRef(null) @@ -47,6 +47,9 @@ const Visualizer = ({ analyser, preset, controls }) => { analyser.getFloatTimeDomainData(timeData) const features = extractFeatures(frequencyData, timeData) + if (pointerRef?.current) { + sceneRef.current.setPointer(pointerRef.current) + } sceneRef.current.update(features, controls) // Throttle hint updates to ~10 Hz so React doesn't re-render at 60 fps. diff --git a/src/visuals/Aurora.js b/src/visuals/Aurora.js index 0994d38..24ef602 100644 --- a/src/visuals/Aurora.js +++ b/src/visuals/Aurora.js @@ -31,6 +31,7 @@ const FRAG = /* glsl */` uniform float uTime; uniform vec2 uResolution; + uniform vec2 uMouse; // normalized 0..1, bottom-left origin uniform float uBandY; uniform float uWaverAmp; uniform float uBrightnessAM; @@ -60,9 +61,14 @@ const FRAG = /* glsl */` // Single ribbon: thin sinuous band centered at y = cy, with FBM displacement. // Brighter near its center; thickness modulated by uWaverAmp. vec3 ribbon(vec2 uv, float cy, float seed, float bright){ - float xPhase = uTime * 0.3 + seed * 13.0; + // Cursor-driven flow: mouse.x (-1..1) accelerates / reverses horizontal + // ribbon scroll, mouse.y nudges vertical bias. + float flow = (uMouse.x - 0.5) * 2.0; + float xPhase = uTime * (0.3 + flow * 0.7) + seed * 13.0; float displ = fbm(vec2(uv.x * 1.4 + xPhase, seed * 7.0)) - 0.5; - displ += sin(uv.x * 3.5 + uTime * (0.6 + seed)) * (0.05 + uWaverAmp * 0.35); + displ += sin(uv.x * 3.5 + uTime * (0.6 + seed) + flow * 1.4) + * (0.05 + uWaverAmp * 0.35); + cy += (uMouse.y - 0.5) * 0.18; float y = cy + displ * 0.45; float thickness = 0.05 + uWaverAmp * 0.22; float d = abs(uv.y - y) / thickness; @@ -77,7 +83,9 @@ const FRAG = /* glsl */` vec3 col = mix(colLo, colHi, smoothstep(0.0, 1.0, intensity)); // Sparing rose hint near apex on attack-spawned ribbons col = mix(col, ROSE, intensity * uRibbonSpawn * 0.25 * step(0.8, seed)); - return col * pow(intensity, 1.4) * bright * 1.6; + // Brighter, sharper ribbon core. Higher exponent tightens the band; + // multiplier pushes the apex into bloom-bright territory. + return col * pow(intensity, 1.1) * bright * 2.6; } void main(){ @@ -85,16 +93,16 @@ const FRAG = /* glsl */` // map to 0..1 vertical; aurora lives mostly in upper half vec2 uv = vec2(frag.x, frag.y); - // Star background — sparse twinkly points - vec3 col = NAVY; - vec2 starGrid = floor(frag * uResolution.y * 0.55); + // Star background — denser, brighter, more variety in twinkle. + vec3 col = NAVY * 0.6; + vec2 starGrid = floor(frag * uResolution.y * 0.65); float s = hash(starGrid); - if(s > 0.992){ + if(s > 0.985){ float tw = 0.5 + 0.5 * sin(uTime * 1.7 + s * 31.0); - col += vec3(0.65, 0.72, 0.85) * (s - 0.992) * 220.0 * tw; + col += vec3(0.85, 0.92, 1.05) * (s - 0.985) * 360.0 * tw; } - // Subtle horizon glow at bottom - col += MINT * 0.04 * smoothstep(-0.4, -1.0, frag.y); + // Mint horizon glow on flux spike, cursor-modulated for taste. + col += MINT * (0.05 + 0.18 * uRibbonSpawn) * smoothstep(-0.4, -1.0, frag.y); // Ribbons — count driven by harmonicEnergy (1..5) float count = clamp(1.0 + floor(uRibbonCount * 4.0 + 0.5), 1.0, 5.0); @@ -112,11 +120,14 @@ const FRAG = /* glsl */` col += ribbon(uv, cy, seed, b); } - // Vignette - float vig = smoothstep(1.5, 0.3, length(frag)); - col *= 0.6 + 0.4 * vig; + // Stronger vignette — deeper edge falloff for cinematic dome feel. + float vig = smoothstep(1.6, 0.2, length(frag)); + col *= 0.4 + 0.6 * vig; - gl_FragColor = vec4(col, 1.0); + // ACES filmic + gamma → ribbon cores bloom hard, sky goes deep. + vec3 mapped = clamp((col*(2.51*col+0.03))/(col*(2.43*col+0.59)+0.14), 0.0, 1.0); + mapped = pow(mapped, vec3(1.0/2.2)); + gl_FragColor = vec4(mapped, 1.0); } ` @@ -131,6 +142,7 @@ export class Aurora { this.uniforms = { uTime: { value: 0 }, uResolution: { value: new THREE.Vector2(1, 1) }, + uMouse: { value: new THREE.Vector2(0.5, 0.5) }, uBandY: { value: 0.2 }, uWaverAmp: { value: 0 }, uBrightnessAM: { value: 0 }, @@ -176,6 +188,12 @@ export class Aurora { this.uniforms.uRibbonCount.value = audio?.harmonicEnergy ?? 0 } + setPointer(pointer) { + if (!pointer) return + const m = pointer.mouse + if (m) this.uniforms.uMouse.value.set(m.x, m.y) + } + updatePreset(preset) { this.preset = preset } diff --git a/src/visuals/Glacier.js b/src/visuals/Glacier.js index acede70..b0602a9 100644 --- a/src/visuals/Glacier.js +++ b/src/visuals/Glacier.js @@ -33,6 +33,7 @@ const FRAG = /* glsl */` uniform float uTime; uniform vec2 uResolution; + uniform vec2 uMouse; // normalized 0..1, bottom-left origin uniform vec3 uHighlightAxis; uniform float uShimmer; uniform float uShimmerRate; @@ -120,33 +121,52 @@ const FRAG = /* glsl */` vec3 p = ro + rd * hit; vec3 n = nrm(p); - // rim light - float rim = pow(1.0 - max(0.0, dot(n, -rd)), 2.5); + // Sharper rim (higher Fresnel exponent → thinner, brighter edge). + float ndv = max(0.0, dot(n, -rd)); + float rim = pow(1.0 - ndv, 4.5); + // Specular hot-spot — sharp, bright, audio-modulated. + vec3 L = normalize(vec3(0.45, 0.85, 0.55)); + vec3 H = normalize(L - rd); + float spec = pow(max(0.0, dot(n, H)), 64.0); // highlight cluster glow follows f0 float cluster = exp(-pow((p.y - uHighlightAxis.y) * 1.5, 2.0)); // refraction tint via internal scattering proxy float scatter = fbm(p * 1.6 + uTime * 0.05); - float trans = mix(0.18, 0.85, uTranslucency); - vec3 inner = mix(COLD_DEEP, COLD_TEAL, scatter); + float trans = mix(0.18, 0.95, uTranslucency); + vec3 inner = mix(COLD_DEEP * 0.6, COLD_TEAL, scatter); vec3 surf = mix(inner, COLD_ACCENT, rim); - surf = mix(surf, COLD_FROST, rim * cluster * (0.6 + 0.4 * uShimmer)); - surf *= mix(0.7, 1.25, trans); + surf = mix(surf, COLD_FROST * 1.6, rim * cluster * (0.7 + 0.6 * uShimmer)); + surf += COLD_FROST * spec * (1.2 + 2.5 * uShimmer); + surf *= mix(0.55, 1.55, trans); - // crack: white fissures on attack, decays via uCrack - float crackPattern = smoothstep(0.65, 0.95, fbm(p * 4.5 + vec3(uTime*0.4))); - surf += COLD_FROST * crackPattern * uCrack * 1.5; + // Secondary cursor-driven highlight: rim-modulated glow at the mouse. + vec2 mouseUv = (uMouse - 0.5) * vec2(uResolution.x / uResolution.y, 1.0); + float md = length(uv - mouseUv); + float mouseHL = exp(-md * md / 0.035); + surf = mix(surf, COLD_FROST * 1.8, rim * mouseHL * 1.0); + surf += COLD_FROST * mouseHL * spec * 1.4; + + // crack: white fissures on attack, decays via uCrack — hotter & sharper. + float crackPattern = smoothstep(0.78, 0.96, fbm(p * 4.5 + vec3(uTime*0.4))); + surf += COLD_FROST * crackPattern * uCrack * 3.0; // depth fog float fog = exp(-hit * 0.18); col = mix(col, surf, fog); } - // subtle vignette - float vig = smoothstep(1.4, 0.4, length(uv)); - col *= 0.55 + 0.45 * vig; + // Stronger vignette → deeper darks at edges. + float vig = smoothstep(1.5, 0.25, length(uv)); + col *= 0.35 + 0.65 * vig; + + // ACES filmic tone-map + gamma — gives bloom something to bite on + // and pushes the blacks low without crushing color. + vec3 x = col; + vec3 mapped = clamp((x*(2.51*x+0.03))/(x*(2.43*x+0.59)+0.14), 0.0, 1.0); + mapped = pow(mapped, vec3(1.0/2.2)); - gl_FragColor = vec4(col, 1.0); + gl_FragColor = vec4(mapped, 1.0); } ` @@ -163,6 +183,7 @@ export class Glacier { this.uniforms = { uTime: { value: 0 }, uResolution: { value: new THREE.Vector2(1, 1) }, + uMouse: { value: new THREE.Vector2(0.5, 0.5) }, uHighlightAxis: { value: new THREE.Vector3(0, 0, 0) }, uShimmer: { value: 0 }, uShimmerRate: { value: 5.5 }, @@ -274,6 +295,12 @@ export class Glacier { this.particleUniforms.uBurst.value = this.particle } + setPointer(pointer) { + if (!pointer) return + const m = pointer.mouse + if (m) this.uniforms.uMouse.value.set(m.x, m.y) + } + updatePreset(preset) { this.preset = preset } diff --git a/src/visuals/Scene.js b/src/visuals/Scene.js index 8166010..a3ee3d1 100644 --- a/src/visuals/Scene.js +++ b/src/visuals/Scene.js @@ -49,9 +49,9 @@ export class Scene { const renderPass = new RenderPass(this.scene, this.camera) this.composer.addPass(renderPass) this.bloomEffect = new BloomEffect({ - intensity: controls?.bloom ?? this.preset.bloom ?? 0.55, - luminanceThreshold: 0.4, - luminanceSmoothing: 0.9, + intensity: controls?.bloom ?? this.preset.bloom ?? 1.1, + luminanceThreshold: 0.55, + luminanceSmoothing: 0.7, }) this.composer.addEffect(this.bloomEffect) this.composer.setSize(width, height) @@ -87,17 +87,23 @@ export class Scene { update(features, controls) { this.controls = controls if (this.bloomEffect) { - this.bloomEffect.intensity = controls?.bloom ?? this.preset.bloom ?? 0.55 + this.bloomEffect.intensity = controls?.bloom ?? this.preset.bloom ?? 1.1 } if (this.activeModule) { this.activeModule.update(features, controls) } } + setPointer(pointer) { + if (this.activeModule && typeof this.activeModule.setPointer === 'function') { + this.activeModule.setPointer(pointer) + } + } + updateControls(controls) { this.controls = controls if (this.bloomEffect) { - this.bloomEffect.intensity = controls?.bloom ?? this.preset.bloom ?? 0.55 + this.bloomEffect.intensity = controls?.bloom ?? this.preset.bloom ?? 1.1 } } diff --git a/src/visuals/Tide.js b/src/visuals/Tide.js index fb785bf..595a514 100644 --- a/src/visuals/Tide.js +++ b/src/visuals/Tide.js @@ -34,6 +34,9 @@ const FRAG = /* glsl */` uniform float uTime; uniform vec2 uResolution; + uniform vec2 uMouse; // normalized 0..1, bottom-left origin + uniform vec2 uClickPos; // last click in normalized 0..1 + uniform float uClickAge; // seconds since last click (large = no recent click) uniform float uWaveAmp; uniform float uCausticDensity; uniform float uStandingWavePeriod; @@ -109,33 +112,69 @@ const FRAG = /* glsl */` ripple += uWaveAmp * 0.18 * sin(r * 6.0 - uTime * 4.0) * exp(-r * 0.4); } + // Click ripple — Evan-Wallace-style "drop a stone" at the cursor. + // Project click NDC into the same world-XZ plane as the rest of the surface, + // then add a decaying expanding ring centered there. + if (uClickAge < 6.0) { + vec2 clickFrag = vec2( + (uClickPos.x - 0.5) * (uResolution.x / uResolution.y), + uClickPos.y - 0.5 + ); + float clickDepth = max(0.001, horizon - clickFrag.y); + float clickWZ = 1.0 / clickDepth; + float clickWX = clickFrag.x * clickWZ * 1.4; + vec2 clickWP = vec2(clickWX, clickWZ); + float cr = length(wp - clickWP); + float decay = exp(-uClickAge * 0.9); + // Expanding wave-front: peak radius grows with age. + float front = exp(-pow(cr - uClickAge * 1.6, 2.0) * 0.6); + ripple += 0.32 * decay * front * sin(cr * 6.0 - uTime * 4.0); + } + float h = ripple + standingWave(wp); float c = caustic(wp, uCausticDensity); // Underwater fog (bands.low) — adds blue haze to far water float fog = 1.0 - exp(-worldZ * (0.05 + uFogDensity * 0.25)); - // Base water color - vec3 water = mix(MIDNIGHT, MOONCYAN, c + h * 0.5); - water = mix(water, MIDNIGHT * 1.2, fog * 0.7); + // Base water color — pushed darker so caustics & sparkle have somewhere + // to peak from. Caustic curve sharpened (higher exponent on c). + vec3 water = mix(MIDNIGHT * 0.5, MOONCYAN, pow(clamp(c + h*0.6, 0.0, 1.0), 1.4)); + water = mix(water, MIDNIGHT * 0.4, fog * 0.85); - // Specular sparkle on crests (bands.high) - float crest = smoothstep(0.65, 0.98, c + h); - vec3 sparkle = SILVER * crest * (0.25 + uSparkle * 1.0); + // Specular sparkle on crests — much sharper threshold + brighter peaks. + float crest = smoothstep(0.78, 1.0, c + h); + crest = pow(crest, 1.8); + vec3 sparkle = (SILVER * 1.6 + FOAM * 0.7) * crest * (0.6 + uSparkle * 2.5); water += sparkle; - // Sky above horizon — soft gradient with subtle stars - vec3 sky = mix(MIDNIGHT * 0.8, MIDNIGHT + MOONCYAN * 0.06, smoothstep(horizon, 1.0, frag.y)); + // Cursor "wake": cursor-induced sparkle wherever the pointer hovers + // over water — feels like a finger trailing through the surface. + if (uMouse.y < 0.62) { + vec2 mouseFrag = vec2( + (uMouse.x - 0.5) * (uResolution.x / uResolution.y), + uMouse.y - 0.5 + ); + float md = length(frag - mouseFrag); + float wake = exp(-md * md * 22.0); + water += FOAM * wake * 0.55; + } + + // Sky above horizon — slightly darker base, brighter star pops. + vec3 sky = mix(MIDNIGHT * 0.55, MIDNIGHT + MOONCYAN * 0.10, smoothstep(horizon, 1.0, frag.y)); float starSeed = hash(floor(frag * uResolution.y * 0.5)); - if(starSeed > 0.997) sky += vec3(0.6) * (starSeed - 0.997) * 333.0; + if(starSeed > 0.996) sky += vec3(0.85, 0.92, 1.0) * (starSeed - 0.996) * 300.0; vec3 col = mix(sky, water, belowHorizon); - // Vignette - float vig = smoothstep(1.5, 0.3, length(frag)); - col *= 0.55 + 0.45 * vig; + // Stronger vignette for cinematic edges. + float vig = smoothstep(1.6, 0.2, length(frag)); + col *= 0.32 + 0.68 * vig; - gl_FragColor = vec4(col, 1.0); + // ACES filmic + gamma → richer blacks, blooming highlights. + vec3 mapped = clamp((col*(2.51*col+0.03))/(col*(2.43*col+0.59)+0.14), 0.0, 1.0); + mapped = pow(mapped, vec3(1.0/2.2)); + gl_FragColor = vec4(mapped, 1.0); } ` @@ -147,9 +186,13 @@ export class Tide { this.smoothCurrent = new THREE.Vector2(0, 0) this.geometry = new THREE.PlaneGeometry(2, 2) + this.clickAge = 999 this.uniforms = { uTime: { value: 0 }, uResolution: { value: new THREE.Vector2(1, 1) }, + uMouse: { value: new THREE.Vector2(0.5, 0.5) }, + uClickPos: { value: new THREE.Vector2(0.5, 0.5) }, + uClickAge: { value: 999 }, uWaveAmp: { value: 0 }, uCausticDensity: { value: 0 }, uStandingWavePeriod: { value: 0 }, @@ -190,6 +233,9 @@ export class Tide { const vib = audio?.vibrato const standingPeriod = vib?.active ? Math.max(0.1, 1 / Math.max(0.5, vib.rateHz || 5)) : 0 + this.clickAge += dt + this.uniforms.uClickAge.value = this.clickAge + this.uniforms.uTime.value = this.time this.uniforms.uWaveAmp.value = Math.min(1.5, (audio?.rms ?? 0) * 3) this.uniforms.uCausticDensity.value = audio?.centroid ?? 0 @@ -199,6 +245,18 @@ export class Tide { this.uniforms.uSparkle.value = audio?.bands?.high ?? 0 } + setPointer(pointer) { + if (!pointer) return + const m = pointer.mouse + if (m) this.uniforms.uMouse.value.set(m.x, m.y) + const click = pointer.click + if (click && click.serial !== this._lastClickSerial) { + this._lastClickSerial = click.serial + this.uniforms.uClickPos.value.set(click.x, click.y) + this.clickAge = 0 + } + } + updatePreset(preset) { this.preset = preset }