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>
This commit is contained in:
158
assets/js/visualizer/galaxy.js
Normal file
158
assets/js/visualizer/galaxy.js
Normal file
@@ -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;
|
||||
134
assets/js/visualizer/grid.js
Normal file
134
assets/js/visualizer/grid.js
Normal file
@@ -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;
|
||||
152
assets/js/visualizer/kaleidoscope.js
Normal file
152
assets/js/visualizer/kaleidoscope.js
Normal file
@@ -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;
|
||||
@@ -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 {
|
||||
|
||||
133
assets/js/visualizer/starfield.js
Normal file
133
assets/js/visualizer/starfield.js
Normal file
@@ -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;
|
||||
138
assets/js/visualizer/vortex.js
Normal file
138
assets/js/visualizer/vortex.js
Normal file
@@ -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;
|
||||
141
assets/js/visualizer/waveform.js
Normal file
141
assets/js/visualizer/waveform.js
Normal file
@@ -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;
|
||||
Reference in New Issue
Block a user