Files
pivoine.art/assets/js/visualizer/grid.js
Sebastian Krüger 8d9d47cea7 feat: add 6 new audio visualizers
- 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 <noreply@anthropic.com>
2025-11-30 10:10:54 +01:00

135 lines
3.4 KiB
JavaScript

/**
* 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;