Marimba-v1 / index.html
MySafeCode's picture
Upload folder using huggingface_hub
9e0cc2f verified
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>3D Marimba - Interactive Musical Instrument</title>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
overflow: hidden;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
position: relative;
}
#canvas {
display: block;
cursor: grab;
}
#canvas:active {
cursor: grabbing;
}
.ui-overlay {
position: absolute;
top: 0;
left: 0;
right: 0;
padding: 20px;
background: linear-gradient(180deg, rgba(0,0,0,0.5) 0%, transparent 100%);
pointer-events: none;
z-index: 10;
}
.header {
display: flex;
justify-content: space-between;
align-items: center;
color: white;
pointer-events: auto;
}
.title {
font-size: 28px;
font-weight: bold;
text-shadow: 2px 2px 4px rgba(0,0,0,0.3);
display: flex;
align-items: center;
gap: 15px;
}
.title h1 {
font-size: 32px;
background: linear-gradient(45deg, #fff, #ffd700);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
}
.controls {
display: flex;
gap: 15px;
flex-wrap: wrap;
}
.control-btn {
padding: 10px 20px;
background: rgba(255,255,255,0.2);
backdrop-filter: blur(10px);
border: 2px solid rgba(255,255,255,0.3);
border-radius: 50px;
color: white;
font-weight: 600;
cursor: pointer;
transition: all 0.3s ease;
pointer-events: auto;
}
.control-btn:hover {
background: rgba(255,255,255,0.3);
transform: translateY(-2px);
box-shadow: 0 5px 15px rgba(0,0,0,0.2);
}
.control-btn.active {
background: linear-gradient(45deg, #667eea, #764ba2);
border-color: transparent;
}
.info-panel {
position: absolute;
bottom: 20px;
left: 20px;
background: rgba(0,0,0,0.7);
backdrop-filter: blur(10px);
padding: 20px;
border-radius: 15px;
color: white;
max-width: 300px;
pointer-events: auto;
}
.info-panel h3 {
margin-bottom: 10px;
color: #ffd700;
}
.info-panel p {
margin: 5px 0;
font-size: 14px;
opacity: 0.9;
}
.note-display {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
font-size: 72px;
font-weight: bold;
color: white;
text-shadow: 3px 3px 6px rgba(0,0,0,0.5);
opacity: 0;
pointer-events: none;
transition: opacity 0.2s ease;
z-index: 20;
}
.note-display.show {
animation: notePop 0.8s ease;
}
@keyframes notePop {
0% {
opacity: 0;
transform: translate(-50%, -50%) scale(0.5);
}
50% {
opacity: 1;
transform: translate(-50%, -50%) scale(1.2);
}
100% {
opacity: 0;
transform: translate(-50%, -50%) scale(1);
}
}
.volume-control {
position: absolute;
bottom: 20px;
right: 20px;
background: rgba(0,0,0,0.7);
backdrop-filter: blur(10px);
padding: 15px;
border-radius: 15px;
color: white;
pointer-events: auto;
}
.volume-slider {
width: 150px;
margin-top: 10px;
}
.loading {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
color: white;
font-size: 24px;
font-weight: bold;
text-align: center;
}
.loading-spinner {
width: 50px;
height: 50px;
border: 3px solid rgba(255,255,255,0.3);
border-top-color: white;
border-radius: 50%;
animation: spin 1s linear infinite;
margin: 20px auto;
}
@keyframes spin {
to { transform: rotate(360deg); }
}
@media (max-width: 768px) {
.title h1 {
font-size: 24px;
}
.info-panel {
display: none;
}
.controls {
gap: 10px;
}
.control-btn {
padding: 8px 15px;
font-size: 14px;
}
}
</style>
</head>
<body>
<div class="loading" id="loading">
<div class="loading-spinner"></div>
Loading 3D Marimba...
</div>
<div class="ui-overlay">
<div class="header">
<div class="title">
<h1>3D Marimba</h1>
<span style="opacity: 0.8;">Interactive Instrument</span>
</div>
<div class="controls">
<button class="control-btn" id="rotateBtn">Auto Rotate</button>
<button class="control-btn" id="resetBtn">Reset View</button>
<button class="control-btn active" id="soundBtn">Sound On</button>
</div>
</div>
</div>
<div class="info-panel">
<h3>How to Play</h3>
<p>🎵 Click on bars to play notes</p>
<p>🖱️ Drag to rotate the view</p>
<p>🔍 Scroll to zoom in/out</p>
<p>⌨️ Press keys A-L for quick play</p>
</div>
<div class="volume-control">
<label for="volume">Volume</label>
<input type="range" id="volume" class="volume-slider" min="0" max="100" value="70">
</div>
<div class="note-display" id="noteDisplay"></div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/r128/three.min.js"></script>
<script>
// Global variables
let scene, camera, renderer, raycaster, mouse;
let marimbaBars = [];
let mallets = [];
let audioContext;
let masterGainNode;
let autoRotate = false;
let soundEnabled = true;
let isDragging = false;
let previousMousePosition = { x: 0, y: 0 };
// Musical notes frequencies (pentatonic scale for pleasant sound)
const notes = [
{ note: 'C', freq: 261.63, color: '#FF6B6B' },
{ note: 'D', freq: 293.66, color: '#4ECDC4' },
{ note: 'E', freq: 329.63, color: '#45B7D1' },
{ note: 'G', freq: 392.00, color: '#96CEB4' },
{ note: 'A', freq: 440.00, color: '#FFEAA7' },
{ note: 'C', freq: 523.25, color: '#DFE6E9' },
{ note: 'D', freq: 587.33, color: '#74B9FF' },
{ note: 'E', freq: 659.25, color: '#A29BFE' },
{ note: 'G', freq: 783.99, color: '#FD79A8' },
{ note: 'A', freq: 880.00, color: '#FDCB6E' },
{ note: 'C', freq: 1046.50, color: '#6C5CE7' },
{ note: 'D', freq: 1174.66, color: '#00B894' }
];
// Initialize Three.js
function init() {
// Scene setup
scene = new THREE.Scene();
scene.fog = new THREE.Fog(0x764ba2, 10, 50);
// Camera setup
camera = new THREE.PerspectiveCamera(
75,
window.innerWidth / window.innerHeight,
0.1,
1000
);
camera.position.set(0, 5, 12);
camera.lookAt(0, 0, 0);
// Renderer setup
renderer = new THREE.WebGLRenderer({ antialias: true, alpha: true });
renderer.setSize(window.innerWidth, window.innerHeight);
renderer.shadowMap.enabled = true;
renderer.shadowMap.type = THREE.PCFSoftShadowMap;
renderer.toneMapping = THREE.ACESFilmicToneMapping;
renderer.toneMappingExposure = 1.2;
document.body.appendChild(renderer.domElement);
// Raycaster for mouse interaction
raycaster = new THREE.Raycaster();
mouse = new THREE.Vector2();
// Lighting
setupLighting();
// Create marimba
createMarimba();
// Create mallets
createMallets();
// Event listeners
setupEventListeners();
// Initialize audio
initAudio();
// Hide loading
document.getElementById('loading').style.display = 'none';
// Start animation loop
animate();
}
function setupLighting() {
// Ambient light
const ambientLight = new THREE.AmbientLight(0xffffff, 0.4);
scene.add(ambientLight);
// Main directional light
const directionalLight = new THREE.DirectionalLight(0xffffff, 1);
directionalLight.position.set(5, 10, 5);
directionalLight.castShadow = true;
directionalLight.shadow.camera.near = 0.1;
directionalLight.shadow.camera.far = 50;
directionalLight.shadow.camera.left = -15;
directionalLight.shadow.camera.right = 15;
directionalLight.shadow.camera.top = 15;
directionalLight.shadow.camera.bottom = -15;
directionalLight.shadow.mapSize.width = 2048;
directionalLight.shadow.mapSize.height = 2048;
scene.add(directionalLight);
// Point lights for ambiance
const pointLight1 = new THREE.PointLight(0xff6b6b, 0.5, 10);
pointLight1.position.set(-5, 3, 3);
scene.add(pointLight1);
const pointLight2 = new THREE.PointLight(0x4ecdc4, 0.5, 10);
pointLight2.position.set(5, 3, 3);
scene.add(pointLight2);
}
function createMarimba() {
const barGroup = new THREE.Group();
// Create resonator box
const resonatorGeometry = new THREE.BoxGeometry(14, 1, 4);
const resonatorMaterial = new THREE.MeshPhongMaterial({
color: 0x8B4513,
shininess: 30
});
const resonator = new THREE.Mesh(resonatorGeometry, resonatorMaterial);
resonator.position.y = -0.5;
resonator.receiveShadow = true;
barGroup.add(resonator);
// Create bars
const barWidth = 1;
const barDepth = 0.3;
const barSpacing = 0.1;
const totalWidth = notes.length * barWidth + (notes.length - 1) * barSpacing;
notes.forEach((note, index) => {
// Calculate bar dimensions (higher notes are shorter)
const barHeight = 0.2;
const barLength = 8 - (index * 0.3);
// Create bar geometry
const barGeometry = new THREE.BoxGeometry(barWidth, barHeight, barLength);
// Create gradient material for each bar
const barMaterial = new THREE.MeshPhongMaterial({
color: 0xD2691E,
emissive: note.color,
emissiveIntensity: 0,
shininess: 100,
specular: 0x222222
});
const bar = new THREE.Mesh(barGeometry, barMaterial);
// Position bars
const xPosition = (index - notes.length / 2 + 0.5) * (barWidth + barSpacing);
bar.position.set(xPosition, 0.5, 0);
bar.castShadow = true;
bar.receiveShadow = true;
// Store note data
bar.userData = {
note: note.note,
frequency: note.freq,
index: index,
originalColor: 0xD2691E,
hitColor: note.color
};
marimbaBars.push(bar);
barGroup.add(bar);
});
scene.add(barGroup);
}
function createMallets() {
const malletGroup = new THREE.Group();
// Create two mallets
for (let i = 0; i < 2; i++) {
// Handle
const handleGeometry = new THREE.CylinderGeometry(0.05, 0.05, 2, 8);
const handleMaterial = new THREE.MeshPhongMaterial({ color: 0x8B4513 });
const handle = new THREE.Mesh(handleGeometry, handleMaterial);
// Head
const headGeometry = new THREE.SphereGeometry(0.3, 16, 16);
const headMaterial = new THREE.MeshPhongMaterial({
color: i === 0 ? 0xFF0000 : 0x0000FF,
shininess: 100
});
const head = new THREE.Mesh(headGeometry, headMaterial);
head.position.y = 1;
const mallet = new THREE.Group();
mallet.add(handle);
mallet.add(head);
mallet.position.set(i === 0 ? -3 : 3, 3, 5);
mallet.rotation.z = i === 0 ? -0.5 : 0.5;
mallets.push(mallet);
malletGroup.add(mallet);
}
scene.add(malletGroup);
}
function initAudio() {
// Create audio context
audioContext = new (window.AudioContext || window.webkitAudioContext)();
// Create master gain node
masterGainNode = audioContext.createGain();
masterGainNode.connect(audioContext.destination);
masterGainNode.gain.value = 0.7;
}
function playNote(frequency, bar) {
if (!soundEnabled) return;
// Create oscillator
const oscillator = audioContext.createOscillator();
const gainNode = audioContext.createGain();
oscillator.connect(gainNode);
gainNode.connect(masterGainNode);
// Set frequency and waveform
oscillator.frequency.value = frequency;
oscillator.type = 'sine';
// ADSR envelope
const now = audioContext.currentTime;
gainNode.gain.setValueAtTime(0, now);
gainNode.gain.linearRampToValueAtTime(0.3, now + 0.01); // Attack
gainNode.gain.exponentialRampToValueAtTime(0.2, now + 0.1); // Decay/Sustain
gainNode.gain.exponentialRampToValueAtTime(0.01, now + 1.5); // Release
// Start and stop
oscillator.start(now);
oscillator.stop(now + 1.5);
// Add harmonics for richer sound
const harmonic = audioContext.createOscillator();
const harmonicGain = audioContext.createGain();
harmonic.connect(harmonicGain);
harmonicGain.connect(masterGainNode);
harmonic.frequency.value = frequency * 2;
harmonic.type = 'triangle';
harmonicGain.gain.setValueAtTime(0, now);
harmonicGain.gain.linearRampToValueAtTime(0.1, now + 0.01);
harmonicGain.gain.exponentialRampToValueAtTime(0.01, now + 1);
harmonic.start(now);
harmonic.stop(now + 1);
// Visual feedback
if (bar) {
animateBarHit(bar);
}
// Show note display
showNoteDisplay(bar.userData.note);
}
function animateBarHit(bar) {
// Animate bar color
bar.material.emissiveIntensity = 0.5;
// Animate bar movement
const originalY = bar.position.y;
let animationTime = 0;
const animateHit = () => {
animationTime += 0.05;
if (animationTime < 0.2) {
// Move down
bar.position.y = originalY - Math.sin(animationTime * Math.PI / 0.2) * 0.1;
} else if (animationTime < 0.4) {
// Move back up
bar.position.y = originalY - Math.sin((animationTime - 0.2) * Math.PI / 0.2) * 0.05;
} else {
bar.position.y = originalY;
bar.material.emissiveIntensity = 0;
return;
}
requestAnimationFrame(animateHit);
};
animateHit();
// Animate mallet
const mallet = mallets[bar.userData.index % 2];
animateMalletHit(mallet, bar.position);
}
function animateMalletHit(mallet, targetPosition) {
const originalPosition = mallet.position.clone();
const originalRotation = mallet.rotation.clone();
let animationTime = 0;
const animateMallet = () => {
animationTime += 0.05;
if (animationTime < 0.3) {
// Move towards target
const t = animationTime / 0.3;
mallet.position.lerpVectors(originalPosition, targetPosition.clone().add(new THREE.Vector3(0, 1, 0)), t);
mallet.rotation.z = originalRotation.z + Math.sin(t * Math.PI) * 0.5;
} else if (animationTime < 0.6) {
// Return to original position
const t = (animationTime - 0.3) / 0.3;
mallet.position.lerpVectors(targetPosition.clone().add(new THREE.Vector3(0, 1, 0)), originalPosition, t);
mallet.rotation.z = originalRotation.z + Math.sin((1 - t) * Math.PI) * 0.5;
} else {
mallet.position.copy(originalPosition);
mallet.rotation.copy(originalRotation);
return;
}
requestAnimationFrame(animateMallet);
};
animateMallet();
}
function showNoteDisplay(note) {
const display = document.getElementById('noteDisplay');
display.textContent = note;
display.classList.add('show');
setTimeout(() => {
display.classList.remove('show');
}, 800);
}
function setupEventListeners() {
// Mouse events
renderer.domElement.addEventListener('mousedown', onMouseDown);
renderer.domElement.addEventListener('mousemove', onMouseMove);
renderer.domElement.addEventListener('mouseup', onMouseUp);
renderer.domElement.addEventListener('click', onMouseClick);
renderer.domElement.addEventListener('wheel', onMouseWheel);
// Touch events for mobile
renderer.domElement.addEventListener('touchstart', onTouchStart);
renderer.domElement.addEventListener('touchmove', onTouchMove);
renderer.domElement.addEventListener('touchend', onTouchEnd);
// Keyboard events
window.addEventListener('keydown', onKeyDown);
// UI controls
document.getElementById('rotateBtn').addEventListener('click', toggleAutoRotate);
document.getElementById('resetBtn').addEventListener('click', resetView);
document.getElementById('soundBtn').addEventListener('click', toggleSound);
document.getElementById('volume').addEventListener('input', updateVolume);
// Window resize
window.addEventListener('resize', onWindowResize);
}
function onMouseDown(event) {
isDragging = true;
previousMousePosition = {
x: event.clientX,
y: event.clientY
};
}
function onMouseMove(event) {
if (!isDragging) return;
const deltaMove = {
x: event.clientX - previousMousePosition.x,
y: event.clientY - previousMousePosition.y
};
// Rotate camera around the marimba
const spherical = new THREE.Spherical();
spherical.setFromVector3(camera.position);
spherical.theta -= deltaMove.x * 0.01;
spherical.phi += deltaMove.y * 0.01;
spherical.phi = Math.max(0.1, Math.min(Math.PI - 0.1, spherical.phi));
camera.position.setFromSpherical(spherical);
camera.lookAt(0, 0, 0);
previousMousePosition = {
x: event.clientX,
y: event.clientY
};
}
function onMouseUp() {
isDragging = false;
}
function onMouseClick(event) {
if (isDragging) return;
// Calculate mouse position in normalized device coordinates
mouse.x = (event.clientX / window.innerWidth) * 2 - 1;
mouse.y = -(event.clientY / window.innerHeight) * 2 + 1;
// Update the picking ray with the camera and mouse position
raycaster.setFromCamera(mouse, camera);
// Calculate objects intersecting the picking ray
const intersects = raycaster.intersectObjects(marimbaBars);
if (intersects.length > 0) {
const bar = intersects[0].object;
playNote(bar.userData.frequency, bar);
}
}
function onMouseWheel(event) {
event.preventDefault();
const zoomSpeed = 0.1;
const direction = event.deltaY > 0 ? 1 : -1;
camera.position.multiplyScalar(1 + direction * zoomSpeed);
// Limit zoom
const distance = camera.position.length();
if (distance < 5) {
camera.position.normalize().multiplyScalar(5);
} else if (distance > 30) {
camera.position.normalize().multiplyScalar(30);
}
}
function onTouchStart(event) {
if (event.touches.length === 1) {
isDragging = true;
previousMousePosition = {
x: event.touches[0].clientX,
y: event.touches[0].clientY
};
}
}
function onTouchMove(event) {
if (event.touches.length === 1 && isDragging) {
const deltaMove = {
x: event.touches[0].clientX - previousMousePosition.x,
y: event.touches[0].clientY - previousMousePosition.y
};
const spherical = new THREE.Spherical();
spherical.setFromVector3(camera.position);
spherical.theta -= deltaMove.x * 0.01;
spherical.phi += deltaMove.y * 0.01;
spherical.phi = Math.max(0.1, Math.min(Math.PI - 0.1, spherical.phi));
camera.position.setFromSpherical(spherical);
camera.lookAt(0, 0, 0);
previousMousePosition = {
x: event.touches[0].clientX,
y: event.touches[0].clientY
};
}
}
function onTouchEnd(event) {
isDragging = false;
// Handle tap for playing notes
if (event.changedTouches.length === 1 && !isDragging) {
const touch = event.changedTouches[0];
mouse.x = (touch.clientX / window.innerWidth) * 2 - 1;
mouse.y = -(touch.clientY / window.innerHeight) * 2 + 1;
raycaster.setFromCamera(mouse, camera);
const intersects = raycaster.intersectObjects(marimbaBars);
if (intersects.length > 0) {
const bar = intersects[0].object;
playNote(bar.userData.frequency, bar);
}
}
}
function onKeyDown(event) {
// Map keyboard keys to notes
const keyMap = {
'a': 0, 's': 1, 'd': 2, 'f': 3, 'g': 4, 'h': 5,
'j': 6, 'k': 7, 'l': 8, ';': 9, "'": 10, 'Enter': 11
};
const key = event.key.toLowerCase();
if (keyMap.hasOwnProperty(key) && marimbaBars[keyMap[key]]) {
const bar = marimbaBars[keyMap[key]];
playNote(bar.userData.frequency, bar);
}
}
function toggleAutoRotate() {
autoRotate = !autoRotate;
const btn = document.getElementById('rotateBtn');
btn.classList.toggle('active', autoRotate);
}
function resetView() {
camera.position.set(0, 5, 12);
camera.lookAt(0, 0, 0);
}
function toggleSound() {
soundEnabled = !soundEnabled;
const btn = document.getElementById('soundBtn');
btn.classList.toggle('active', soundEnabled);
btn.textContent = soundEnabled ? 'Sound On' : 'Sound Off';
}
function updateVolume(event) {
const volume = event.target.value / 100;
masterGainNode.gain.value = volume;
}
function onWindowResize() {
camera.aspect = window.innerWidth / window.innerHeight;
camera.updateProjectionMatrix();
renderer.setSize(window.innerWidth, window.innerHeight);
}
function animate() {
requestAnimationFrame(animate);
// Auto rotation
if (autoRotate && !isDragging) {
const time = Date.now() * 0.0005;
camera.position.x = Math.sin(time) * 12;
camera.position.z = Math.cos(time) * 12;
camera.lookAt(0, 0, 0);
}
// Animate mallets slightly
mallets.forEach((mallet, index) => {
mallet.rotation.y = Math.sin(Date.now() * 0.001 + index) * 0.1;
});
renderer.render(scene, camera);
}
// Initialize the application
window.addEventListener('load', init);
</script>
</body>
</html>