Source identity
| Field | Recorded value |
|---|---|
| Source type | GitHub source |
| Original record | Janady13/Jennys-Drumste / 3d-drum-visualizer.js |
| Date | Not specified in source |
| Repository | Janady13/Jennys-Drumste |
| Revision | 01c36ac1a180916e11864b4573b8c1999e2c5db3 |
| Branch | claude/complete-integration-feature-011CUkTzyyk8bKmXaRXJAR61 |
| Path or archive member | 3d-drum-visualizer.js |
| Original SHA-256 | 4e1368204c60f8d610a3e1efd19d66aec3903b01afaf78587e719565ed4112ed |
| Git blob SHA | 68412b7e0542180e8a9b6548fdc9ef087e8b09dd |
| Reading-copy SHA-256 | 4e1368204c60f8d610a3e1efd19d66aec3903b01afaf78587e719565ed4112ed |
Recovered source transcript
Download complete source text ↓719 lines · 21.6 KB
/**
* 3D DRUM VISUALIZER WITH WEBGL
* Advanced graphics engine for immersive drum learning
*
* Features:
* - 3D drum kit with realistic physics
* - Particle systems for each hit
* - Audio-reactive visualizations
* - Dynamic lighting and bloom effects
* - Performance optimized for 60fps
*/
class DrumVisualizer3D {
constructor(canvasId) {
this.canvas = document.getElementById(canvasId);
if (!this.canvas) {
console.error('Canvas not found:', canvasId);
return;
}
this.gl = this.canvas.getContext('webgl2', {
alpha: true,
antialias: true,
preserveDrawingBuffer: false
});
if (!this.gl) {
console.error('WebGL 2 not supported');
return;
}
// Scene setup
this.camera = {
position: [0, 2, 5],
rotation: [0, 0, 0],
fov: 45,
near: 0.1,
far: 100
};
this.lights = [];
this.particles = [];
this.drumMeshes = new Map();
this.animations = [];
// Performance tracking
this.fps = 60;
this.lastFrame = 0;
this.frameCount = 0;
this.init();
console.log('🎨 3D Visualizer initialized');
}
/**
* Initialize WebGL scene
*/
init() {
const gl = this.gl;
// Setup viewport
this.resize();
window.addEventListener('resize', () => this.resize());
// Enable depth testing
gl.enable(gl.DEPTH_TEST);
gl.depthFunc(gl.LEQUAL);
// Enable blending for particles
gl.enable(gl.BLEND);
gl.blendFunc(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA);
// Clear color
gl.clearColor(0.02, 0.02, 0.05, 1.0);
// Initialize shaders
this.initShaders();
// Create 3D drum kit
this.createDrumKit();
// Setup lighting
this.setupLighting();
// Start render loop
this.render();
}
/**
* Initialize shader programs
*/
initShaders() {
const gl = this.gl;
// Vertex shader for drum meshes
const drumVertexShader = `#version 300 es
precision highp float;
in vec3 aPosition;
in vec3 aNormal;
in vec2 aTexCoord;
uniform mat4 uModelMatrix;
uniform mat4 uViewMatrix;
uniform mat4 uProjectionMatrix;
uniform mat3 uNormalMatrix;
out vec3 vNormal;
out vec3 vPosition;
out vec2 vTexCoord;
void main() {
vec4 worldPosition = uModelMatrix * vec4(aPosition, 1.0);
vPosition = worldPosition.xyz;
vNormal = normalize(uNormalMatrix * aNormal);
vTexCoord = aTexCoord;
gl_Position = uProjectionMatrix * uViewMatrix * worldPosition;
}
`;
// Fragment shader with Phong lighting
const drumFragmentShader = `#version 300 es
precision highp float;
in vec3 vNormal;
in vec3 vPosition;
in vec2 vTexCoord;
uniform vec3 uLightPosition;
uniform vec3 uCameraPosition;
uniform vec3 uDrumColor;
uniform float uGlow;
uniform float uMetallic;
out vec4 fragColor;
void main() {
// Lighting calculations
vec3 normal = normalize(vNormal);
vec3 lightDir = normalize(uLightPosition - vPosition);
vec3 viewDir = normalize(uCameraPosition - vPosition);
vec3 reflectDir = reflect(-lightDir, normal);
// Diffuse
float diff = max(dot(normal, lightDir), 0.0);
// Specular
float spec = pow(max(dot(viewDir, reflectDir), 0.0), 32.0);
// Rim lighting for depth
float rim = 1.0 - max(dot(viewDir, normal), 0.0);
rim = pow(rim, 3.0);
// Combine
vec3 ambient = uDrumColor * 0.3;
vec3 diffuse = uDrumColor * diff * 0.6;
vec3 specular = vec3(1.0) * spec * uMetallic;
vec3 rimLight = vec3(0.0, 1.0, 1.0) * rim * 0.5;
vec3 color = ambient + diffuse + specular + rimLight;
// Add glow effect when hit
color += uDrumColor * uGlow * 2.0;
fragColor = vec4(color, 1.0);
}
`;
// Particle shader
const particleVertexShader = `#version 300 es
precision highp float;
in vec3 aPosition;
in float aSize;
in vec4 aColor;
uniform mat4 uViewMatrix;
uniform mat4 uProjectionMatrix;
out vec4 vColor;
void main() {
vColor = aColor;
gl_Position = uProjectionMatrix * uViewMatrix * vec4(aPosition, 1.0);
gl_PointSize = aSize;
}
`;
const particleFragmentShader = `#version 300 es
precision highp float;
in vec4 vColor;
out vec4 fragColor;
void main() {
// Circular particle
vec2 coord = gl_PointCoord - vec2(0.5);
float dist = length(coord);
if (dist > 0.5) discard;
float alpha = vColor.a * (1.0 - dist * 2.0);
fragColor = vec4(vColor.rgb, alpha);
}
`;
// Compile shaders
this.drumProgram = this.createProgram(drumVertexShader, drumFragmentShader);
this.particleProgram = this.createProgram(particleVertexShader, particleFragmentShader);
}
/**
* Create shader program
*/
createProgram(vertexSource, fragmentSource) {
const gl = this.gl;
const vertexShader = this.compileShader(gl.VERTEX_SHADER, vertexSource);
const fragmentShader = this.compileShader(gl.FRAGMENT_SHADER, fragmentSource);
const program = gl.createProgram();
gl.attachShader(program, vertexShader);
gl.attachShader(program, fragmentShader);
gl.linkProgram(program);
if (!gl.getProgramParameter(program, gl.LINK_STATUS)) {
console.error('Program link error:', gl.getProgramInfoLog(program));
return null;
}
return program;
}
/**
* Compile shader
*/
compileShader(type, source) {
const gl = this.gl;
const shader = gl.createShader(type);
gl.shaderSource(shader, source);
gl.compileShader(shader);
if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) {
console.error('Shader compile error:', gl.getShaderInfoLog(shader));
gl.deleteShader(shader);
return null;
}
return shader;
}
/**
* Create 3D drum kit geometry
*/
createDrumKit() {
// Drum positions (X, Y, Z)
const drumPositions = {
'Kick': [0, -0.5, 0],
'Snare': [-0.5, 0, 0.5],
'Hi-Hat': [-1, 0.3, 0.8],
'Tom 1': [0.3, 0.2, 0.3],
'Tom 2': [0.7, 0.1, 0.2],
'Tom 3': [0.9, -0.1, -0.3],
'Crash': [-1.2, 0.8, 0.5],
'Ride': [1.2, 0.7, 0]
};
// Drum colors (RGB)
const drumColors = {
'Kick': [0.8, 0.2, 0.2], // Red
'Snare': [0.2, 0.8, 1.0], // Cyan
'Hi-Hat': [1.0, 1.0, 0.2], // Yellow
'Tom 1': [1.0, 0.5, 0.0], // Orange
'Tom 2': [0.8, 0.3, 1.0], // Purple
'Tom 3': [0.3, 1.0, 0.3], // Green
'Crash': [1.0, 0.2, 1.0], // Magenta
'Ride': [0.2, 1.0, 0.8] // Teal
};
// Create mesh for each drum
Object.entries(drumPositions).forEach(([name, position]) => {
const isCymbal = name.includes('Crash') || name.includes('Ride') || name.includes('Hi-Hat');
const geometry = isCymbal ? this.createCylinderGeometry(0.4, 0.02, 32) :
this.createCylinderGeometry(0.3, 0.4, 32);
this.drumMeshes.set(name, {
name,
geometry,
position,
color: drumColors[name],
rotation: [0, 0, 0],
scale: [1, 1, 1],
glow: 0,
metallic: isCymbal ? 0.9 : 0.3
});
});
}
/**
* Create cylinder geometry (for drums and cymbals)
*/
createCylinderGeometry(radius, height, segments) {
const positions = [];
const normals = [];
const indices = [];
// Top and bottom caps
for (let y = 0; y <= 1; y++) {
const yPos = y === 0 ? -height / 2 : height / 2;
for (let i = 0; i <= segments; i++) {
const angle = (i / segments) * Math.PI * 2;
const x = Math.cos(angle) * radius;
const z = Math.sin(angle) * radius;
positions.push(x, yPos, z);
normals.push(0, y === 0 ? -1 : 1, 0);
}
}
// Side
for (let i = 0; i <= segments; i++) {
const angle = (i / segments) * Math.PI * 2;
const x = Math.cos(angle) * radius;
const z = Math.sin(angle) * radius;
positions.push(x, -height / 2, z);
normals.push(Math.cos(angle), 0, Math.sin(angle));
positions.push(x, height / 2, z);
normals.push(Math.cos(angle), 0, Math.sin(angle));
}
// Create indices
const capStart1 = 0;
const capStart2 = segments + 1;
const sideStart = (segments + 1) * 2;
// Top and bottom cap indices
for (let i = 1; i < segments; i++) {
indices.push(capStart1, capStart1 + i, capStart1 + i + 1);
indices.push(capStart2, capStart2 + i + 1, capStart2 + i);
}
// Side indices
for (let i = 0; i < segments; i++) {
const base = sideStart + i * 2;
indices.push(base, base + 1, base + 2);
indices.push(base + 1, base + 3, base + 2);
}
return {
positions: new Float32Array(positions),
normals: new Float32Array(normals),
indices: new Uint16Array(indices)
};
}
/**
* Setup dynamic lighting
*/
setupLighting() {
this.lights = [
{ position: [2, 4, 3], color: [1.0, 1.0, 1.0], intensity: 1.0 },
{ position: [-2, 3, 2], color: [0.0, 1.0, 1.0], intensity: 0.5 },
{ position: [0, 5, -2], color: [1.0, 0.0, 1.0], intensity: 0.3 }
];
}
/**
* Trigger drum hit animation
*/
hitDrum(drumName, velocity = 80) {
const drum = this.drumMeshes.get(drumName);
if (!drum) return;
// Add glow effect
drum.glow = velocity / 127;
// Animate glow decay
this.animations.push({
target: drum,
property: 'glow',
start: drum.glow,
end: 0,
duration: 500,
startTime: Date.now()
});
// Create particles
this.createParticles(drum.position, drum.color, velocity);
// Add slight bounce animation
this.animations.push({
target: drum,
property: 'position',
axis: 1, // Y axis
start: drum.position[1],
end: drum.position[1] - 0.05,
duration: 100,
startTime: Date.now(),
bounce: true
});
}
/**
* Create particle explosion for drum hit
*/
createParticles(position, color, velocity) {
const particleCount = Math.floor(velocity / 4) + 10;
for (let i = 0; i < particleCount; i++) {
const angle = Math.random() * Math.PI * 2;
const speed = (Math.random() * 0.02 + 0.01) * (velocity / 127);
const spread = Math.random() * 0.5 + 0.5;
this.particles.push({
position: [...position],
velocity: [
Math.cos(angle) * speed * spread,
Math.random() * speed * 2,
Math.sin(angle) * speed * spread
],
color: [...color, 1.0],
size: Math.random() * 10 + 5,
life: 1.0,
decay: 0.02
});
}
}
/**
* Update animations
*/
updateAnimations(deltaTime) {
const now = Date.now();
this.animations = this.animations.filter(anim => {
const elapsed = now - anim.startTime;
const progress = Math.min(elapsed / anim.duration, 1);
if (anim.property === 'glow') {
anim.target[anim.property] = anim.start + (anim.end - anim.start) * progress;
} else if (anim.property === 'position') {
const value = anim.start + (anim.end - anim.start) * progress;
anim.target[anim.property][anim.axis] = value;
// Bounce back
if (progress >= 1 && anim.bounce && !anim.bounced) {
anim.bounced = true;
anim.startTime = now;
[anim.start, anim.end] = [anim.end, anim.start];
return true;
}
}
return progress < 1 || (anim.bounce && !anim.bounced);
});
// Update particles
this.particles = this.particles.filter(particle => {
particle.position[0] += particle.velocity[0];
particle.position[1] += particle.velocity[1];
particle.position[2] += particle.velocity[2];
particle.velocity[1] -= 0.001; // Gravity
particle.life -= particle.decay;
particle.color[3] = particle.life;
return particle.life > 0;
});
}
/**
* Render scene
*/
render(timestamp = 0) {
const deltaTime = timestamp - this.lastFrame;
this.lastFrame = timestamp;
requestAnimationFrame(t => this.render(t));
const gl = this.gl;
// Update animations
this.updateAnimations(deltaTime);
// Clear
gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT);
// Update camera (smooth rotation)
const time = timestamp * 0.0001;
this.camera.position[0] = Math.sin(time) * 5;
this.camera.position[2] = Math.cos(time) * 5;
// Calculate matrices
const viewMatrix = this.createViewMatrix();
const projectionMatrix = this.createProjectionMatrix();
// Render drums
this.renderDrums(viewMatrix, projectionMatrix);
// Render particles
this.renderParticles(viewMatrix, projectionMatrix);
// Update FPS
this.frameCount++;
if (this.frameCount % 60 === 0) {
this.fps = Math.round(1000 / deltaTime);
}
}
/**
* Render drum meshes
*/
renderDrums(viewMatrix, projectionMatrix) {
const gl = this.gl;
gl.useProgram(this.drumProgram);
// Get uniform locations
const uModelMatrix = gl.getUniformLocation(this.drumProgram, 'uModelMatrix');
const uViewMatrix = gl.getUniformLocation(this.drumProgram, 'uViewMatrix');
const uProjectionMatrix = gl.getUniformLocation(this.drumProgram, 'uProjectionMatrix');
const uNormalMatrix = gl.getUniformLocation(this.drumProgram, 'uNormalMatrix');
const uLightPosition = gl.getUniformLocation(this.drumProgram, 'uLightPosition');
const uCameraPosition = gl.getUniformLocation(this.drumProgram, 'uCameraPosition');
const uDrumColor = gl.getUniformLocation(this.drumProgram, 'uDrumColor');
const uGlow = gl.getUniformLocation(this.drumProgram, 'uGlow');
const uMetallic = gl.getUniformLocation(this.drumProgram, 'uMetallic');
// Set uniforms
gl.uniformMatrix4fv(uViewMatrix, false, viewMatrix);
gl.uniformMatrix4fv(uProjectionMatrix, false, projectionMatrix);
gl.uniform3fv(uLightPosition, this.lights[0].position);
gl.uniform3fv(uCameraPosition, this.camera.position);
// Render each drum
this.drumMeshes.forEach(drum => {
const modelMatrix = this.createModelMatrix(drum);
const normalMatrix = this.createNormalMatrix(modelMatrix);
gl.uniformMatrix4fv(uModelMatrix, false, modelMatrix);
gl.uniformMatrix3fv(uNormalMatrix, false, normalMatrix);
gl.uniform3fv(uDrumColor, drum.color);
gl.uniform1f(uGlow, drum.glow);
gl.uniform1f(uMetallic, drum.metallic);
this.renderMesh(drum.geometry);
});
}
/**
* Render particles
*/
renderParticles(viewMatrix, projectionMatrix) {
if (this.particles.length === 0) return;
const gl = this.gl;
gl.useProgram(this.particleProgram);
const positions = [];
const sizes = [];
const colors = [];
this.particles.forEach(particle => {
positions.push(...particle.position);
sizes.push(particle.size);
colors.push(...particle.color);
});
// Create buffers
const positionBuffer = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, positionBuffer);
gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(positions), gl.DYNAMIC_DRAW);
const aPosition = gl.getAttribLocation(this.particleProgram, 'aPosition');
gl.enableVertexAttribArray(aPosition);
gl.vertexAttribPointer(aPosition, 3, gl.FLOAT, false, 0, 0);
// Set uniforms
const uViewMatrix = gl.getUniformLocation(this.particleProgram, 'uViewMatrix');
const uProjectionMatrix = gl.getUniformLocation(this.particleProgram, 'uProjectionMatrix');
gl.uniformMatrix4fv(uViewMatrix, false, viewMatrix);
gl.uniformMatrix4fv(uProjectionMatrix, false, projectionMatrix);
// Draw particles
gl.drawArrays(gl.POINTS, 0, this.particles.length);
gl.deleteBuffer(positionBuffer);
}
/**
* Helper: Render mesh
*/
renderMesh(geometry) {
const gl = this.gl;
// Position buffer
const positionBuffer = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, positionBuffer);
gl.bufferData(gl.ARRAY_BUFFER, geometry.positions, gl.STATIC_DRAW);
const aPosition = gl.getAttribLocation(this.drumProgram, 'aPosition');
gl.enableVertexAttribArray(aPosition);
gl.vertexAttribPointer(aPosition, 3, gl.FLOAT, false, 0, 0);
// Normal buffer
const normalBuffer = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, normalBuffer);
gl.bufferData(gl.ARRAY_BUFFER, geometry.normals, gl.STATIC_DRAW);
const aNormal = gl.getAttribLocation(this.drumProgram, 'aNormal');
gl.enableVertexAttribArray(aNormal);
gl.vertexAttribPointer(aNormal, 3, gl.FLOAT, false, 0, 0);
// Index buffer
const indexBuffer = gl.createBuffer();
gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, indexBuffer);
gl.bufferData(gl.ELEMENT_ARRAY_BUFFER, geometry.indices, gl.STATIC_DRAW);
// Draw
gl.drawElements(gl.TRIANGLES, geometry.indices.length, gl.UNSIGNED_SHORT, 0);
// Cleanup
gl.deleteBuffer(positionBuffer);
gl.deleteBuffer(normalBuffer);
gl.deleteBuffer(indexBuffer);
}
/**
* Matrix operations
*/
createModelMatrix(drum) {
const matrix = this.createIdentityMatrix();
this.translate(matrix, drum.position);
this.scale(matrix, drum.scale);
return matrix;
}
createViewMatrix() {
const matrix = this.createIdentityMatrix();
this.translate(matrix, this.camera.position.map(v => -v));
return matrix;
}
createProjectionMatrix() {
const aspect = this.canvas.width / this.canvas.height;
return this.perspective(this.camera.fov, aspect, this.camera.near, this.camera.far);
}
createNormalMatrix(modelMatrix) {
// Simplified 3x3 normal matrix from 4x4 model matrix
return new Float32Array([
modelMatrix[0], modelMatrix[1], modelMatrix[2],
modelMatrix[4], modelMatrix[5], modelMatrix[6],
modelMatrix[8], modelMatrix[9], modelMatrix[10]
]);
}
createIdentityMatrix() {
return new Float32Array([
1, 0, 0, 0,
0, 1, 0, 0,
0, 0, 1, 0,
0, 0, 0, 1
]);
}
translate(matrix, vector) {
matrix[12] += vector[0];
matrix[13] += vector[1];
matrix[14] += vector[2];
}
scale(matrix, vector) {
matrix[0] *= vector[0];
matrix[5] *= vector[1];
matrix[10] *= vector[2];
}
perspective(fov, aspect, near, far) {
const f = 1.0 / Math.tan(fov * Math.PI / 360);
const rangeInv = 1.0 / (near - far);
return new Float32Array([
f / aspect, 0, 0, 0,
0, f, 0, 0,
0, 0, (near + far) * rangeInv, -1,
0, 0, near * far * rangeInv * 2, 0
]);
}
resize() {
this.canvas.width = this.canvas.clientWidth * window.devicePixelRatio;
this.canvas.height = this.canvas.clientHeight * window.devicePixelRatio;
this.gl.viewport(0, 0, this.canvas.width, this.canvas.height);
}
/**
* Get current FPS
*/
getFPS() {
return this.fps;
}
/**
* Cleanup
*/
destroy() {
// Cancel animation frame
this.gl.getExtension('WEBGL_lose_context').loseContext();
}
}
// Export for use
if (typeof module !== 'undefined' && module.exports) {
module.exports = DrumVisualizer3D;
}