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