Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 31 additions & 1 deletion src/App.jsx
Original file line number Diff line number Diff line change
@@ -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'
Expand All @@ -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,
Expand Down Expand Up @@ -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 <Landing onStart={handleStart} />
}
Expand All @@ -88,6 +117,7 @@ function App() {
analyser={analyser}
preset={preset}
controls={controls}
pointerRef={pointerRef}
/>
</div>

Expand Down
5 changes: 4 additions & 1 deletion src/components/Visualizer.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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.
Expand Down
46 changes: 32 additions & 14 deletions src/visuals/Aurora.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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;
Expand All @@ -77,24 +83,26 @@ 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(){
vec2 frag = (gl_FragCoord.xy - 0.5*uResolution.xy) / uResolution.y;
// 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);
Expand All @@ -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);
}
`

Expand All @@ -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 },
Expand Down Expand Up @@ -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
}
Expand Down
53 changes: 40 additions & 13 deletions src/visuals/Glacier.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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);
}
`

Expand All @@ -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 },
Expand Down Expand Up @@ -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
}
Expand Down
16 changes: 11 additions & 5 deletions src/visuals/Scene.js
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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
}
}

Expand Down
Loading