From 8d9d47cea73d7d0ca5123c82af9be0c3018550e1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebastian=20Kr=C3=BCger?= Date: Sun, 30 Nov 2025 10:10:54 +0100 Subject: [PATCH] feat: add 6 new audio visualizers MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Vortex: spiral particles pulled toward center, speed reacts to beat - Starfield: flying through stars with depth parallax and streak effects - Grid: 3D wave plane with ripple effects from mouse and audio - Galaxy: 3-arm spiral galaxy with tilted perspective - Waveform: circular audio waveform in concentric rings - Kaleidoscope: 8-segment mirrored geometric patterns All visualizers include: - GLSL shaders with audio reactivity (low/mid/high frequencies) - Mouse tracking for interactive parallax - Beat-synchronized animations 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- assets/js/visualizer/galaxy.js | 158 +++++++++++++++++++++++++++ assets/js/visualizer/grid.js | 134 +++++++++++++++++++++++ assets/js/visualizer/kaleidoscope.js | 152 ++++++++++++++++++++++++++ assets/js/visualizer/scene.js | 14 ++- assets/js/visualizer/starfield.js | 133 ++++++++++++++++++++++ assets/js/visualizer/vortex.js | 138 +++++++++++++++++++++++ assets/js/visualizer/waveform.js | 141 ++++++++++++++++++++++++ 7 files changed, 869 insertions(+), 1 deletion(-) create mode 100644 assets/js/visualizer/galaxy.js create mode 100644 assets/js/visualizer/grid.js create mode 100644 assets/js/visualizer/kaleidoscope.js create mode 100644 assets/js/visualizer/starfield.js create mode 100644 assets/js/visualizer/vortex.js create mode 100644 assets/js/visualizer/waveform.js diff --git a/assets/js/visualizer/galaxy.js b/assets/js/visualizer/galaxy.js new file mode 100644 index 0000000..55bbdf4 --- /dev/null +++ b/assets/js/visualizer/galaxy.js @@ -0,0 +1,158 @@ +/** + * Galaxy Visualizer + * Spiral arm formation with rotating particles + */ + +import * as THREE from 'three'; +import { BaseVisualizer } from './base-visualizer.js'; + +const vertexShader = ` +uniform float uTime; +uniform float uLow; +uniform float uMid; +uniform float uHigh; +uniform float uMouseX; +uniform float uMouseY; + +attribute float aArm; +attribute float aOffset; +attribute float aRadius; + +varying float vAlpha; +varying float vArm; + +void main() { + // Spiral arm parameters + float arms = 3.0; + float armAngle = aArm * (6.28318 / arms); + + // Radius with some variation + float radius = aRadius * (1.0 + uLow * 0.3); + + // Spiral: angle increases with radius + float spiralTightness = 0.15; + float angle = armAngle + aRadius * spiralTightness + uTime * (0.2 + uMid * 0.5); + + // Add offset for thickness + angle += aOffset * 0.3; + float radiusOffset = aOffset * 3.0; + + // Calculate position + float x = cos(angle) * (radius + radiusOffset); + float y = sin(angle) * (radius + radiusOffset); + + // Height variation - thicker disk toward center + float z = sin(aOffset * 3.0 + aRadius * 0.1) * (3.0 - aRadius * 0.05); + z += uLow * 5.0; // Bounce with bass + + // High frequencies add sparkle + x += sin(aOffset * 20.0 + uTime * 5.0) * uHigh * 2.0; + y += cos(aOffset * 20.0 + uTime * 5.0) * uHigh * 2.0; + + // Tilt the galaxy + float tiltX = 0.3; + float cosX = cos(tiltX); + float sinX = sin(tiltX); + float newY = y * cosX - z * sinX; + float newZ = y * sinX + z * cosX; + y = newY; + z = newZ; + + // Mouse offset + x -= uMouseX * 5.0; + y -= uMouseY * 4.0; + + vec3 pos = vec3(x, y, z); + vec4 mvPosition = modelViewMatrix * vec4(pos, 1.0); + + // Size - larger toward center, pulses with bass + float size = 2.5 * (1.0 - aRadius / 60.0) + uLow * 1.0; + gl_PointSize = size * (300.0 / -mvPosition.z); + + // Alpha - brighter toward center and in arm cores + vAlpha = (0.7 - aRadius / 80.0) * (1.0 - abs(aOffset) * 0.3) + uLow * 0.2; + vArm = aArm; + + gl_Position = projectionMatrix * mvPosition; +} +`; + +const fragmentShader = ` +varying float vAlpha; +varying float vArm; + +void main() { + float dist = length(gl_PointCoord - vec2(0.5)); + if (dist > 0.5) discard; + + float alpha = 1.0 - smoothstep(0.0, 0.5, dist); + alpha *= vAlpha; + + vec3 color = vec3(1.0); + + gl_FragColor = vec4(color, alpha); +} +`; + +export class GalaxyVisualizer extends BaseVisualizer { + static name = 'Galaxy'; + + constructor(scene) { + super(scene); + this.particleCount = 4000; + this.arms = 3; + this.init(); + } + + init() { + const positions = new Float32Array(this.particleCount * 3); + const armIndices = new Float32Array(this.particleCount); + const offsets = new Float32Array(this.particleCount); + const radii = new Float32Array(this.particleCount); + + for (let i = 0; i < this.particleCount; i++) { + // Assign to an arm + const arm = Math.floor(Math.random() * this.arms); + + // Radius - more particles toward center + const radius = Math.pow(Math.random(), 0.5) * 50; + + // Offset from arm center + const offset = (Math.random() - 0.5) * 2; + + positions[i * 3] = 0; + positions[i * 3 + 1] = 0; + positions[i * 3 + 2] = 0; + + armIndices[i] = arm; + offsets[i] = offset; + radii[i] = radius; + } + + const geometry = new THREE.BufferGeometry(); + geometry.setAttribute('position', new THREE.BufferAttribute(positions, 3)); + geometry.setAttribute('aArm', new THREE.BufferAttribute(armIndices, 1)); + geometry.setAttribute('aOffset', new THREE.BufferAttribute(offsets, 1)); + geometry.setAttribute('aRadius', new THREE.BufferAttribute(radii, 1)); + + this.material = this.createShaderMaterial(vertexShader, fragmentShader, { + uMouseX: { value: 0 }, + uMouseY: { value: 0 } + }); + + this.mesh = new THREE.Points(geometry, this.material); + } + + update(bands, time, mouse = { x: 0, y: 0 }) { + if (!this.material) return; + + this.material.uniforms.uTime.value = time; + this.material.uniforms.uLow.value = bands.low || 0; + this.material.uniforms.uMid.value = bands.mid || 0; + this.material.uniforms.uHigh.value = bands.high || 0; + this.material.uniforms.uMouseX.value = mouse.x; + this.material.uniforms.uMouseY.value = mouse.y; + } +} + +export default GalaxyVisualizer; diff --git a/assets/js/visualizer/grid.js b/assets/js/visualizer/grid.js new file mode 100644 index 0000000..afe3e17 --- /dev/null +++ b/assets/js/visualizer/grid.js @@ -0,0 +1,134 @@ +/** + * Wave Grid Visualizer + * 3D plane of dots that ripple like water + */ + +import * as THREE from 'three'; +import { BaseVisualizer } from './base-visualizer.js'; + +const vertexShader = ` +uniform float uTime; +uniform float uLow; +uniform float uMid; +uniform float uHigh; +uniform float uMouseX; +uniform float uMouseY; + +attribute float aIndex; + +varying float vAlpha; +varying float vHeight; + +void main() { + vec3 pos = position; + + // Distance from center for wave calculations + float dist = length(pos.xy); + + // Multiple wave sources + float wave1 = sin(dist * 0.3 - uTime * 2.0) * (5.0 + uLow * 10.0); + float wave2 = sin(dist * 0.5 - uTime * 3.0 + 1.0) * (3.0 + uMid * 5.0); + float wave3 = cos(pos.x * 0.2 + uTime) * cos(pos.y * 0.2 + uTime) * (2.0 + uHigh * 4.0); + + // Combine waves for height + float height = wave1 + wave2 + wave3; + pos.z = height; + + // Mouse creates ripple effect + float mouseDist = length(pos.xy - vec2(uMouseX * 30.0, uMouseY * 30.0)); + pos.z += sin(mouseDist * 0.5 - uTime * 4.0) * 3.0 * (1.0 / (1.0 + mouseDist * 0.1)); + + // Rotate grid for better viewing angle + float tiltAngle = 0.6; + float cosT = cos(tiltAngle); + float sinT = sin(tiltAngle); + float newY = pos.y * cosT - pos.z * sinT; + float newZ = pos.y * sinT + pos.z * cosT; + pos.y = newY; + pos.z = newZ - 20.0; // Push back + + vec4 mvPosition = modelViewMatrix * vec4(pos, 1.0); + + // Size pulses with bass + float size = 2.5 + uLow * 1.5; + gl_PointSize = size * (300.0 / -mvPosition.z); + + // Alpha based on height + vAlpha = 0.6 + abs(height) * 0.02; + vHeight = height; + + gl_Position = projectionMatrix * mvPosition; +} +`; + +const fragmentShader = ` +varying float vAlpha; +varying float vHeight; + +void main() { + float dist = length(gl_PointCoord - vec2(0.5)); + if (dist > 0.5) discard; + + float alpha = 1.0 - smoothstep(0.1, 0.5, dist); + alpha *= vAlpha; + + vec3 color = vec3(1.0); + + gl_FragColor = vec4(color, alpha); +} +`; + +export class GridVisualizer extends BaseVisualizer { + static name = 'Grid'; + + constructor(scene) { + super(scene); + this.gridSize = 40; // 40x40 grid + this.spacing = 3; + this.init(); + } + + init() { + const count = this.gridSize * this.gridSize; + const positions = new Float32Array(count * 3); + const indices = new Float32Array(count); + + let idx = 0; + const offset = (this.gridSize - 1) * this.spacing / 2; + + for (let x = 0; x < this.gridSize; x++) { + for (let y = 0; y < this.gridSize; y++) { + positions[idx * 3] = x * this.spacing - offset; + positions[idx * 3 + 1] = y * this.spacing - offset; + positions[idx * 3 + 2] = 0; + + indices[idx] = idx; + idx++; + } + } + + const geometry = new THREE.BufferGeometry(); + geometry.setAttribute('position', new THREE.BufferAttribute(positions, 3)); + geometry.setAttribute('aIndex', new THREE.BufferAttribute(indices, 1)); + + this.material = this.createShaderMaterial(vertexShader, fragmentShader, { + uMouseX: { value: 0 }, + uMouseY: { value: 0 } + }); + + this.mesh = new THREE.Points(geometry, this.material); + } + + update(bands, time, mouse = { x: 0, y: 0 }) { + if (!this.material) return; + + this.material.uniforms.uTime.value = time; + this.material.uniforms.uLow.value = bands.low || 0; + this.material.uniforms.uMid.value = bands.mid || 0; + this.material.uniforms.uHigh.value = bands.high || 0; + this.material.uniforms.uMouseX.value = mouse.x; + this.material.uniforms.uMouseY.value = mouse.y; + } +} + +export default GridVisualizer; diff --git a/assets/js/visualizer/kaleidoscope.js b/assets/js/visualizer/kaleidoscope.js new file mode 100644 index 0000000..e0b3de0 --- /dev/null +++ b/assets/js/visualizer/kaleidoscope.js @@ -0,0 +1,152 @@ +/** + * Kaleidoscope Visualizer + * Mirrored geometric patterns that morph with audio + */ + +import * as THREE from 'three'; +import { BaseVisualizer } from './base-visualizer.js'; + +const vertexShader = ` +uniform float uTime; +uniform float uLow; +uniform float uMid; +uniform float uHigh; +uniform float uMouseX; +uniform float uMouseY; + +attribute float aSegment; +attribute float aLayer; +attribute float aProgress; + +varying float vAlpha; +varying float vSegment; + +void main() { + // Kaleidoscope parameters + float segments = 8.0; + float segmentAngle = 6.28318 / segments; + + // Base angle for this segment + float baseAngle = aSegment * segmentAngle; + + // Create pattern within segment + float patternRadius = 10.0 + aLayer * 15.0 + uLow * 10.0; + float patternAngle = aProgress * segmentAngle * 0.8; // Stay within segment + + // Mirror effect - alternate segments are flipped + float mirror = mod(aSegment, 2.0) < 1.0 ? 1.0 : -1.0; + patternAngle *= mirror; + + // Add time-based morphing + float morph = sin(uTime * 2.0 + aLayer) * (5.0 + uMid * 10.0); + patternRadius += morph; + + // Final angle + float angle = baseAngle + patternAngle + uTime * 0.2; + + // High frequencies add shimmer + float shimmer = sin(aProgress * 20.0 + uTime * 8.0) * uHigh * 3.0; + patternRadius += shimmer; + + // Calculate position + float x = cos(angle) * patternRadius; + float y = sin(angle) * patternRadius; + float z = sin(aProgress * 6.28 + uTime * 2.0) * (3.0 + uLow * 5.0); + + // Mouse offset + x -= uMouseX * 5.0; + y -= uMouseY * 4.0; + + vec3 pos = vec3(x, y, z); + vec4 mvPosition = modelViewMatrix * vec4(pos, 1.0); + + // Size varies with layer + float size = 2.0 + aLayer * 0.5 + uLow * 1.0; + gl_PointSize = size * (300.0 / -mvPosition.z); + + // Alpha - inner layers brighter + vAlpha = 0.8 - aLayer * 0.1 + uLow * 0.2; + vSegment = aSegment; + + gl_Position = projectionMatrix * mvPosition; +} +`; + +const fragmentShader = ` +varying float vAlpha; +varying float vSegment; + +void main() { + float dist = length(gl_PointCoord - vec2(0.5)); + if (dist > 0.5) discard; + + float alpha = 1.0 - smoothstep(0.0, 0.5, dist); + alpha *= vAlpha; + + vec3 color = vec3(1.0); + + gl_FragColor = vec4(color, alpha); +} +`; + +export class KaleidoscopeVisualizer extends BaseVisualizer { + static name = 'Kaleidoscope'; + + constructor(scene) { + super(scene); + this.segments = 8; + this.layers = 5; + this.pointsPerSegmentLayer = 30; + this.init(); + } + + init() { + const count = this.segments * this.layers * this.pointsPerSegmentLayer; + const positions = new Float32Array(count * 3); + const segmentIndices = new Float32Array(count); + const layerIndices = new Float32Array(count); + const progress = new Float32Array(count); + + let idx = 0; + for (let seg = 0; seg < this.segments; seg++) { + for (let layer = 0; layer < this.layers; layer++) { + for (let p = 0; p < this.pointsPerSegmentLayer; p++) { + positions[idx * 3] = 0; + positions[idx * 3 + 1] = 0; + positions[idx * 3 + 2] = 0; + + segmentIndices[idx] = seg; + layerIndices[idx] = layer; + progress[idx] = p / this.pointsPerSegmentLayer; + idx++; + } + } + } + + const geometry = new THREE.BufferGeometry(); + geometry.setAttribute('position', new THREE.BufferAttribute(positions, 3)); + geometry.setAttribute('aSegment', new THREE.BufferAttribute(segmentIndices, 1)); + geometry.setAttribute('aLayer', new THREE.BufferAttribute(layerIndices, 1)); + geometry.setAttribute('aProgress', new THREE.BufferAttribute(progress, 1)); + + this.material = this.createShaderMaterial(vertexShader, fragmentShader, { + uMouseX: { value: 0 }, + uMouseY: { value: 0 } + }); + + this.mesh = new THREE.Points(geometry, this.material); + } + + update(bands, time, mouse = { x: 0, y: 0 }) { + if (!this.material) return; + + this.material.uniforms.uTime.value = time; + this.material.uniforms.uLow.value = bands.low || 0; + this.material.uniforms.uMid.value = bands.mid || 0; + this.material.uniforms.uHigh.value = bands.high || 0; + this.material.uniforms.uMouseX.value = mouse.x; + this.material.uniforms.uMouseY.value = mouse.y; + } +} + +export default KaleidoscopeVisualizer; diff --git a/assets/js/visualizer/scene.js b/assets/js/visualizer/scene.js index f2129ab..7c86246 100644 --- a/assets/js/visualizer/scene.js +++ b/assets/js/visualizer/scene.js @@ -7,12 +7,24 @@ import * as THREE from 'three'; import { SphereVisualizer } from './particles.js'; import { TunnelVisualizer } from './tunnel.js'; import { HelixVisualizer } from './helix.js'; +import { VortexVisualizer } from './vortex.js'; +import { StarfieldVisualizer } from './starfield.js'; +import { GridVisualizer } from './grid.js'; +import { GalaxyVisualizer } from './galaxy.js'; +import { WaveformVisualizer } from './waveform.js'; +import { KaleidoscopeVisualizer } from './kaleidoscope.js'; // Available visualizer classes const VISUALIZERS = [ SphereVisualizer, TunnelVisualizer, - HelixVisualizer + HelixVisualizer, + VortexVisualizer, + StarfieldVisualizer, + GridVisualizer, + GalaxyVisualizer, + WaveformVisualizer, + KaleidoscopeVisualizer ]; export class Visualizer { diff --git a/assets/js/visualizer/starfield.js b/assets/js/visualizer/starfield.js new file mode 100644 index 0000000..c0d3066 --- /dev/null +++ b/assets/js/visualizer/starfield.js @@ -0,0 +1,133 @@ +/** + * Starfield Visualizer + * Flying through stars that streak and pulse with the beat + */ + +import * as THREE from 'three'; +import { BaseVisualizer } from './base-visualizer.js'; + +const vertexShader = ` +uniform float uTime; +uniform float uLow; +uniform float uMid; +uniform float uHigh; +uniform float uMouseX; +uniform float uMouseY; + +attribute float aSize; +attribute float aSpeed; + +varying float vAlpha; +varying float vStreak; + +void main() { + vec3 pos = position; + + // Stars fly toward camera (positive Z) + float speed = (10.0 + uLow * 30.0) * aSpeed; + float z = mod(pos.z + uTime * speed, 200.0) - 100.0; + pos.z = z; + + // Depth factor - stars closer are brighter and larger + float depthFactor = (z + 100.0) / 200.0; + + // Slight drift based on position + pos.x += sin(uTime * 0.5 + position.x * 0.1) * 2.0; + pos.y += cos(uTime * 0.5 + position.y * 0.1) * 2.0; + + // Mouse parallax - closer stars move more + pos.x -= uMouseX * 10.0 * (1.0 - depthFactor); + pos.y -= uMouseY * 8.0 * (1.0 - depthFactor); + + vec4 mvPosition = modelViewMatrix * vec4(pos, 1.0); + + // Size increases as stars approach + float size = aSize * (1.0 - depthFactor) * 3.0; + size *= (1.0 + uLow * 0.5); + gl_PointSize = size * (300.0 / -mvPosition.z); + + // Alpha and streak based on depth and speed + vAlpha = (1.0 - depthFactor) * 0.9 + uHigh * 0.2; + vStreak = speed * 0.02; // For potential streak effect + + gl_Position = projectionMatrix * mvPosition; +} +`; + +const fragmentShader = ` +varying float vAlpha; +varying float vStreak; + +void main() { + vec2 uv = gl_PointCoord - vec2(0.5); + float dist = length(uv); + + if (dist > 0.5) discard; + + // Core glow + float alpha = 1.0 - smoothstep(0.0, 0.5, dist); + alpha *= vAlpha; + + // Add slight elongation effect for speed + float streak = 1.0 - smoothstep(0.0, 0.3, abs(uv.y)); + alpha *= mix(1.0, streak, min(vStreak, 0.5)); + + vec3 color = vec3(1.0); + + gl_FragColor = vec4(color, alpha); +} +`; + +export class StarfieldVisualizer extends BaseVisualizer { + static name = 'Starfield'; + + constructor(scene) { + super(scene); + this.starCount = 2000; + this.init(); + } + + init() { + const positions = new Float32Array(this.starCount * 3); + const sizes = new Float32Array(this.starCount); + const speeds = new Float32Array(this.starCount); + + for (let i = 0; i < this.starCount; i++) { + // Distribute stars in a cylinder around the camera + const angle = Math.random() * Math.PI * 2; + const radius = 10 + Math.random() * 60; + + positions[i * 3] = Math.cos(angle) * radius; + positions[i * 3 + 1] = Math.sin(angle) * radius; + positions[i * 3 + 2] = Math.random() * 200 - 100; // -100 to 100 + + sizes[i] = 1.0 + Math.random() * 3.0; + speeds[i] = 0.5 + Math.random() * 1.0; + } + + const geometry = new THREE.BufferGeometry(); + geometry.setAttribute('position', new THREE.BufferAttribute(positions, 3)); + geometry.setAttribute('aSize', new THREE.BufferAttribute(sizes, 1)); + geometry.setAttribute('aSpeed', new THREE.BufferAttribute(speeds, 1)); + + this.material = this.createShaderMaterial(vertexShader, fragmentShader, { + uMouseX: { value: 0 }, + uMouseY: { value: 0 } + }); + + this.mesh = new THREE.Points(geometry, this.material); + } + + update(bands, time, mouse = { x: 0, y: 0 }) { + if (!this.material) return; + + this.material.uniforms.uTime.value = time; + this.material.uniforms.uLow.value = bands.low || 0; + this.material.uniforms.uMid.value = bands.mid || 0; + this.material.uniforms.uHigh.value = bands.high || 0; + this.material.uniforms.uMouseX.value = mouse.x; + this.material.uniforms.uMouseY.value = mouse.y; + } +} + +export default StarfieldVisualizer; diff --git a/assets/js/visualizer/vortex.js b/assets/js/visualizer/vortex.js new file mode 100644 index 0000000..6684a03 --- /dev/null +++ b/assets/js/visualizer/vortex.js @@ -0,0 +1,138 @@ +/** + * Vortex Visualizer + * Spiral of particles being pulled into a center point + */ + +import * as THREE from 'three'; +import { BaseVisualizer } from './base-visualizer.js'; + +const vertexShader = ` +uniform float uTime; +uniform float uLow; +uniform float uMid; +uniform float uHigh; +uniform float uMouseX; +uniform float uMouseY; + +attribute float aAngle; +attribute float aRadius; +attribute float aSpeed; + +varying float vAlpha; +varying float vRadius; + +void main() { + // Spiral motion - particles orbit and get pulled inward + float time = uTime * (0.5 + uMid * 2.0); + float angle = aAngle + time * aSpeed; + + // Radius pulses with bass, particles get pulled in + float radiusPull = 1.0 - uLow * 0.3; + float radius = aRadius * radiusPull; + + // Add some vertical motion based on radius + float z = sin(angle * 3.0 + uTime) * 5.0 * (1.0 - aRadius / 50.0); + z += uLow * 10.0; // Bounce toward viewer + + // Calculate position + float x = cos(angle) * radius; + float y = sin(angle) * radius; + + // High frequencies add turbulence + x += sin(aAngle * 10.0 + uTime * 5.0) * uHigh * 3.0; + y += cos(aAngle * 10.0 + uTime * 5.0) * uHigh * 3.0; + + // Mouse offset + x -= uMouseX * 5.0; + y -= uMouseY * 4.0; + + vec3 pos = vec3(x, y, z); + vec4 mvPosition = modelViewMatrix * vec4(pos, 1.0); + + // Size based on radius - larger in center + float size = 3.0 * (1.0 - aRadius / 60.0) + uLow * 2.0; + gl_PointSize = size * (300.0 / -mvPosition.z); + + // Alpha - brighter toward center + vAlpha = 0.8 - aRadius / 80.0 + uLow * 0.2; + vRadius = aRadius; + + gl_Position = projectionMatrix * mvPosition; +} +`; + +const fragmentShader = ` +varying float vAlpha; +varying float vRadius; + +void main() { + float dist = length(gl_PointCoord - vec2(0.5)); + if (dist > 0.5) discard; + + float alpha = 1.0 - smoothstep(0.1, 0.5, dist); + alpha *= vAlpha; + + // Pure white + vec3 color = vec3(1.0); + + gl_FragColor = vec4(color, alpha); +} +`; + +export class VortexVisualizer extends BaseVisualizer { + static name = 'Vortex'; + + constructor(scene) { + super(scene); + this.particleCount = 3000; + this.init(); + } + + init() { + const positions = new Float32Array(this.particleCount * 3); + const angles = new Float32Array(this.particleCount); + const radii = new Float32Array(this.particleCount); + const speeds = new Float32Array(this.particleCount); + + for (let i = 0; i < this.particleCount; i++) { + // Distribute particles in a disk + const angle = Math.random() * Math.PI * 2; + const radius = 5 + Math.random() * 45; // 5 to 50 + + positions[i * 3] = 0; + positions[i * 3 + 1] = 0; + positions[i * 3 + 2] = 0; + + angles[i] = angle; + radii[i] = radius; + // Faster rotation for particles closer to center + speeds[i] = 0.5 + (1.0 - radius / 50.0) * 1.5; + } + + const geometry = new THREE.BufferGeometry(); + geometry.setAttribute('position', new THREE.BufferAttribute(positions, 3)); + geometry.setAttribute('aAngle', new THREE.BufferAttribute(angles, 1)); + geometry.setAttribute('aRadius', new THREE.BufferAttribute(radii, 1)); + geometry.setAttribute('aSpeed', new THREE.BufferAttribute(speeds, 1)); + + this.material = this.createShaderMaterial(vertexShader, fragmentShader, { + uMouseX: { value: 0 }, + uMouseY: { value: 0 } + }); + + this.mesh = new THREE.Points(geometry, this.material); + } + + update(bands, time, mouse = { x: 0, y: 0 }) { + if (!this.material) return; + + this.material.uniforms.uTime.value = time; + this.material.uniforms.uLow.value = bands.low || 0; + this.material.uniforms.uMid.value = bands.mid || 0; + this.material.uniforms.uHigh.value = bands.high || 0; + this.material.uniforms.uMouseX.value = mouse.x; + this.material.uniforms.uMouseY.value = mouse.y; + } +} + +export default VortexVisualizer; diff --git a/assets/js/visualizer/waveform.js b/assets/js/visualizer/waveform.js new file mode 100644 index 0000000..6f1e730 --- /dev/null +++ b/assets/js/visualizer/waveform.js @@ -0,0 +1,141 @@ +/** + * Waveform Circle Visualizer + * Audio waveform wrapped in a circle - classic visualizer style + */ + +import * as THREE from 'three'; +import { BaseVisualizer } from './base-visualizer.js'; + +const vertexShader = ` +uniform float uTime; +uniform float uLow; +uniform float uMid; +uniform float uHigh; +uniform float uMouseX; +uniform float uMouseY; + +attribute float aAngle; +attribute float aRing; + +varying float vAlpha; +varying float vRing; + +void main() { + // Base radius for each ring + float baseRadius = 15.0 + aRing * 12.0; + + // Waveform displacement based on angle and audio + float waveFreq = 8.0 + aRing * 4.0; + float wave = sin(aAngle * waveFreq + uTime * 3.0) * (uMid * 8.0 + 2.0); + wave += cos(aAngle * waveFreq * 0.5 - uTime * 2.0) * (uLow * 6.0); + wave += sin(aAngle * waveFreq * 2.0 + uTime * 5.0) * (uHigh * 4.0); + + float radius = baseRadius + wave; + + // Calculate position + float x = cos(aAngle) * radius; + float y = sin(aAngle) * radius; + float z = sin(aAngle * 3.0 + uTime) * 3.0 + uLow * 8.0; + + // Slow rotation + float rotAngle = uTime * 0.1; + float cosR = cos(rotAngle); + float sinR = sin(rotAngle); + float newX = x * cosR - y * sinR; + float newY = x * sinR + y * cosR; + x = newX; + y = newY; + + // Mouse offset + x -= uMouseX * 5.0; + y -= uMouseY * 4.0; + + vec3 pos = vec3(x, y, z); + vec4 mvPosition = modelViewMatrix * vec4(pos, 1.0); + + // Size based on audio intensity + float size = 2.5 + uLow * 1.5 + abs(wave) * 0.1; + gl_PointSize = size * (300.0 / -mvPosition.z); + + // Alpha - outer rings slightly dimmer + vAlpha = 0.8 - aRing * 0.15 + uLow * 0.2; + vRing = aRing; + + gl_Position = projectionMatrix * mvPosition; +} +`; + +const fragmentShader = ` +varying float vAlpha; +varying float vRing; + +void main() { + float dist = length(gl_PointCoord - vec2(0.5)); + if (dist > 0.5) discard; + + float alpha = 1.0 - smoothstep(0.1, 0.5, dist); + alpha *= vAlpha; + + vec3 color = vec3(1.0); + + gl_FragColor = vec4(color, alpha); +} +`; + +export class WaveformVisualizer extends BaseVisualizer { + static name = 'Waveform'; + + constructor(scene) { + super(scene); + this.rings = 4; + this.pointsPerRing = 200; + this.init(); + } + + init() { + const count = this.rings * this.pointsPerRing; + const positions = new Float32Array(count * 3); + const angles = new Float32Array(count); + const rings = new Float32Array(count); + + let idx = 0; + for (let ring = 0; ring < this.rings; ring++) { + for (let i = 0; i < this.pointsPerRing; i++) { + const angle = (i / this.pointsPerRing) * Math.PI * 2; + + positions[idx * 3] = 0; + positions[idx * 3 + 1] = 0; + positions[idx * 3 + 2] = 0; + + angles[idx] = angle; + rings[idx] = ring; + idx++; + } + } + + const geometry = new THREE.BufferGeometry(); + geometry.setAttribute('position', new THREE.BufferAttribute(positions, 3)); + geometry.setAttribute('aAngle', new THREE.BufferAttribute(angles, 1)); + geometry.setAttribute('aRing', new THREE.BufferAttribute(rings, 1)); + + this.material = this.createShaderMaterial(vertexShader, fragmentShader, { + uMouseX: { value: 0 }, + uMouseY: { value: 0 } + }); + + this.mesh = new THREE.Points(geometry, this.material); + } + + update(bands, time, mouse = { x: 0, y: 0 }) { + if (!this.material) return; + + this.material.uniforms.uTime.value = time; + this.material.uniforms.uLow.value = bands.low || 0; + this.material.uniforms.uMid.value = bands.mid || 0; + this.material.uniforms.uHigh.value = bands.high || 0; + this.material.uniforms.uMouseX.value = mouse.x; + this.material.uniforms.uMouseY.value = mouse.y; + } +} + +export default WaveformVisualizer;