MySafeCode commited on
Commit
9e0cc2f
·
verified ·
1 Parent(s): 1b21cbf

Upload folder using huggingface_hub

Browse files
Files changed (1) hide show
  1. index.html +628 -593
index.html CHANGED
@@ -3,7 +3,7 @@
3
  <head>
4
  <meta charset="UTF-8">
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
- <title>3D Physics Playground</title>
7
  <style>
8
  * {
9
  margin: 0;
@@ -13,8 +13,8 @@
13
 
14
  body {
15
  font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
16
- background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
17
  overflow: hidden;
 
18
  position: relative;
19
  }
20
 
@@ -27,739 +27,774 @@
27
  cursor: grabbing;
28
  }
29
 
30
- .ui-panel {
31
  position: absolute;
32
- top: 20px;
33
- left: 20px;
34
- background: rgba(255, 255, 255, 0.95);
35
- backdrop-filter: blur(10px);
36
- border-radius: 15px;
37
  padding: 20px;
38
- box-shadow: 0 10px 30px rgba(0, 0, 0, 0.2);
39
- min-width: 280px;
40
- z-index: 100;
41
- transition: transform 0.3s ease;
42
  }
43
 
44
- .ui-panel:hover {
45
- transform: translateY(-2px);
46
- box-shadow: 0 15px 40px rgba(0, 0, 0, 0.3);
 
 
 
47
  }
48
 
49
- .panel-header {
 
 
 
50
  display: flex;
51
  align-items: center;
52
- margin-bottom: 20px;
53
- padding-bottom: 15px;
54
- border-bottom: 2px solid #e0e0e0;
55
  }
56
 
57
- .panel-title {
58
- font-size: 1.4em;
59
- font-weight: bold;
60
- background: linear-gradient(135deg, #667eea, #764ba2);
61
  -webkit-background-clip: text;
62
  -webkit-text-fill-color: transparent;
63
- flex: 1;
64
- }
65
-
66
- .controls-group {
67
- margin-bottom: 20px;
68
- }
69
-
70
- .control-label {
71
- display: block;
72
- margin-bottom: 8px;
73
- color: #555;
74
- font-size: 0.9em;
75
- font-weight: 600;
76
- text-transform: uppercase;
77
- letter-spacing: 0.5px;
78
  }
79
 
80
- .button-grid {
81
- display: grid;
82
- grid-template-columns: repeat(2, 1fr);
83
- gap: 10px;
84
- margin-bottom: 15px;
85
  }
86
 
87
- .btn {
88
- background: linear-gradient(135deg, #667eea, #764ba2);
 
 
 
 
89
  color: white;
90
- border: none;
91
- padding: 12px 20px;
92
- border-radius: 8px;
93
- cursor: pointer;
94
- font-size: 0.95em;
95
  font-weight: 600;
 
96
  transition: all 0.3s ease;
97
- position: relative;
98
- overflow: hidden;
99
- }
100
-
101
- .btn::before {
102
- content: '';
103
- position: absolute;
104
- top: 50%;
105
- left: 50%;
106
- width: 0;
107
- height: 0;
108
- border-radius: 50%;
109
- background: rgba(255, 255, 255, 0.3);
110
- transform: translate(-50%, -50%);
111
- transition: width 0.6s, height 0.6s;
112
  }
113
 
114
- .btn:hover::before {
115
- width: 300px;
116
- height: 300px;
117
- }
118
-
119
- .btn:hover {
120
  transform: translateY(-2px);
121
- box-shadow: 0 5px 15px rgba(102, 126, 234, 0.4);
122
  }
123
 
124
- .btn:active {
125
- transform: translateY(0);
 
126
  }
127
 
128
- .btn.active {
129
- background: linear-gradient(135deg, #f093fb, #f5576c);
130
- }
131
-
132
- .slider-container {
133
- margin-bottom: 15px;
134
- }
135
-
136
- .slider {
137
- width: 100%;
138
- height: 6px;
139
- border-radius: 3px;
140
- background: #e0e0e0;
141
- outline: none;
142
- -webkit-appearance: none;
143
  }
144
 
145
- .slider::-webkit-slider-thumb {
146
- -webkit-appearance: none;
147
- appearance: none;
148
- width: 20px;
149
- height: 20px;
150
- border-radius: 50%;
151
- background: linear-gradient(135deg, #667eea, #764ba2);
152
- cursor: pointer;
153
- transition: all 0.3s ease;
154
  }
155
 
156
- .slider::-webkit-slider-thumb:hover {
157
- transform: scale(1.2);
158
- box-shadow: 0 0 10px rgba(102, 126, 234, 0.5);
 
159
  }
160
 
161
- .slider-value {
162
- display: inline-block;
163
- margin-left: 10px;
164
- color: #667eea;
 
 
165
  font-weight: bold;
166
- }
167
-
168
- .stats {
169
- background: rgba(0, 0, 0, 0.7);
170
  color: white;
171
- padding: 15px;
172
- border-radius: 10px;
173
- font-family: 'Courier New', monospace;
174
- font-size: 0.9em;
175
- line-height: 1.6;
176
- }
177
-
178
- .stat-row {
179
- display: flex;
180
- justify-content: space-between;
181
- margin-bottom: 5px;
182
  }
183
 
184
- .stat-label {
185
- color: #aaa;
186
  }
187
 
188
- .stat-value {
189
- color: #4fc3f7;
190
- font-weight: bold;
 
 
 
 
 
 
 
 
 
 
191
  }
192
 
193
- .info-panel {
194
  position: absolute;
195
  bottom: 20px;
196
- left: 20px;
197
- background: rgba(255, 255, 255, 0.95);
198
  backdrop-filter: blur(10px);
199
- padding: 15px 20px;
200
- border-radius: 10px;
201
- box-shadow: 0 5px 20px rgba(0, 0, 0, 0.2);
202
- max-width: 400px;
203
- }
204
-
205
- .info-text {
206
- color: #666;
207
- font-size: 0.9em;
208
- line-height: 1.5;
209
- }
210
-
211
- .info-text strong {
212
- color: #667eea;
213
  }
214
 
215
- .color-picker {
216
- display: flex;
217
- gap: 8px;
218
- flex-wrap: wrap;
219
  margin-top: 10px;
220
  }
221
 
222
- .color-option {
223
- width: 30px;
224
- height: 30px;
225
- border-radius: 50%;
226
- cursor: pointer;
227
- transition: all 0.3s ease;
228
- border: 2px solid transparent;
 
 
229
  }
230
 
231
- .color-option:hover {
232
- transform: scale(1.1);
233
- border-color: #333;
 
 
 
 
 
234
  }
235
 
236
- .color-option.selected {
237
- border-color: #667eea;
238
- box-shadow: 0 0 10px currentColor;
239
  }
240
 
241
  @media (max-width: 768px) {
242
- .ui-panel {
243
- left: 10px;
244
- top: 10px;
245
- min-width: 250px;
246
- padding: 15px;
247
- }
248
-
249
- .button-grid {
250
- grid-template-columns: 1fr;
251
  }
252
-
253
  .info-panel {
254
  display: none;
255
  }
256
- }
257
-
258
- .loading {
259
- position: fixed;
260
- top: 0;
261
- left: 0;
262
- right: 0;
263
- bottom: 0;
264
- background: linear-gradient(135deg, #667eea, #764ba2);
265
- display: flex;
266
- align-items: center;
267
- justify-content: center;
268
- z-index: 1000;
269
- transition: opacity 0.5s ease;
270
- }
271
-
272
- .loading.hidden {
273
- opacity: 0;
274
- pointer-events: none;
275
- }
276
-
277
- .loading-text {
278
- color: white;
279
- font-size: 2em;
280
- font-weight: bold;
281
- animation: pulse 1.5s ease-in-out infinite;
282
- }
283
-
284
- @keyframes pulse {
285
- 0%, 100% { opacity: 1; }
286
- 50% { opacity: 0.5; }
287
  }
288
  </style>
289
  </head>
290
  <body>
291
  <div class="loading" id="loading">
292
- <div class="loading-text">Loading Physics Engine...</div>
 
293
  </div>
294
 
295
- <canvas id="canvas"></canvas>
296
-
297
- <div class="ui-panel">
298
- <div class="panel-header">
299
- <div class="panel-title">🎮 Physics Controls</div>
300
- </div>
301
-
302
- <div class="controls-group">
303
- <label class="control-label">Spawn Objects</label>
304
- <div class="button-grid">
305
- <button class="btn" onclick="spawnObject('sphere')">⚪ Sphere</button>
306
- <button class="btn" onclick="spawnObject('box')">📦 Box</button>
307
- <button class="btn" onclick="spawnObject('cylinder')">🥤 Cylinder</button>
308
- <button class="btn" onclick="spawnObject('cone')">🔺 Cone</button>
309
- </div>
310
- </div>
311
-
312
- <div class="controls-group">
313
- <label class="control-label">Object Color</label>
314
- <div class="color-picker">
315
- <div class="color-option selected" style="background: #ff6b6b" onclick="selectColor('#ff6b6b')"></div>
316
- <div class="color-option" style="background: #4ecdc4" onclick="selectColor('#4ecdc4')"></div>
317
- <div class="color-option" style="background: #45b7d1" onclick="selectColor('#45b7d1')"></div>
318
- <div class="color-option" style="background: #96ceb4" onclick="selectColor('#96ceb4')"></div>
319
- <div class="color-option" style="background: #feca57" onclick="selectColor('#feca57')"></div>
320
- <div class="color-option" style="background: #dfe6e9" onclick="selectColor('#dfe6e9')"></div>
321
- <div class="color-option" style="background: #a29bfe" onclick="selectColor('#a29bfe')"></div>
322
- <div class="color-option" style="background: #fd79a8" onclick="selectColor('#fd79a8')"></div>
323
- </div>
324
- </div>
325
-
326
- <div class="controls-group">
327
- <div class="slider-container">
328
- <label class="control-label">Gravity <span class="slider-value" id="gravityValue">-9.8</span></label>
329
- <input type="range" class="slider" id="gravitySlider" min="-20" max="0" value="-9.8" step="0.1" oninput="updateGravity(this.value)">
330
- </div>
331
- <div class="slider-container">
332
- <label class="control-label">Bounciness <span class="slider-value" id="bounceValue">0.7</span></label>
333
- <input type="range" class="slider" id="bounceSlider" min="0" max="1" value="0.7" step="0.1" oninput="updateBounciness(this.value)">
334
- </div>
335
- <div class="slider-container">
336
- <label class="control-label">Object Size <span class="slider-value" id="sizeValue">1.0</span></label>
337
- <input type="range" class="slider" id="sizeSlider" min="0.5" max="3" value="1" step="0.1" oninput="updateSize(this.value)">
338
- </div>
339
- </div>
340
-
341
- <div class="controls-group">
342
- <button class="btn" onclick="clearAllObjects()" style="width: 100%; background: linear-gradient(135deg, #f093fb, #f5576c);">🗑️ Clear All</button>
343
- <button class="btn" onclick="togglePause()" id="pauseBtn" style="width: 100%; margin-top: 10px;">⏸️ Pause</button>
344
- </div>
345
-
346
- <div class="stats">
347
- <div class="stat-row">
348
- <span class="stat-label">Objects:</span>
349
- <span class="stat-value" id="objectCount">0</span>
350
- </div>
351
- <div class="stat-row">
352
- <span class="stat-label">FPS:</span>
353
- <span class="stat-value" id="fps">60</span>
354
  </div>
355
- <div class="stat-row">
356
- <span class="stat-label">Physics:</span>
357
- <span class="stat-value" id="physicsStatus">Active</span>
 
358
  </div>
359
  </div>
360
  </div>
361
 
362
  <div class="info-panel">
363
- <div class="info-text">
364
- <strong>Controls:</strong><br>
365
- Click to spawn objects<br>
366
- Drag to rotate camera<br>
367
- Scroll to zoom<br>
368
- • Space to pause physics<br>
369
- • R to reset camera
370
- </div>
 
 
371
  </div>
372
 
 
 
373
  <script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/r128/three.min.js"></script>
374
  <script>
375
  // Global variables
376
- let scene, camera, renderer, world;
377
- let objects = [];
378
- let isPaused = false;
379
- let selectedColor = '#ff6b6b';
380
- let objectSize = 1;
381
- let bounciness = 0.7;
382
- let gravity = -9.8;
383
- let frameCount = 0;
384
- let lastTime = performance.now();
385
- let fps = 60;
386
-
387
- // Physics properties
388
- const physicsObjects = [];
389
-
390
- // Initialize the scene
 
 
 
 
 
 
 
 
 
 
 
 
391
  function init() {
392
- // Create scene
393
  scene = new THREE.Scene();
394
- scene.fog = new THREE.Fog(0x000000, 10, 50);
395
-
396
- // Create camera
397
- camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 1000);
398
- camera.position.set(0, 10, 20);
 
 
 
 
 
399
  camera.lookAt(0, 0, 0);
400
 
401
- // Create renderer
402
- renderer = new THREE.WebGLRenderer({
403
- canvas: document.getElementById('canvas'),
404
- antialias: true,
405
- alpha: true
406
- });
407
  renderer.setSize(window.innerWidth, window.innerHeight);
408
  renderer.shadowMap.enabled = true;
409
  renderer.shadowMap.type = THREE.PCFSoftShadowMap;
 
 
 
 
 
 
 
410
 
411
  // Lighting
412
- const ambientLight = new THREE.AmbientLight(0xffffff, 0.4);
413
- scene.add(ambientLight);
414
 
415
- const directionalLight = new THREE.DirectionalLight(0xffffff, 0.8);
416
- directionalLight.position.set(5, 10, 5);
417
- directionalLight.castShadow = true;
418
- directionalLight.shadow.camera.left = -20;
419
- directionalLight.shadow.camera.right = 20;
420
- directionalLight.shadow.camera.top = 20;
421
- directionalLight.shadow.camera.bottom = -20;
422
- scene.add(directionalLight);
423
 
424
- const pointLight = new THREE.PointLight(0x667eea, 0.5);
425
- pointLight.position.set(-5, 5, -5);
426
- scene.add(pointLight);
427
 
428
- // Create ground
429
- const groundGeometry = new THREE.PlaneGeometry(50, 50);
430
- const groundMaterial = new THREE.MeshStandardMaterial({
431
- color: 0x2c3e50,
432
- roughness: 0.8,
433
- metalness: 0.2
434
- });
435
- const ground = new THREE.Mesh(groundGeometry, groundMaterial);
436
- ground.rotation.x = -Math.PI / 2;
437
- ground.receiveShadow = true;
438
- scene.add(ground);
439
 
440
- // Create walls
441
- createWalls();
442
 
443
- // Mouse controls
444
- setupControls();
445
 
446
- // Start animation
447
  animate();
448
-
449
- // Hide loading screen
450
- setTimeout(() => {
451
- document.getElementById('loading').classList.add('hidden');
452
- }, 500);
453
-
454
- // Spawn initial objects
455
- setTimeout(() => {
456
- for (let i = 0; i < 3; i++) {
457
- setTimeout(() => spawnObject('sphere'), i * 200);
458
- }
459
- }, 600);
460
  }
461
 
462
- function createWalls() {
463
- const wallMaterial = new THREE.MeshStandardMaterial({
464
- color: 0x34495e,
465
- transparent: true,
466
- opacity: 0.3
467
- });
468
 
469
- // Back wall
470
- const backWall = new THREE.Mesh(
471
- new THREE.PlaneGeometry(50, 20),
472
- wallMaterial
473
- );
474
- backWall.position.z = -25;
475
- backWall.position.y = 10;
476
- scene.add(backWall);
477
-
478
- // Side walls
479
- const leftWall = new THREE.Mesh(
480
- new THREE.PlaneGeometry(50, 20),
481
- wallMaterial
482
- );
483
- leftWall.rotation.y = Math.PI / 2;
484
- leftWall.position.x = -25;
485
- leftWall.position.y = 10;
486
- scene.add(leftWall);
487
-
488
- const rightWall = new THREE.Mesh(
489
- new THREE.PlaneGeometry(50, 20),
490
- wallMaterial
491
- );
492
- rightWall.rotation.y = -Math.PI / 2;
493
- rightWall.position.x = 25;
494
- rightWall.position.y = 10;
495
- scene.add(rightWall);
496
- }
497
-
498
- function setupControls() {
499
- let mouseX = 0, mouseY = 0;
500
- let targetRotationX = 0, targetRotationY = 0;
501
- let isDragging = false;
502
-
503
- renderer.domElement.addEventListener('mousedown', (e) => {
504
- if (e.button === 0) {
505
- isDragging = true;
506
- mouseX = e.clientX;
507
- mouseY = e.clientY;
508
- }
509
- });
510
 
511
- renderer.domElement.addEventListener('mousemove', (e) => {
512
- if (isDragging) {
513
- const deltaX = e.clientX - mouseX;
514
- const deltaY = e.clientY - mouseY;
515
- targetRotationY += deltaX * 0.01;
516
- targetRotationX += deltaY * 0.01;
517
- targetRotationX = Math.max(-Math.PI / 3, Math.min(Math.PI / 3, targetRotationX));
518
- mouseX = e.clientX;
519
- mouseY = e.clientY;
520
- }
521
- });
522
 
523
- renderer.domElement.addEventListener('mouseup', () => {
524
- isDragging = false;
525
- });
 
526
 
527
- renderer.domElement.addEventListener('click', (e) => {
528
- if (!isDragging) {
529
- const rect = renderer.domElement.getBoundingClientRect();
530
- const x = ((e.clientX - rect.left) / rect.width) * 2 - 1;
531
- const y = -((e.clientY - rect.top) / rect.height) * 2 + 1;
532
-
533
- const vector = new THREE.Vector3(x, y, 0.5);
534
- vector.unproject(camera);
535
- const dir = vector.sub(camera.position).normalize();
536
- const distance = -camera.position.y / dir.y;
537
- const pos = camera.position.clone().add(dir.multiplyScalar(distance));
538
-
539
- spawnObjectAt(pos);
540
- }
541
  });
542
-
543
- renderer.domElement.addEventListener('wheel', (e) => {
544
- camera.position.z += e.deltaY * 0.01;
545
- camera.position.z = Math.max(5, Math.min(50, camera.position.z));
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
546
  });
 
 
 
547
 
548
- // Update camera rotation
549
- function updateCamera() {
550
- const radius = camera.position.length();
551
- camera.position.x = radius * Math.sin(targetRotationY) * Math.cos(targetRotationX);
552
- camera.position.y = radius * Math.sin(targetRotationX);
553
- camera.position.z = radius * Math.cos(targetRotationY) * Math.cos(targetRotationX);
554
- camera.lookAt(0, 0, 0);
555
- requestAnimationFrame(updateCamera);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
556
  }
557
- updateCamera();
558
-
559
- // Keyboard controls
560
- document.addEventListener('keydown', (e) => {
561
- if (e.code === 'Space') {
562
- e.preventDefault();
563
- togglePause();
564
- } else if (e.code === 'KeyR') {
565
- camera.position.set(0, 10, 20);
566
- targetRotationX = 0;
567
- targetRotationY = 0;
568
- }
569
- });
570
  }
571
 
572
- function spawnObject(type) {
573
- const x = (Math.random() - 0.5) * 10;
574
- const y = 10 + Math.random() * 5;
575
- const z = (Math.random() - 0.5) * 10;
576
- spawnObjectAt(new THREE.Vector3(x, y, z), type);
577
- }
578
-
579
- function spawnObjectAt(position, type = 'sphere') {
580
- let geometry;
581
-
582
- switch(type) {
583
- case 'box':
584
- geometry = new THREE.BoxGeometry(objectSize, objectSize, objectSize);
585
- break;
586
- case 'cylinder':
587
- geometry = new THREE.CylinderGeometry(objectSize/2, objectSize/2, objectSize, 32);
588
- break;
589
- case 'cone':
590
- geometry = new THREE.ConeGeometry(objectSize/2, objectSize, 32);
591
- break;
592
- default:
593
- geometry = new THREE.SphereGeometry(objectSize/2, 32, 32);
594
- }
595
 
596
- const material = new THREE.MeshStandardMaterial({
597
- color: selectedColor,
598
- roughness: 0.4,
599
- metalness: 0.3
600
- });
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
601
 
602
- const mesh = new THREE.Mesh(geometry, material);
603
- mesh.position.copy(position);
604
- mesh.castShadow = true;
605
- mesh.receiveShadow = true;
606
- scene.add(mesh);
607
-
608
- // Physics object
609
- const physicsObj = {
610
- mesh: mesh,
611
- velocity: new THREE.Vector3(
612
- (Math.random() - 0.5) * 2,
613
- 0,
614
- (Math.random() - 0.5) * 2
615
- ),
616
- angularVelocity: new THREE.Vector3(
617
- (Math.random() - 0.5) * 0.1,
618
- (Math.random() - 0.5) * 0.1,
619
- (Math.random() - 0.5) * 0.1
620
- ),
621
- mass: objectSize
 
 
 
 
622
  };
623
-
624
- objects.push(physicsObj);
625
- updateObjectCount();
 
 
 
626
  }
627
 
628
- function updatePhysics() {
629
- if (isPaused) return;
630
-
631
- const dt = 1/60;
632
- const gravityVec = new THREE.Vector3(0, gravity, 0);
633
-
634
- objects.forEach(obj => {
635
- // Apply gravity
636
- obj.velocity.add(gravityVec.clone().multiplyScalar(dt));
637
-
638
- // Update position
639
- obj.mesh.position.add(obj.velocity.clone().multiplyScalar(dt));
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
640
 
641
- // Update rotation
642
- obj.mesh.rotation.x += obj.angularVelocity.x;
643
- obj.mesh.rotation.y += obj.angularVelocity.y;
644
- obj.mesh.rotation.z += obj.angularVelocity.z;
 
 
 
 
 
645
 
646
- // Ground collision
647
- if (obj.mesh.position.y - objectSize/2 < 0) {
648
- obj.mesh.position.y = objectSize/2;
649
- obj.velocity.y *= -bounciness;
650
- obj.angularVelocity.multiplyScalar(0.9);
651
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
652
 
653
- // Wall collisions
654
- const boundary = 25;
655
- if (Math.abs(obj.mesh.position.x) > boundary - objectSize/2) {
656
- obj.mesh.position.x = Math.sign(obj.mesh.position.x) * (boundary - objectSize/2);
657
- obj.velocity.x *= -bounciness;
658
- }
659
- if (Math.abs(obj.mesh.position.z) > boundary - objectSize/2) {
660
- obj.mesh.position.z = Math.sign(obj.mesh.position.z) * (boundary - objectSize/2);
661
- obj.velocity.z *= -bounciness;
662
- }
663
 
664
- // Object-to-object collisions (simplified)
665
- objects.forEach(other => {
666
- if (obj !== other) {
667
- const distance = obj.mesh.position.distanceTo(other.mesh.position);
668
- const minDistance = objectSize;
669
-
670
- if (distance < minDistance) {
671
- const direction = obj.mesh.position.clone().sub(other.mesh.position).normalize();
672
- obj.mesh.position.add(direction.multiplyScalar(minDistance - distance));
673
-
674
- const relativeVelocity = obj.velocity.clone().sub(other.velocity);
675
- const speed = relativeVelocity.dot(direction);
676
-
677
- if (speed > 0) {
678
- obj.velocity.sub(direction.multiplyScalar(speed * bounciness));
679
- }
680
- }
681
- }
682
- });
 
 
 
 
683
 
684
- // Apply damping
685
- obj.velocity.multiplyScalar(0.999);
686
- obj.angularVelocity.multiplyScalar(0.995);
687
- });
688
  }
689
 
690
- function animate() {
691
- requestAnimationFrame(animate);
692
 
693
- updatePhysics();
694
- renderer.render(scene, camera);
 
695
 
696
- // Calculate FPS
697
- frameCount++;
698
- const currentTime = performance.now();
699
- if (currentTime >= lastTime + 1000) {
700
- fps = Math.round((frameCount * 1000) / (currentTime - lastTime));
701
- document.getElementById('fps').textContent = fps;
702
- frameCount = 0;
703
- lastTime = currentTime;
 
704
  }
705
  }
706
 
707
- function updateObjectCount() {
708
- document.getElementById('objectCount').textContent = objects.length;
 
 
 
 
 
 
 
 
 
 
 
 
 
709
  }
710
 
711
- function clearAllObjects() {
712
- objects.forEach(obj => {
713
- scene.remove(obj.mesh);
714
- obj.mesh.geometry.dispose();
715
- obj.mesh.material.dispose();
716
- });
717
- objects = [];
718
- updateObjectCount();
719
  }
720
 
721
- function togglePause() {
722
- isPaused = !isPaused;
723
- const btn = document.getElementById('pauseBtn');
724
- btn.textContent = isPaused ? '▶️ Play' : '⏸️ Pause';
725
- btn.classList.toggle('active', isPaused);
726
- document.getElementById('physicsStatus').textContent = isPaused ? 'Paused' : 'Active';
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
727
  }
728
 
729
- function selectColor(color) {
730
- selectedColor = color;
731
- document.querySelectorAll('.color-option').forEach(el => {
732
- el.classList.remove('selected');
733
- if (el.style.background === color) {
734
- el.classList.add('selected');
 
 
 
 
 
 
 
 
 
735
  }
736
- });
737
  }
738
 
739
- function updateGravity(value) {
740
- gravity = parseFloat(value);
741
- document.getElementById('gravityValue').textContent = value;
 
 
 
 
 
 
 
 
 
742
  }
743
 
744
- function updateBounciness(value) {
745
- bounciness = parseFloat(value);
746
- document.getElementById('bounceValue').textContent = value;
 
747
  }
748
 
749
- function updateSize(value) {
750
- objectSize = parseFloat(value);
751
- document.getElementById('sizeValue').textContent = value;
752
  }
753
 
754
- // Handle window resize
755
- window.addEventListener('resize', () => {
 
 
 
 
 
 
 
 
 
 
 
756
  camera.aspect = window.innerWidth / window.innerHeight;
757
  camera.updateProjectionMatrix();
758
  renderer.setSize(window.innerWidth, window.innerHeight);
759
- });
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
760
 
761
  // Initialize the application
762
- init();
763
  </script>
764
  </body>
765
  </html>
 
3
  <head>
4
  <meta charset="UTF-8">
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>3D Marimba - Interactive Musical Instrument</title>
7
  <style>
8
  * {
9
  margin: 0;
 
13
 
14
  body {
15
  font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
 
16
  overflow: hidden;
17
+ background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
18
  position: relative;
19
  }
20
 
 
27
  cursor: grabbing;
28
  }
29
 
30
+ .ui-overlay {
31
  position: absolute;
32
+ top: 0;
33
+ left: 0;
34
+ right: 0;
 
 
35
  padding: 20px;
36
+ background: linear-gradient(180deg, rgba(0,0,0,0.5) 0%, transparent 100%);
37
+ pointer-events: none;
38
+ z-index: 10;
 
39
  }
40
 
41
+ .header {
42
+ display: flex;
43
+ justify-content: space-between;
44
+ align-items: center;
45
+ color: white;
46
+ pointer-events: auto;
47
  }
48
 
49
+ .title {
50
+ font-size: 28px;
51
+ font-weight: bold;
52
+ text-shadow: 2px 2px 4px rgba(0,0,0,0.3);
53
  display: flex;
54
  align-items: center;
55
+ gap: 15px;
 
 
56
  }
57
 
58
+ .title h1 {
59
+ font-size: 32px;
60
+ background: linear-gradient(45deg, #fff, #ffd700);
 
61
  -webkit-background-clip: text;
62
  -webkit-text-fill-color: transparent;
63
+ background-clip: text;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
64
  }
65
 
66
+ .controls {
67
+ display: flex;
68
+ gap: 15px;
69
+ flex-wrap: wrap;
 
70
  }
71
 
72
+ .control-btn {
73
+ padding: 10px 20px;
74
+ background: rgba(255,255,255,0.2);
75
+ backdrop-filter: blur(10px);
76
+ border: 2px solid rgba(255,255,255,0.3);
77
+ border-radius: 50px;
78
  color: white;
 
 
 
 
 
79
  font-weight: 600;
80
+ cursor: pointer;
81
  transition: all 0.3s ease;
82
+ pointer-events: auto;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
83
  }
84
 
85
+ .control-btn:hover {
86
+ background: rgba(255,255,255,0.3);
 
 
 
 
87
  transform: translateY(-2px);
88
+ box-shadow: 0 5px 15px rgba(0,0,0,0.2);
89
  }
90
 
91
+ .control-btn.active {
92
+ background: linear-gradient(45deg, #667eea, #764ba2);
93
+ border-color: transparent;
94
  }
95
 
96
+ .info-panel {
97
+ position: absolute;
98
+ bottom: 20px;
99
+ left: 20px;
100
+ background: rgba(0,0,0,0.7);
101
+ backdrop-filter: blur(10px);
102
+ padding: 20px;
103
+ border-radius: 15px;
104
+ color: white;
105
+ max-width: 300px;
106
+ pointer-events: auto;
 
 
 
 
107
  }
108
 
109
+ .info-panel h3 {
110
+ margin-bottom: 10px;
111
+ color: #ffd700;
 
 
 
 
 
 
112
  }
113
 
114
+ .info-panel p {
115
+ margin: 5px 0;
116
+ font-size: 14px;
117
+ opacity: 0.9;
118
  }
119
 
120
+ .note-display {
121
+ position: absolute;
122
+ top: 50%;
123
+ left: 50%;
124
+ transform: translate(-50%, -50%);
125
+ font-size: 72px;
126
  font-weight: bold;
 
 
 
 
127
  color: white;
128
+ text-shadow: 3px 3px 6px rgba(0,0,0,0.5);
129
+ opacity: 0;
130
+ pointer-events: none;
131
+ transition: opacity 0.2s ease;
132
+ z-index: 20;
 
 
 
 
 
 
133
  }
134
 
135
+ .note-display.show {
136
+ animation: notePop 0.8s ease;
137
  }
138
 
139
+ @keyframes notePop {
140
+ 0% {
141
+ opacity: 0;
142
+ transform: translate(-50%, -50%) scale(0.5);
143
+ }
144
+ 50% {
145
+ opacity: 1;
146
+ transform: translate(-50%, -50%) scale(1.2);
147
+ }
148
+ 100% {
149
+ opacity: 0;
150
+ transform: translate(-50%, -50%) scale(1);
151
+ }
152
  }
153
 
154
+ .volume-control {
155
  position: absolute;
156
  bottom: 20px;
157
+ right: 20px;
158
+ background: rgba(0,0,0,0.7);
159
  backdrop-filter: blur(10px);
160
+ padding: 15px;
161
+ border-radius: 15px;
162
+ color: white;
163
+ pointer-events: auto;
 
 
 
 
 
 
 
 
 
 
164
  }
165
 
166
+ .volume-slider {
167
+ width: 150px;
 
 
168
  margin-top: 10px;
169
  }
170
 
171
+ .loading {
172
+ position: absolute;
173
+ top: 50%;
174
+ left: 50%;
175
+ transform: translate(-50%, -50%);
176
+ color: white;
177
+ font-size: 24px;
178
+ font-weight: bold;
179
+ text-align: center;
180
  }
181
 
182
+ .loading-spinner {
183
+ width: 50px;
184
+ height: 50px;
185
+ border: 3px solid rgba(255,255,255,0.3);
186
+ border-top-color: white;
187
+ border-radius: 50%;
188
+ animation: spin 1s linear infinite;
189
+ margin: 20px auto;
190
  }
191
 
192
+ @keyframes spin {
193
+ to { transform: rotate(360deg); }
 
194
  }
195
 
196
  @media (max-width: 768px) {
197
+ .title h1 {
198
+ font-size: 24px;
 
 
 
 
 
 
 
199
  }
200
+
201
  .info-panel {
202
  display: none;
203
  }
204
+
205
+ .controls {
206
+ gap: 10px;
207
+ }
208
+
209
+ .control-btn {
210
+ padding: 8px 15px;
211
+ font-size: 14px;
212
+ }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
213
  }
214
  </style>
215
  </head>
216
  <body>
217
  <div class="loading" id="loading">
218
+ <div class="loading-spinner"></div>
219
+ Loading 3D Marimba...
220
  </div>
221
 
222
+ <div class="ui-overlay">
223
+ <div class="header">
224
+ <div class="title">
225
+ <h1>3D Marimba</h1>
226
+ <span style="opacity: 0.8;">Interactive Instrument</span>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
227
  </div>
228
+ <div class="controls">
229
+ <button class="control-btn" id="rotateBtn">Auto Rotate</button>
230
+ <button class="control-btn" id="resetBtn">Reset View</button>
231
+ <button class="control-btn active" id="soundBtn">Sound On</button>
232
  </div>
233
  </div>
234
  </div>
235
 
236
  <div class="info-panel">
237
+ <h3>How to Play</h3>
238
+ <p>🎵 Click on bars to play notes</p>
239
+ <p>🖱️ Drag to rotate the view</p>
240
+ <p>🔍 Scroll to zoom in/out</p>
241
+ <p>⌨️ Press keys A-L for quick play</p>
242
+ </div>
243
+
244
+ <div class="volume-control">
245
+ <label for="volume">Volume</label>
246
+ <input type="range" id="volume" class="volume-slider" min="0" max="100" value="70">
247
  </div>
248
 
249
+ <div class="note-display" id="noteDisplay"></div>
250
+
251
  <script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/r128/three.min.js"></script>
252
  <script>
253
  // Global variables
254
+ let scene, camera, renderer, raycaster, mouse;
255
+ let marimbaBars = [];
256
+ let mallets = [];
257
+ let audioContext;
258
+ let masterGainNode;
259
+ let autoRotate = false;
260
+ let soundEnabled = true;
261
+ let isDragging = false;
262
+ let previousMousePosition = { x: 0, y: 0 };
263
+
264
+ // Musical notes frequencies (pentatonic scale for pleasant sound)
265
+ const notes = [
266
+ { note: 'C', freq: 261.63, color: '#FF6B6B' },
267
+ { note: 'D', freq: 293.66, color: '#4ECDC4' },
268
+ { note: 'E', freq: 329.63, color: '#45B7D1' },
269
+ { note: 'G', freq: 392.00, color: '#96CEB4' },
270
+ { note: 'A', freq: 440.00, color: '#FFEAA7' },
271
+ { note: 'C', freq: 523.25, color: '#DFE6E9' },
272
+ { note: 'D', freq: 587.33, color: '#74B9FF' },
273
+ { note: 'E', freq: 659.25, color: '#A29BFE' },
274
+ { note: 'G', freq: 783.99, color: '#FD79A8' },
275
+ { note: 'A', freq: 880.00, color: '#FDCB6E' },
276
+ { note: 'C', freq: 1046.50, color: '#6C5CE7' },
277
+ { note: 'D', freq: 1174.66, color: '#00B894' }
278
+ ];
279
+
280
+ // Initialize Three.js
281
  function init() {
282
+ // Scene setup
283
  scene = new THREE.Scene();
284
+ scene.fog = new THREE.Fog(0x764ba2, 10, 50);
285
+
286
+ // Camera setup
287
+ camera = new THREE.PerspectiveCamera(
288
+ 75,
289
+ window.innerWidth / window.innerHeight,
290
+ 0.1,
291
+ 1000
292
+ );
293
+ camera.position.set(0, 5, 12);
294
  camera.lookAt(0, 0, 0);
295
 
296
+ // Renderer setup
297
+ renderer = new THREE.WebGLRenderer({ antialias: true, alpha: true });
 
 
 
 
298
  renderer.setSize(window.innerWidth, window.innerHeight);
299
  renderer.shadowMap.enabled = true;
300
  renderer.shadowMap.type = THREE.PCFSoftShadowMap;
301
+ renderer.toneMapping = THREE.ACESFilmicToneMapping;
302
+ renderer.toneMappingExposure = 1.2;
303
+ document.body.appendChild(renderer.domElement);
304
+
305
+ // Raycaster for mouse interaction
306
+ raycaster = new THREE.Raycaster();
307
+ mouse = new THREE.Vector2();
308
 
309
  // Lighting
310
+ setupLighting();
 
311
 
312
+ // Create marimba
313
+ createMarimba();
 
 
 
 
 
 
314
 
315
+ // Create mallets
316
+ createMallets();
 
317
 
318
+ // Event listeners
319
+ setupEventListeners();
 
 
 
 
 
 
 
 
 
320
 
321
+ // Initialize audio
322
+ initAudio();
323
 
324
+ // Hide loading
325
+ document.getElementById('loading').style.display = 'none';
326
 
327
+ // Start animation loop
328
  animate();
 
 
 
 
 
 
 
 
 
 
 
 
329
  }
330
 
331
+ function setupLighting() {
332
+ // Ambient light
333
+ const ambientLight = new THREE.AmbientLight(0xffffff, 0.4);
334
+ scene.add(ambientLight);
 
 
335
 
336
+ // Main directional light
337
+ const directionalLight = new THREE.DirectionalLight(0xffffff, 1);
338
+ directionalLight.position.set(5, 10, 5);
339
+ directionalLight.castShadow = true;
340
+ directionalLight.shadow.camera.near = 0.1;
341
+ directionalLight.shadow.camera.far = 50;
342
+ directionalLight.shadow.camera.left = -15;
343
+ directionalLight.shadow.camera.right = 15;
344
+ directionalLight.shadow.camera.top = 15;
345
+ directionalLight.shadow.camera.bottom = -15;
346
+ directionalLight.shadow.mapSize.width = 2048;
347
+ directionalLight.shadow.mapSize.height = 2048;
348
+ scene.add(directionalLight);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
349
 
350
+ // Point lights for ambiance
351
+ const pointLight1 = new THREE.PointLight(0xff6b6b, 0.5, 10);
352
+ pointLight1.position.set(-5, 3, 3);
353
+ scene.add(pointLight1);
 
 
 
 
 
 
 
354
 
355
+ const pointLight2 = new THREE.PointLight(0x4ecdc4, 0.5, 10);
356
+ pointLight2.position.set(5, 3, 3);
357
+ scene.add(pointLight2);
358
+ }
359
 
360
+ function createMarimba() {
361
+ const barGroup = new THREE.Group();
362
+
363
+ // Create resonator box
364
+ const resonatorGeometry = new THREE.BoxGeometry(14, 1, 4);
365
+ const resonatorMaterial = new THREE.MeshPhongMaterial({
366
+ color: 0x8B4513,
367
+ shininess: 30
 
 
 
 
 
 
368
  });
369
+ const resonator = new THREE.Mesh(resonatorGeometry, resonatorMaterial);
370
+ resonator.position.y = -0.5;
371
+ resonator.receiveShadow = true;
372
+ barGroup.add(resonator);
373
+
374
+ // Create bars
375
+ const barWidth = 1;
376
+ const barDepth = 0.3;
377
+ const barSpacing = 0.1;
378
+ const totalWidth = notes.length * barWidth + (notes.length - 1) * barSpacing;
379
+
380
+ notes.forEach((note, index) => {
381
+ // Calculate bar dimensions (higher notes are shorter)
382
+ const barHeight = 0.2;
383
+ const barLength = 8 - (index * 0.3);
384
+
385
+ // Create bar geometry
386
+ const barGeometry = new THREE.BoxGeometry(barWidth, barHeight, barLength);
387
+
388
+ // Create gradient material for each bar
389
+ const barMaterial = new THREE.MeshPhongMaterial({
390
+ color: 0xD2691E,
391
+ emissive: note.color,
392
+ emissiveIntensity: 0,
393
+ shininess: 100,
394
+ specular: 0x222222
395
+ });
396
+
397
+ const bar = new THREE.Mesh(barGeometry, barMaterial);
398
+
399
+ // Position bars
400
+ const xPosition = (index - notes.length / 2 + 0.5) * (barWidth + barSpacing);
401
+ bar.position.set(xPosition, 0.5, 0);
402
+ bar.castShadow = true;
403
+ bar.receiveShadow = true;
404
+
405
+ // Store note data
406
+ bar.userData = {
407
+ note: note.note,
408
+ frequency: note.freq,
409
+ index: index,
410
+ originalColor: 0xD2691E,
411
+ hitColor: note.color
412
+ };
413
+
414
+ marimbaBars.push(bar);
415
+ barGroup.add(bar);
416
  });
417
+
418
+ scene.add(barGroup);
419
+ }
420
 
421
+ function createMallets() {
422
+ const malletGroup = new THREE.Group();
423
+
424
+ // Create two mallets
425
+ for (let i = 0; i < 2; i++) {
426
+ // Handle
427
+ const handleGeometry = new THREE.CylinderGeometry(0.05, 0.05, 2, 8);
428
+ const handleMaterial = new THREE.MeshPhongMaterial({ color: 0x8B4513 });
429
+ const handle = new THREE.Mesh(handleGeometry, handleMaterial);
430
+
431
+ // Head
432
+ const headGeometry = new THREE.SphereGeometry(0.3, 16, 16);
433
+ const headMaterial = new THREE.MeshPhongMaterial({
434
+ color: i === 0 ? 0xFF0000 : 0x0000FF,
435
+ shininess: 100
436
+ });
437
+ const head = new THREE.Mesh(headGeometry, headMaterial);
438
+ head.position.y = 1;
439
+
440
+ const mallet = new THREE.Group();
441
+ mallet.add(handle);
442
+ mallet.add(head);
443
+ mallet.position.set(i === 0 ? -3 : 3, 3, 5);
444
+ mallet.rotation.z = i === 0 ? -0.5 : 0.5;
445
+
446
+ mallets.push(mallet);
447
+ malletGroup.add(mallet);
448
  }
449
+
450
+ scene.add(malletGroup);
 
 
 
 
 
 
 
 
 
 
 
451
  }
452
 
453
+ function initAudio() {
454
+ // Create audio context
455
+ audioContext = new (window.AudioContext || window.webkitAudioContext)();
456
+
457
+ // Create master gain node
458
+ masterGainNode = audioContext.createGain();
459
+ masterGainNode.connect(audioContext.destination);
460
+ masterGainNode.gain.value = 0.7;
461
+ }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
462
 
463
+ function playNote(frequency, bar) {
464
+ if (!soundEnabled) return;
465
+
466
+ // Create oscillator
467
+ const oscillator = audioContext.createOscillator();
468
+ const gainNode = audioContext.createGain();
469
+
470
+ oscillator.connect(gainNode);
471
+ gainNode.connect(masterGainNode);
472
+
473
+ // Set frequency and waveform
474
+ oscillator.frequency.value = frequency;
475
+ oscillator.type = 'sine';
476
+
477
+ // ADSR envelope
478
+ const now = audioContext.currentTime;
479
+ gainNode.gain.setValueAtTime(0, now);
480
+ gainNode.gain.linearRampToValueAtTime(0.3, now + 0.01); // Attack
481
+ gainNode.gain.exponentialRampToValueAtTime(0.2, now + 0.1); // Decay/Sustain
482
+ gainNode.gain.exponentialRampToValueAtTime(0.01, now + 1.5); // Release
483
+
484
+ // Start and stop
485
+ oscillator.start(now);
486
+ oscillator.stop(now + 1.5);
487
+
488
+ // Add harmonics for richer sound
489
+ const harmonic = audioContext.createOscillator();
490
+ const harmonicGain = audioContext.createGain();
491
+ harmonic.connect(harmonicGain);
492
+ harmonicGain.connect(masterGainNode);
493
+ harmonic.frequency.value = frequency * 2;
494
+ harmonic.type = 'triangle';
495
+ harmonicGain.gain.setValueAtTime(0, now);
496
+ harmonicGain.gain.linearRampToValueAtTime(0.1, now + 0.01);
497
+ harmonicGain.gain.exponentialRampToValueAtTime(0.01, now + 1);
498
+ harmonic.start(now);
499
+ harmonic.stop(now + 1);
500
+
501
+ // Visual feedback
502
+ if (bar) {
503
+ animateBarHit(bar);
504
+ }
505
+
506
+ // Show note display
507
+ showNoteDisplay(bar.userData.note);
508
+ }
509
 
510
+ function animateBarHit(bar) {
511
+ // Animate bar color
512
+ bar.material.emissiveIntensity = 0.5;
513
+
514
+ // Animate bar movement
515
+ const originalY = bar.position.y;
516
+ let animationTime = 0;
517
+
518
+ const animateHit = () => {
519
+ animationTime += 0.05;
520
+
521
+ if (animationTime < 0.2) {
522
+ // Move down
523
+ bar.position.y = originalY - Math.sin(animationTime * Math.PI / 0.2) * 0.1;
524
+ } else if (animationTime < 0.4) {
525
+ // Move back up
526
+ bar.position.y = originalY - Math.sin((animationTime - 0.2) * Math.PI / 0.2) * 0.05;
527
+ } else {
528
+ bar.position.y = originalY;
529
+ bar.material.emissiveIntensity = 0;
530
+ return;
531
+ }
532
+
533
+ requestAnimationFrame(animateHit);
534
  };
535
+
536
+ animateHit();
537
+
538
+ // Animate mallet
539
+ const mallet = mallets[bar.userData.index % 2];
540
+ animateMalletHit(mallet, bar.position);
541
  }
542
 
543
+ function animateMalletHit(mallet, targetPosition) {
544
+ const originalPosition = mallet.position.clone();
545
+ const originalRotation = mallet.rotation.clone();
546
+
547
+ let animationTime = 0;
548
+
549
+ const animateMallet = () => {
550
+ animationTime += 0.05;
551
+
552
+ if (animationTime < 0.3) {
553
+ // Move towards target
554
+ const t = animationTime / 0.3;
555
+ mallet.position.lerpVectors(originalPosition, targetPosition.clone().add(new THREE.Vector3(0, 1, 0)), t);
556
+ mallet.rotation.z = originalRotation.z + Math.sin(t * Math.PI) * 0.5;
557
+ } else if (animationTime < 0.6) {
558
+ // Return to original position
559
+ const t = (animationTime - 0.3) / 0.3;
560
+ mallet.position.lerpVectors(targetPosition.clone().add(new THREE.Vector3(0, 1, 0)), originalPosition, t);
561
+ mallet.rotation.z = originalRotation.z + Math.sin((1 - t) * Math.PI) * 0.5;
562
+ } else {
563
+ mallet.position.copy(originalPosition);
564
+ mallet.rotation.copy(originalRotation);
565
+ return;
566
+ }
567
+
568
+ requestAnimationFrame(animateMallet);
569
+ };
570
+
571
+ animateMallet();
572
+ }
573
 
574
+ function showNoteDisplay(note) {
575
+ const display = document.getElementById('noteDisplay');
576
+ display.textContent = note;
577
+ display.classList.add('show');
578
+
579
+ setTimeout(() => {
580
+ display.classList.remove('show');
581
+ }, 800);
582
+ }
583
 
584
+ function setupEventListeners() {
585
+ // Mouse events
586
+ renderer.domElement.addEventListener('mousedown', onMouseDown);
587
+ renderer.domElement.addEventListener('mousemove', onMouseMove);
588
+ renderer.domElement.addEventListener('mouseup', onMouseUp);
589
+ renderer.domElement.addEventListener('click', onMouseClick);
590
+ renderer.domElement.addEventListener('wheel', onMouseWheel);
591
+
592
+ // Touch events for mobile
593
+ renderer.domElement.addEventListener('touchstart', onTouchStart);
594
+ renderer.domElement.addEventListener('touchmove', onTouchMove);
595
+ renderer.domElement.addEventListener('touchend', onTouchEnd);
596
+
597
+ // Keyboard events
598
+ window.addEventListener('keydown', onKeyDown);
599
+
600
+ // UI controls
601
+ document.getElementById('rotateBtn').addEventListener('click', toggleAutoRotate);
602
+ document.getElementById('resetBtn').addEventListener('click', resetView);
603
+ document.getElementById('soundBtn').addEventListener('click', toggleSound);
604
+ document.getElementById('volume').addEventListener('input', updateVolume);
605
+
606
+ // Window resize
607
+ window.addEventListener('resize', onWindowResize);
608
+ }
609
 
610
+ function onMouseDown(event) {
611
+ isDragging = true;
612
+ previousMousePosition = {
613
+ x: event.clientX,
614
+ y: event.clientY
615
+ };
616
+ }
 
 
 
617
 
618
+ function onMouseMove(event) {
619
+ if (!isDragging) return;
620
+
621
+ const deltaMove = {
622
+ x: event.clientX - previousMousePosition.x,
623
+ y: event.clientY - previousMousePosition.y
624
+ };
625
+
626
+ // Rotate camera around the marimba
627
+ const spherical = new THREE.Spherical();
628
+ spherical.setFromVector3(camera.position);
629
+ spherical.theta -= deltaMove.x * 0.01;
630
+ spherical.phi += deltaMove.y * 0.01;
631
+ spherical.phi = Math.max(0.1, Math.min(Math.PI - 0.1, spherical.phi));
632
+
633
+ camera.position.setFromSpherical(spherical);
634
+ camera.lookAt(0, 0, 0);
635
+
636
+ previousMousePosition = {
637
+ x: event.clientX,
638
+ y: event.clientY
639
+ };
640
+ }
641
 
642
+ function onMouseUp() {
643
+ isDragging = false;
 
 
644
  }
645
 
646
+ function onMouseClick(event) {
647
+ if (isDragging) return;
648
 
649
+ // Calculate mouse position in normalized device coordinates
650
+ mouse.x = (event.clientX / window.innerWidth) * 2 - 1;
651
+ mouse.y = -(event.clientY / window.innerHeight) * 2 + 1;
652
 
653
+ // Update the picking ray with the camera and mouse position
654
+ raycaster.setFromCamera(mouse, camera);
655
+
656
+ // Calculate objects intersecting the picking ray
657
+ const intersects = raycaster.intersectObjects(marimbaBars);
658
+
659
+ if (intersects.length > 0) {
660
+ const bar = intersects[0].object;
661
+ playNote(bar.userData.frequency, bar);
662
  }
663
  }
664
 
665
+ function onMouseWheel(event) {
666
+ event.preventDefault();
667
+
668
+ const zoomSpeed = 0.1;
669
+ const direction = event.deltaY > 0 ? 1 : -1;
670
+
671
+ camera.position.multiplyScalar(1 + direction * zoomSpeed);
672
+
673
+ // Limit zoom
674
+ const distance = camera.position.length();
675
+ if (distance < 5) {
676
+ camera.position.normalize().multiplyScalar(5);
677
+ } else if (distance > 30) {
678
+ camera.position.normalize().multiplyScalar(30);
679
+ }
680
  }
681
 
682
+ function onTouchStart(event) {
683
+ if (event.touches.length === 1) {
684
+ isDragging = true;
685
+ previousMousePosition = {
686
+ x: event.touches[0].clientX,
687
+ y: event.touches[0].clientY
688
+ };
689
+ }
690
  }
691
 
692
+ function onTouchMove(event) {
693
+ if (event.touches.length === 1 && isDragging) {
694
+ const deltaMove = {
695
+ x: event.touches[0].clientX - previousMousePosition.x,
696
+ y: event.touches[0].clientY - previousMousePosition.y
697
+ };
698
+
699
+ const spherical = new THREE.Spherical();
700
+ spherical.setFromVector3(camera.position);
701
+ spherical.theta -= deltaMove.x * 0.01;
702
+ spherical.phi += deltaMove.y * 0.01;
703
+ spherical.phi = Math.max(0.1, Math.min(Math.PI - 0.1, spherical.phi));
704
+
705
+ camera.position.setFromSpherical(spherical);
706
+ camera.lookAt(0, 0, 0);
707
+
708
+ previousMousePosition = {
709
+ x: event.touches[0].clientX,
710
+ y: event.touches[0].clientY
711
+ };
712
+ }
713
  }
714
 
715
+ function onTouchEnd(event) {
716
+ isDragging = false;
717
+
718
+ // Handle tap for playing notes
719
+ if (event.changedTouches.length === 1 && !isDragging) {
720
+ const touch = event.changedTouches[0];
721
+ mouse.x = (touch.clientX / window.innerWidth) * 2 - 1;
722
+ mouse.y = -(touch.clientY / window.innerHeight) * 2 + 1;
723
+
724
+ raycaster.setFromCamera(mouse, camera);
725
+ const intersects = raycaster.intersectObjects(marimbaBars);
726
+
727
+ if (intersects.length > 0) {
728
+ const bar = intersects[0].object;
729
+ playNote(bar.userData.frequency, bar);
730
  }
731
+ }
732
  }
733
 
734
+ function onKeyDown(event) {
735
+ // Map keyboard keys to notes
736
+ const keyMap = {
737
+ 'a': 0, 's': 1, 'd': 2, 'f': 3, 'g': 4, 'h': 5,
738
+ 'j': 6, 'k': 7, 'l': 8, ';': 9, "'": 10, 'Enter': 11
739
+ };
740
+
741
+ const key = event.key.toLowerCase();
742
+ if (keyMap.hasOwnProperty(key) && marimbaBars[keyMap[key]]) {
743
+ const bar = marimbaBars[keyMap[key]];
744
+ playNote(bar.userData.frequency, bar);
745
+ }
746
  }
747
 
748
+ function toggleAutoRotate() {
749
+ autoRotate = !autoRotate;
750
+ const btn = document.getElementById('rotateBtn');
751
+ btn.classList.toggle('active', autoRotate);
752
  }
753
 
754
+ function resetView() {
755
+ camera.position.set(0, 5, 12);
756
+ camera.lookAt(0, 0, 0);
757
  }
758
 
759
+ function toggleSound() {
760
+ soundEnabled = !soundEnabled;
761
+ const btn = document.getElementById('soundBtn');
762
+ btn.classList.toggle('active', soundEnabled);
763
+ btn.textContent = soundEnabled ? 'Sound On' : 'Sound Off';
764
+ }
765
+
766
+ function updateVolume(event) {
767
+ const volume = event.target.value / 100;
768
+ masterGainNode.gain.value = volume;
769
+ }
770
+
771
+ function onWindowResize() {
772
  camera.aspect = window.innerWidth / window.innerHeight;
773
  camera.updateProjectionMatrix();
774
  renderer.setSize(window.innerWidth, window.innerHeight);
775
+ }
776
+
777
+ function animate() {
778
+ requestAnimationFrame(animate);
779
+
780
+ // Auto rotation
781
+ if (autoRotate && !isDragging) {
782
+ const time = Date.now() * 0.0005;
783
+ camera.position.x = Math.sin(time) * 12;
784
+ camera.position.z = Math.cos(time) * 12;
785
+ camera.lookAt(0, 0, 0);
786
+ }
787
+
788
+ // Animate mallets slightly
789
+ mallets.forEach((mallet, index) => {
790
+ mallet.rotation.y = Math.sin(Date.now() * 0.001 + index) * 0.1;
791
+ });
792
+
793
+ renderer.render(scene, camera);
794
+ }
795
 
796
  // Initialize the application
797
+ window.addEventListener('load', init);
798
  </script>
799
  </body>
800
  </html>