ParulPandey commited on
Commit
b7cfff8
·
verified ·
1 Parent(s): 428ddcc

Update index.html

Browse files
Files changed (1) hide show
  1. index.html +814 -18
index.html CHANGED
@@ -1,19 +1,815 @@
1
- <!doctype html>
2
- <html>
3
- <head>
4
- <meta charset="utf-8" />
5
- <meta name="viewport" content="width=device-width" />
6
- <title>My static Space</title>
7
- <link rel="stylesheet" href="style.css" />
8
- </head>
9
- <body>
10
- <div class="card">
11
- <h1>Welcome to your static Space!</h1>
12
- <p>You can modify this app directly by editing <i>index.html</i> in the Files and versions tab.</p>
13
- <p>
14
- Also don't forget to check the
15
- <a href="https://huggingface.co/docs/hub/spaces" target="_blank">Spaces documentation</a>.
16
- </p>
17
- </div>
18
- </body>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
19
  </html>
 
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>Interactive 3D Heart with Hand Gestures</title>
7
+ <style>
8
+ body {
9
+ margin: 0;
10
+ overflow: hidden;
11
+ font-family: Arial, sans-serif;
12
+ background-color: #000020;
13
+ }
14
+ #info {
15
+ position: absolute;
16
+ top: 10px;
17
+ width: 100%;
18
+ text-align: center;
19
+ color: white;
20
+ z-index: 100;
21
+ font-size: 16px;
22
+ text-shadow: 1px 1px 2px black;
23
+ }
24
+ #scene-container {
25
+ position: absolute;
26
+ width: 100%;
27
+ height: 100%;
28
+ }
29
+ #video {
30
+ position: absolute;
31
+ width: 100%;
32
+ height: 100%;
33
+ object-fit: cover;
34
+ z-index: -1;
35
+ transform: scaleX(-1); /* Mirror the video */
36
+ opacity: 0.8; /* Make video slightly transparent */
37
+ }
38
+ .loading {
39
+ position: absolute;
40
+ top: 50%;
41
+ left: 50%;
42
+ transform: translate(-50%, -50%);
43
+ color: #00ffff;
44
+ font-size: 24px;
45
+ text-shadow: 2px 2px 4px rgba(0, 0, 0, 0.8);
46
+ z-index: 100; /* Ensure it's visible */
47
+ transition: opacity 1s ease;
48
+ background-color: rgba(0, 0, 32, 0.7);
49
+ padding: 20px;
50
+ border-radius: 10px;
51
+ border: 1px solid #00ffff;
52
+ box-shadow: 0 0 15px rgba(0, 255, 255, 0.5);
53
+ }
54
+ .sci-fi-overlay {
55
+ position: absolute;
56
+ top: 0;
57
+ left: 0;
58
+ width: 100%;
59
+ height: 100%;
60
+ background: radial-gradient(ellipse at center, rgba(0,20,80,0.2) 0%, rgba(0,10,40,0.6) 100%);
61
+ pointer-events: none;
62
+ z-index: 1;
63
+ }
64
+ .grid {
65
+ position: absolute;
66
+ top: 0;
67
+ left: 0;
68
+ width: 100%;
69
+ height: 100%;
70
+ background-image:
71
+ linear-gradient(rgba(0, 100, 255, 0.1) 1px, transparent 1px),
72
+ linear-gradient(90deg, rgba(0, 100, 255, 0.1) 1px, transparent 1px);
73
+ background-size: 40px 40px;
74
+ pointer-events: none;
75
+ z-index: 2;
76
+ opacity: 0.5;
77
+ }
78
+ #webcamButton {
79
+ position: absolute;
80
+ bottom: 20px;
81
+ left: 50%;
82
+ transform: translateX(-50%);
83
+ background-color: #ff4757;
84
+ color: white;
85
+ border: none;
86
+ padding: 10px 20px;
87
+ border-radius: 50px;
88
+ font-weight: bold;
89
+ cursor: pointer;
90
+ z-index: 100;
91
+ transition: all 0.3s ease;
92
+ }
93
+ #webcamButton:hover {
94
+ background-color: #ff6b81;
95
+ transform: translateX(-50%) scale(1.05);
96
+ }
97
+ #webcamButton:disabled {
98
+ background-color: #555;
99
+ cursor: not-allowed;
100
+ }
101
+ .output_canvas {
102
+ position: absolute;
103
+ width: 100%;
104
+ height: 100%;
105
+ left: 0;
106
+ top: 0;
107
+ z-index: 20;
108
+ pointer-events: none;
109
+ }
110
+
111
+ .gesture-indicator {
112
+ position: absolute;
113
+ bottom: 80px;
114
+ left: 50%;
115
+ transform: translateX(-50%);
116
+ background-color: rgba(0, 0, 0, 0.7);
117
+ color: #00ffff;
118
+ padding: 8px 16px;
119
+ border-radius: 20px;
120
+ font-size: 16px;
121
+ z-index: 100;
122
+ transition: opacity 0.3s ease;
123
+ border: 1px solid #00ffff;
124
+ box-shadow: 0 0 10px rgba(0, 255, 255, 0.5);
125
+ opacity: 0;
126
+ }
127
+
128
+ .controls-guide {
129
+ position: absolute;
130
+ bottom: 20px;
131
+ right: 20px;
132
+ z-index: 100;
133
+ display: flex;
134
+ flex-direction: column;
135
+ gap: 10px;
136
+ }
137
+
138
+ .guide-item {
139
+ display: flex;
140
+ align-items: center;
141
+ background-color: rgba(0, 0, 0, 0.7);
142
+ padding: 8px;
143
+ border-radius: 10px;
144
+ border: 1px solid #00ffff;
145
+ }
146
+
147
+ .guide-icon {
148
+ font-size: 24px;
149
+ margin-right: 10px;
150
+ }
151
+
152
+ .guide-text {
153
+ color: white;
154
+ font-size: 14px;
155
+ }
156
+ </style>
157
+ </head>
158
+ <body>
159
+ <div id="info">Interactive 3D Heart</div>
160
+ <div class="sci-fi-overlay"></div>
161
+ <div class="grid"></div>
162
+ <video id="video" playsinline></video>
163
+ <div id="scene-container"></div>
164
+ <div class="loading" id="loading-text">Loading...</div>
165
+ <button id="webcamButton">Enable Webcam</button>
166
+ <canvas class="output_canvas"></canvas>
167
+ <div style="position:fixed;top:10px;right:10px;z-index:200;">
168
+ <label style="color:#00ffff;font-weight:bold;background:rgba(0,0,32,0.7);padding:6px 12px;border-radius:8px;">
169
+ <input type="checkbox" id="pulseToggle" style="vertical-align:middle;margin-right:6px;"> Heart Pulsing
170
+ </label>
171
+ </div>
172
+ <!-- Legend for gestures -->
173
+ <div id="gesture-legend" style="position:fixed;top:80px;right:10px;z-index:201;background:rgba(0,0,32,0.85);border-radius:12px;padding:18px 22px 18px 18px;box-shadow:0 0 16px #00ffff44;border:1.5px solid #00ffff;max-width:270px;min-width:200px;">
174
+ <div style="color:#00ffff;font-size:18px;font-weight:bold;margin-bottom:10px;text-align:left;">How to Control the Heart</div>
175
+ <div style="display:flex;align-items:flex-start;margin-bottom:12px;">
176
+ <span style="font-size:2em;margin-right:12px;">☝️</span>
177
+ <div>
178
+ <span style="color:#fff;font-weight:bold;">Point One Index Finger</span><br>
179
+ <span style="color:#aaa;font-size:14px;">Rotate &amp; Tilt<br><span style="font-size:12px;">(Like dragging with a mouse, use either hand)</span></span>
180
+ </div>
181
+ </div>
182
+ <div style="display:flex;align-items:flex-start;margin-bottom:12px;">
183
+ <span style="font-size:2em;margin-right:12px;">🖐️</span>
184
+ <div>
185
+ <span style="color:#fff;font-weight:bold;">Spread Hand</span><br>
186
+ <span style="color:#aaa;font-size:14px;">Zoom in/out<br><span style="font-size:12px;">(Like mouse wheel, use either hand)</span></span>
187
+ </div>
188
+ </div>
189
+ <div style="display:flex;align-items:flex-start;margin-bottom:12px;">
190
+ <span style="font-size:2em;margin-right:12px;">✊</span>
191
+ <div>
192
+ <span style="color:#fff;font-weight:bold;">Make a Fist</span><br>
193
+ <span style="color:#aaa;font-size:14px;">Reset Heart<br><span style="font-size:12px;">(Return to starting view)</span></span>
194
+ </div>
195
+ </div>
196
+ <div style="display:flex;align-items:flex-start;margin-bottom:12px;">
197
+ <span style="font-size:2em;margin-right:12px;">✌️</span>
198
+ <div>
199
+ <span style="color:#fff;font-weight:bold;">Peace Sign</span><br>
200
+ <span style="color:#aaa;font-size:14px;">Toggle Pulsing<br><span style="font-size:12px;">(Turn heart pulsing on/off)</span></span>
201
+ </div>
202
+ </div>
203
+ <div style="margin-top:16px;color:#00ffff;font-size:13px;">Tip: Point one finger to rotate/tilt, spread your hand to zoom, make a fist to reset, peace sign to pulse!</div>
204
+ </div>
205
+ <!-- Import Map for ES Modules -->
206
+ <script type="importmap">
207
+ {
208
+ "imports": {
209
+ "three": "https://unpkg.com/[email protected]/build/three.module.js",
210
+ "three/addons/": "https://unpkg.com/[email protected]/examples/jsm/"
211
+ }
212
+ }
213
+ </script>
214
+ <!-- MediaPipe -->
215
+ <script src="https://cdn.jsdelivr.net/npm/@mediapipe/[email protected]/drawing_utils.js"></script>
216
+ <script src="https://cdn.jsdelivr.net/npm/@mediapipe/[email protected]/hands.js"></script>
217
+ <!-- Main Script (using ES modules) -->
218
+ <script type="module">
219
+ import * as THREE from 'three';
220
+ import { GLTFLoader } from 'three/addons/loaders/GLTFLoader.js';
221
+
222
+ // Scene setup
223
+ let scene, camera, renderer, heart;
224
+ let video, hands;
225
+ let rotationSpeed = 0.01;
226
+ let canvasElement, canvasCtx;
227
+
228
+ // Hand tracking variables
229
+ let leftHand = null;
230
+ let rightHand = null;
231
+ let isPinching = false;
232
+ let startPinchRotation = 0;
233
+ let currentRotation = 0;
234
+
235
+ // UI elements
236
+ let loadingText;
237
+
238
+ // Option to enable/disable pulsing
239
+ let pulsingEnabled = false;
240
+
241
+ // Wait for DOM content to load before accessing elements
242
+ document.addEventListener('DOMContentLoaded', () => {
243
+ const webcamButton = document.getElementById('webcamButton');
244
+ webcamButton.addEventListener('click', initApp);
245
+ });
246
+
247
+ // Initialize the app when the button is clicked
248
+ async function initApp() {
249
+ console.log("Initializing application");
250
+
251
+ // Initialize DOM elements
252
+ loadingText = document.getElementById('loading-text');
253
+
254
+ const webcamButton = document.getElementById('webcamButton');
255
+ webcamButton.disabled = true;
256
+
257
+ // Setup canvas for hand tracking visualization
258
+ canvasElement = document.querySelector('.output_canvas');
259
+ canvasCtx = canvasElement.getContext('2d');
260
+
261
+ // Initialize Three.js scene
262
+ setupScene();
263
+
264
+ // Add a debug object to ensure rendering works
265
+ addDebugCube();
266
+
267
+ // Load the heart model
268
+ loadHeartModel();
269
+
270
+ // Setup video and MediaPipe
271
+ await setupMediaPipe();
272
+
273
+ // Start animation loop
274
+ animate();
275
+
276
+ console.log("Initialization complete");
277
+ webcamButton.textContent = 'Webcam Enabled';
278
+ webcamButton.style.backgroundColor = '#4CAF50';
279
+ }
280
+
281
+ function addDebugCube() {
282
+ // Add a simple cube to verify rendering pipeline
283
+ const geometry = new THREE.BoxGeometry(1, 1, 1);
284
+ const material = new THREE.MeshPhongMaterial({ color: 0x00ffff });
285
+ const cube = new THREE.Mesh(geometry, material);
286
+ cube.position.set(0, 0, 0);
287
+ scene.add(cube);
288
+
289
+ // Auto-remove after 5 seconds
290
+ setTimeout(() => {
291
+ scene.remove(cube);
292
+ console.log("Debug cube removed");
293
+ }, 5000);
294
+ }
295
+
296
+ function setupScene() {
297
+ // Create scene
298
+ scene = new THREE.Scene();
299
+ scene.background = null; // Transparent background
300
+
301
+ // Create camera
302
+ const aspect = window.innerWidth / window.innerHeight;
303
+ camera = new THREE.PerspectiveCamera(75, aspect, 0.1, 1000);
304
+ camera.position.z = 5;
305
+
306
+ // Create renderer with alpha: true for transparency
307
+ renderer = new THREE.WebGLRenderer({ antialias: true, alpha: true });
308
+ renderer.setSize(window.innerWidth, window.innerHeight);
309
+ renderer.setClearColor(0x000000, 0); // Fully transparent
310
+ document.getElementById('scene-container').appendChild(renderer.domElement);
311
+
312
+ // Add lights
313
+ const ambientLight = new THREE.AmbientLight(0x404040, 2);
314
+ scene.add(ambientLight);
315
+
316
+ const directionalLight = new THREE.DirectionalLight(0xffffff, 2);
317
+ directionalLight.position.set(1, 1, 1);
318
+ scene.add(directionalLight);
319
+
320
+ const bluePointLight = new THREE.PointLight(0x0044ff, 1.5, 20);
321
+ bluePointLight.position.set(-3, 2, 3);
322
+ scene.add(bluePointLight);
323
+
324
+ const redPointLight = new THREE.PointLight(0xff4400, 1.5, 20);
325
+ redPointLight.position.set(3, -2, 3);
326
+ scene.add(redPointLight);
327
+
328
+ // Setup window resize handler
329
+ window.addEventListener('resize', onWindowResize);
330
+ }
331
+
332
+ function createHeartPlaceholder() {
333
+ console.log("Creating heart placeholder");
334
+
335
+ // Set your desired base scale here for the placeholder
336
+ const baseScale = 3;
337
+ // Create a simple heart-like shape using a sphere
338
+ const geometry = new THREE.SphereGeometry(1.5, 32, 32);
339
+ const material = new THREE.MeshPhongMaterial({
340
+ color: 0xff0066,
341
+ shininess: 100,
342
+ emissive: 0x330000
343
+ });
344
+ heart = new THREE.Mesh(geometry, material);
345
+ heart.scale.set(baseScale, baseScale, baseScale);
346
+ scene.add(heart);
347
+ // Sync pulsingEnabled with checkbox state
348
+ const pulseToggle = document.getElementById('pulseToggle');
349
+ if (pulseToggle) pulsingEnabled = pulseToggle.checked;
350
+ if (pulsingEnabled) addPulsingEffect(heart, baseScale);
351
+ loadingText.textContent = 'Using placeholder heart (models failed to load)';
352
+ setTimeout(() => {
353
+ loadingText.style.display = 'none';
354
+ }, 3000);
355
+ }
356
+
357
+ function loadHeartModel() {
358
+ console.log("Loading heart model...");
359
+ const loader = new GLTFLoader();
360
+
361
+ // Show that we're loading
362
+ loadingText.textContent = 'Loading heart model...';
363
+
364
+ // Set your desired base scale here
365
+ const baseScale = 50; // Try 2, 5, 10, etc. for different sizes
366
+ // Load directly with the available model
367
+ loader.load(
368
+ 'stylizedhumanheart.glb',
369
+ // Success callback
370
+ function(gltf) {
371
+ console.log("Model loaded successfully:", gltf);
372
+ heart = gltf.scene;
373
+ heart.scale.set(baseScale, baseScale, baseScale);
374
+ scene.add(heart);
375
+ // Center the model precisely in the scene
376
+ const box = new THREE.Box3().setFromObject(heart);
377
+ const center = box.getCenter(new THREE.Vector3());
378
+ const size = box.getSize(new THREE.Vector3());
379
+ // Center at (0,0,0) and adjust for any vertical offset
380
+ heart.position.x = -center.x;
381
+ heart.position.y = -center.y + (size.y / 2 - center.y); // shift so geometric center is at 0
382
+ heart.position.z = -center.z;
383
+ // Make sure model is visible
384
+ heart.traverse((node) => {
385
+ if (node.isMesh) {
386
+ node.material.transparent = false;
387
+ node.material.opacity = 1.0;
388
+ node.material.needsUpdate = true;
389
+ }
390
+ });
391
+ // Sync pulsingEnabled with checkbox state
392
+ const pulseToggle = document.getElementById('pulseToggle');
393
+ if (pulseToggle) pulsingEnabled = pulseToggle.checked;
394
+ if (pulsingEnabled) addPulsingEffect(heart, baseScale);
395
+ loadingText.style.display = 'none';
396
+ },
397
+ // Progress callback
398
+ function(xhr) {
399
+ const percent = xhr.loaded / xhr.total * 100;
400
+ loadingText.textContent = `Loading: ${Math.round(percent)}%`;
401
+ },
402
+ // Error callback
403
+ function(error) {
404
+ console.error('Error loading heart model:', error);
405
+ loadingText.textContent = 'Error loading model. Creating placeholder...';
406
+ createHeartPlaceholder();
407
+ }
408
+ );
409
+ }
410
+
411
+ // Use a callback-based loop for MediaPipe Hands
412
+ let mediapipeActive = false;
413
+
414
+ async function mediapipeFrameLoop() {
415
+ if (!mediapipeActive) return;
416
+ if (video && video.readyState === 4 && hands) {
417
+ await hands.send({ image: video });
418
+ }
419
+ // Only request the next frame after the previous one is processed
420
+ if (mediapipeActive) requestAnimationFrame(mediapipeFrameLoop);
421
+ }
422
+
423
+ async function setupMediaPipe() {
424
+ video = document.getElementById('video');
425
+
426
+ try {
427
+ console.log("Requesting camera permission...");
428
+ loadingText.textContent = 'Requesting camera permission...';
429
+
430
+ // Access the webcam with explicit permissions
431
+ const stream = await navigator.mediaDevices.getUserMedia({
432
+ video: { facingMode: 'user' },
433
+ audio: false
434
+ });
435
+ console.log("Camera permission granted:", stream);
436
+ video.srcObject = stream;
437
+
438
+ // Set canvas dimensions to match video
439
+ video.onloadedmetadata = () => {
440
+ console.log("Video metadata loaded");
441
+ video.play();
442
+ canvasElement.width = video.videoWidth;
443
+ canvasElement.height = video.videoHeight;
444
+ loadingText.textContent = 'Camera enabled. Hand tracking active.';
445
+ setTimeout(() => {
446
+ loadingText.style.opacity = '0';
447
+ setTimeout(() => loadingText.style.display = 'none', 1000);
448
+ }, 3000);
449
+ // Start MediaPipe hand tracking loop
450
+ mediapipeActive = true;
451
+ mediapipeFrameLoop();
452
+ };
453
+ } catch (error) {
454
+ console.error("Camera permission denied or error:", error);
455
+ loadingText.textContent = "Please allow camera access for hand tracking to work!";
456
+ webcamButton.disabled = false;
457
+ }
458
+
459
+ // Initialize MediaPipe Hands
460
+ hands = new Hands({
461
+ locateFile: (file) => {
462
+ return `https://cdn.jsdelivr.net/npm/@mediapipe/[email protected]/${file}`;
463
+ }
464
+ });
465
+
466
+ hands.setOptions({
467
+ maxNumHands: 2,
468
+ modelComplexity: 1,
469
+ minDetectionConfidence: 0.6, // Increased for more reliable detection
470
+ minTrackingConfidence: 0.5,
471
+ selfieMode: true // Correctly handle mirrored camera feed
472
+ });
473
+
474
+ hands.onResults(onHandResults);
475
+
476
+ // Remove the old event listener for loadeddata
477
+ // video.addEventListener('loadeddata', async () => {
478
+ // await hands.send({ image: video });
479
+ // });
480
+ }
481
+
482
+ function onHandResults(results) {
483
+ leftHand = null;
484
+ rightHand = null;
485
+
486
+ // Clear canvas
487
+ canvasCtx.clearRect(0, 0, canvasElement.width, canvasElement.height);
488
+
489
+ if (results.multiHandLandmarks && results.multiHandLandmarks.length > 0) {
490
+ for (let i = 0; i < results.multiHandLandmarks.length; i++) {
491
+ const handLandmarks = results.multiHandLandmarks[i];
492
+ const handedness = results.multiHandedness[i].label;
493
+
494
+ // Note: MediaPipe hand detection is mirrored, so "Left" hand is actually right hand in the video
495
+ if (handedness === 'Left') {
496
+ rightHand = handLandmarks; // Mapping correctly to user's perspective
497
+ } else if (handedness === 'Right') {
498
+ leftHand = handLandmarks; // Mapping correctly to user's perspective
499
+ }
500
+
501
+ // Draw only index fingertip as a small dot for clarity
502
+ const indexTip = handLandmarks[8];
503
+ canvasCtx.beginPath();
504
+ canvasCtx.arc(
505
+ indexTip.x * canvasElement.width,
506
+ indexTip.y * canvasElement.height,
507
+ 5,
508
+ 0,
509
+ 2 * Math.PI
510
+ );
511
+ canvasCtx.fillStyle = '#00ffff';
512
+ canvasCtx.fill();
513
+ }
514
+ }
515
+
516
+ // Process hand gestures and get activity status (no on-screen gesture indicator)
517
+ processHandGestures();
518
+ }
519
+
520
+ // Track current gesture for UI updates
521
+ let currentGesture = null;
522
+
523
+ // Remove gesture indicator text and hide the indicator (no-op)
524
+ function updateGestureIndicator(gesture) {
525
+ // No operation: all gesture indicator UI is removed
526
+ }
527
+
528
+ function processHandGestures() {
529
+ // Peace sign (index and middle fingers extended, others folded) toggles pulsing (debounced)
530
+ function isPeaceSign(hand) {
531
+ if (!hand) return false;
532
+ const palm = hand[0];
533
+ // Index and middle tips far, others close
534
+ const indexTip = hand[8];
535
+ const middleTip = hand[12];
536
+ if (!indexTip || !middleTip) return false;
537
+ const indexDist = calculateDistance(palm, indexTip);
538
+ const middleDist = calculateDistance(palm, middleTip);
539
+ let folded = 0;
540
+ [4,16,20].forEach(i => {
541
+ const tip = hand[i];
542
+ if (!tip) return;
543
+ const d = calculateDistance(palm, tip);
544
+ if (d < 0.08) folded++;
545
+ });
546
+ // Both index and middle extended, at least 2 others folded
547
+ return (indexDist > 0.16 && middleDist > 0.16 && folded >= 2);
548
+ }
549
+
550
+ if (!processHandGestures.lastPeace) processHandGestures.lastPeace = false;
551
+ const peaceNow = (leftHand && isPeaceSign(leftHand)) || (rightHand && isPeaceSign(rightHand));
552
+ if (peaceNow && !processHandGestures.lastPeace) {
553
+ pulsingEnabled = !pulsingEnabled;
554
+ if (heart) {
555
+ let scale = heart.scale.x;
556
+ if (pulsingEnabled) {
557
+ addPulsingEffect(heart, scale);
558
+ } else {
559
+ delete heart.userData.update;
560
+ heart.scale.set(scale, scale, scale);
561
+ }
562
+ }
563
+ }
564
+ processHandGestures.lastPeace = peaceNow;
565
+ if (!heart) return;
566
+
567
+ // Keep track of whether any gesture is active
568
+ let gestureActive = false;
569
+
570
+ // Minimal gestures: left index finger = rotate/tilt, left hand spread = zoom, fist = reset
571
+ let leftIndex = leftHand ? leftHand[8] : null;
572
+
573
+ // Helper: detect fist (all tips close to palm)
574
+ function isFist(hand) {
575
+ if (!hand) return false;
576
+ const palm = hand[0];
577
+ let closed = 0;
578
+ [4,8,12,16,20].forEach(i => {
579
+ const tip = hand[i];
580
+ if (!tip) return;
581
+ const d = calculateDistance(palm, tip);
582
+ if (d < 0.08) closed++;
583
+ });
584
+ return closed >= 4;
585
+ }
586
+
587
+ // If either hand makes a fist, reset heart position (rotation, tilt, zoom, and pulsing off)
588
+ if ((leftHand && isFist(leftHand)) || (rightHand && isFist(rightHand))) {
589
+ // Reset to starting position: facing forward, upright, default zoom
590
+ heart.rotation.set(0, 0, 0);
591
+ heart.position.set(0, 0, 0);
592
+ camera.position.set(0, 0, 5);
593
+ // Turn off pulsing
594
+ pulsingEnabled = false;
595
+ if (heart) {
596
+ delete heart.userData.update;
597
+ let scale = heart.scale.x;
598
+ heart.scale.set(scale, scale, scale);
599
+ }
600
+ processHandGestures.lastLeftIndex = null;
601
+ processHandGestures.pinchSmoothing = null;
602
+ processHandGestures.xSmoothing = null;
603
+ return true;
604
+ }
605
+
606
+ // If left index finger is visible, allow rotation/tilt (like mouse drag)
607
+ if (leftIndex) {
608
+ gestureActive = true;
609
+
610
+ // Smoothing variables (static across calls)
611
+ if (!processHandGestures.lastLeftIndex) {
612
+ processHandGestures.lastLeftIndex = { x: leftIndex.x, y: leftIndex.y };
613
+ }
614
+ if (!processHandGestures.pinchSmoothing) {
615
+ processHandGestures.pinchSmoothing = { lastRotation: heart ? heart.rotation.y : 0, velocity: 0 };
616
+ }
617
+ if (!processHandGestures.xSmoothing) {
618
+ processHandGestures.xSmoothing = { lastX: heart ? heart.rotation.x : 0, velocity: 0 };
619
+ }
620
+ const smoothing = processHandGestures.pinchSmoothing;
621
+ const xSmooth = processHandGestures.xSmoothing;
622
+
623
+ // Calculate movement deltas (in screen space)
624
+ const deltaX = leftIndex.x - processHandGestures.lastLeftIndex.x;
625
+ const deltaY = leftIndex.y - processHandGestures.lastLeftIndex.y;
626
+
627
+ // Sensitivity (tweak as needed)
628
+ const ROTATE_SENS = 7.5; // More sensitive for easier rotation
629
+ const TILT_SENS = 7.5;
630
+
631
+ // Horizontal rotation (Y axis): move left = rotate right, move right = rotate left (mirrored webcam)
632
+ let targetY = heart.rotation.y + deltaX * ROTATE_SENS;
633
+ // Remove clamping for full 360 rotation
634
+ smoothing.velocity = (targetY - smoothing.lastRotation) * 0.4;
635
+ smoothing.lastRotation += smoothing.velocity;
636
+ heart.rotation.y = smoothing.lastRotation;
637
+
638
+ // Vertical tilt (X axis): move up = tilt up, move down = tilt down (natural)
639
+ let targetX = heart.rotation.x + deltaY * TILT_SENS;
640
+ // Clamp only X axis (tilt), not Y (rotation)
641
+ targetX = THREE.MathUtils.clamp(targetX, -Math.PI/2, Math.PI/2);
642
+ xSmooth.velocity = (targetX - xSmooth.lastX) * 0.4;
643
+ xSmooth.lastX += xSmooth.velocity;
644
+ heart.rotation.x = xSmooth.lastX;
645
+
646
+ // Update last position
647
+ processHandGestures.lastLeftIndex.x = leftIndex.x;
648
+ processHandGestures.lastLeftIndex.y = leftIndex.y;
649
+ } else {
650
+ // Not rotating, keep last rotation (locked), but allow zoom
651
+ if (processHandGestures.pinchSmoothing && heart) processHandGestures.pinchSmoothing.lastRotation = heart.rotation.y;
652
+ if (processHandGestures.xSmoothing && heart) processHandGestures.xSmoothing.lastX = heart.rotation.x;
653
+ // Reset lastLeftIndex so next time we don't get a big jump
654
+ processHandGestures.lastLeftIndex = null;
655
+ }
656
+
657
+ // Helper: detect fist (all tips close to palm)
658
+ function isFist(hand) {
659
+ // Compare tip (4,8,12,16,20) to palm (0)
660
+ const palm = hand[0];
661
+ let closed = 0;
662
+ [4,8,12,16,20].forEach(i => {
663
+ const tip = hand[i];
664
+ const d = calculateDistance(palm, tip);
665
+ if (d < 0.08) closed++;
666
+ });
667
+ return closed >= 4;
668
+ }
669
+
670
+ // Process open hand gesture (for zoom)
671
+ if (leftHand) {
672
+ gestureActive = true;
673
+
674
+ // Improved method: use distance between thumb and pinky
675
+ const thumb = leftHand[4];
676
+ const pinky = leftHand[20];
677
+
678
+ if (thumb && pinky) {
679
+ const handSpread = calculateDistance(thumb, pinky);
680
+
681
+ // Map the hand spread to camera zoom with better sensitivity
682
+ const zoomFactor = THREE.MathUtils.lerp(10, 3, handSpread * 2);
683
+
684
+ // Smooth camera movement
685
+ camera.position.z = THREE.MathUtils.lerp(
686
+ camera.position.z,
687
+ zoomFactor,
688
+ 0.1 // Smoothing factor
689
+ );
690
+ }
691
+ }
692
+
693
+ // Return whether any gesture is active to control auto-rotation
694
+ return gestureActive;
695
+ }
696
+
697
+ function calculateDistance(point1, point2) {
698
+ return Math.sqrt(
699
+ Math.pow(point1.x - point2.x, 2) +
700
+ Math.pow(point1.y - point2.y, 2)
701
+ );
702
+ }
703
+
704
+ function addPulsingEffect(model, baseScale = 1.0) {
705
+ let time = 0;
706
+ // Heartbeat: very sharp up, very quick drop, more visible
707
+ model.userData.update = function(delta) {
708
+ time += delta;
709
+ // 1.4 Hz = ~84 bpm (children's heart rate, slightly faster)
710
+ const freq = 1.4;
711
+ const t = (time * freq) % 1.0;
712
+ // Sharper, more prominent heartbeat: very sharp peak, quick drop
713
+ let beat = Math.exp(-60 * (t - 0.12) * (t - 0.12)) * 2.2; // much sharper, higher
714
+ beat += 0.22 * Math.max(0, Math.sin(Math.PI * t)); // more visible base pulse
715
+ const pulseFactor = 1.0 + 0.13 * beat;
716
+ model.scale.set(
717
+ baseScale * pulseFactor,
718
+ baseScale * pulseFactor,
719
+ baseScale * pulseFactor
720
+ );
721
+ };
722
+ }
723
+
724
+ function onWindowResize() {
725
+ camera.aspect = window.innerWidth / window.innerHeight;
726
+ camera.updateProjectionMatrix();
727
+ renderer.setSize(window.innerWidth, window.innerHeight);
728
+
729
+ // Update canvas size if needed
730
+ if (canvasElement && video) {
731
+ canvasElement.width = video.videoWidth;
732
+ canvasElement.height = video.videoHeight;
733
+ }
734
+ }
735
+
736
+ // Track hand presence for auto-rotation
737
+ let handsDetectedTime = 0;
738
+ let lastHandsDetectedState = false;
739
+
740
+ function animate() {
741
+ requestAnimationFrame(animate);
742
+
743
+ // Apply heart pulsing effect only if enabled
744
+ if (pulsingEnabled && heart && heart.userData.update) {
745
+ heart.userData.update(0.01);
746
+ }
747
+
748
+ // Check if hands are detected
749
+ const handsDetected = leftHand || rightHand;
750
+
751
+ // Track when hands state changes for smoother transitions
752
+ if (handsDetected !== lastHandsDetectedState) {
753
+ lastHandsDetectedState = handsDetected;
754
+ handsDetectedTime = Date.now();
755
+
756
+ // Update gesture indicator when hands disappear
757
+ if (!handsDetected) {
758
+ updateGestureIndicator("No Hands Detected");
759
+ }
760
+ }
761
+
762
+ // Remove automatic rotation and wobble
763
+ // if (heart) {
764
+ // if (!handsDetected) {
765
+ // // Auto rotation only when no hands detected
766
+ // const timeSinceHandsGone = Date.now() - handsDetectedTime;
767
+ // // Start auto-rotation only after hands have been absent for 1.5 seconds
768
+ // if (timeSinceHandsGone > 1500) {
769
+ // // Gradually increase rotation speed
770
+ // const speedFactor = Math.min(1.0, (timeSinceHandsGone - 1500) / 2000);
771
+ // heart.rotation.y += rotationSpeed * speedFactor;
772
+ // }
773
+ // }
774
+ // // Add slight wobble for more dynamic presentation when no specific interaction
775
+ // if (!isPinching && !handsDetected) {
776
+ // const wobble = Math.sin(Date.now() * 0.001) * 0.005;
777
+ // heart.rotation.x = wobble;
778
+ // }
779
+ // }
780
+
781
+ renderer.render(scene, camera);
782
+ }
783
+ </script>
784
+
785
+ <script>
786
+ // UI for toggling pulsing
787
+ // This script must run after the main script so pulsingEnabled and heart are available
788
+ // If you want the heart to pulse by default, set pulsingEnabled = true above
789
+
790
+ document.addEventListener('DOMContentLoaded', () => {
791
+ const pulseToggle = document.getElementById('pulseToggle');
792
+ if (pulseToggle) {
793
+ pulseToggle.checked = false;
794
+ pulseToggle.addEventListener('change', (e) => {
795
+ pulsingEnabled = e.target.checked;
796
+ // Remove/update pulsing effect on the fly
797
+ if (heart) {
798
+ if (pulsingEnabled) {
799
+ // Re-add pulsing effect with current scale
800
+ let scale = heart.scale.x;
801
+ addPulsingEffect(heart, scale);
802
+ } else {
803
+ // Remove pulsing effect
804
+ delete heart.userData.update;
805
+ // Reset to current scale
806
+ let scale = heart.scale.x;
807
+ heart.scale.set(scale, scale, scale);
808
+ }
809
+ }
810
+ });
811
+ }
812
+ });
813
+ </script>
814
+ </body>
815
  </html>