JamesToth commited on
Commit
1d6eb75
·
verified ·
1 Parent(s): adf9abb

Update index.html

Browse files
Files changed (1) hide show
  1. index.html +243 -262
index.html CHANGED
@@ -3,385 +3,366 @@
3
  <head>
4
  <meta charset="UTF-8">
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
- <title>Hand Control Particle System</title>
7
  <style>
8
- body { margin: 0; overflow: hidden; background-color: #050505; font-family: 'Segoe UI', sans-serif; }
 
9
 
10
- /* Video hidden, used for processing */
11
- #input-video { display: none; }
12
-
13
- /* UI Overlay */
14
- #ui-container {
15
  position: absolute;
16
- top: 20px;
17
- left: 20px;
18
- width: 280px;
19
- padding: 20px;
20
- background: rgba(20, 20, 20, 0.6);
21
- backdrop-filter: blur(10px);
22
- border-radius: 16px;
23
- border: 1px solid rgba(255, 255, 255, 0.1);
24
- color: white;
25
- z-index: 10;
26
- box-shadow: 0 4px 30px rgba(0, 0, 0, 0.5);
27
- transition: all 0.3s ease;
28
  }
29
 
30
- h1 { font-size: 1.2rem; margin: 0 0 15px 0; font-weight: 300; letter-spacing: 1px; }
31
 
32
- .control-group { margin-bottom: 15px; }
33
- label { display: block; font-size: 0.8rem; margin-bottom: 5px; color: #aaa; }
34
 
35
  select, input[type="color"] {
36
  width: 100%;
37
- padding: 8px;
38
  border-radius: 8px;
39
- border: none;
40
- background: rgba(255,255,255,0.1);
41
  color: white;
42
  outline: none;
43
- cursor: pointer;
 
44
  }
45
-
46
- select option { background: #222; }
47
 
48
- .status {
49
- font-size: 0.75rem;
50
- color: #00ff88;
51
- margin-top: 10px;
 
 
 
 
 
 
 
 
 
 
52
  display: flex;
53
  align-items: center;
54
- gap: 6px;
 
 
55
  }
56
 
57
- .dot { width: 8px; height: 8px; background: #00ff88; border-radius: 50%; box-shadow: 0 0 5px #00ff88;}
58
- .dot.inactive { background: #ff4444; box-shadow: 0 0 5px #ff4444; }
59
 
60
- /* Loading Overlay */
61
- #loading {
62
  position: absolute;
63
- top: 50%; left: 50%;
64
- transform: translate(-50%, -50%);
65
- color: white;
66
- font-size: 1.5rem;
 
67
  pointer-events: none;
68
  }
69
  </style>
70
  <script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/r128/three.min.js"></script>
71
  <script src="https://cdn.jsdelivr.net/npm/@mediapipe/camera_utils/camera_utils.js" crossorigin="anonymous"></script>
72
  <script src="https://cdn.jsdelivr.net/npm/@mediapipe/control_utils/control_utils.js" crossorigin="anonymous"></script>
73
- <script src="https://cdn.jsdelivr.net/npm/@mediapipe/drawing_utils/drawing_utils.js" crossorigin="anonymous"></script>
74
  <script src="https://cdn.jsdelivr.net/npm/@mediapipe/hands/hands.js" crossorigin="anonymous"></script>
75
  </head>
76
  <body>
77
 
78
- <div id="loading">Initializing AI & Graphics...</div>
79
-
80
- <div id="ui-container">
81
- <h1>Particle Controller</h1>
82
 
83
- <div class="control-group">
84
- <label>Shape Template</label>
85
- <select id="shape-select">
86
- <option value="heart">Love Heart</option>
87
- <option value="saturn">Saturn Ring</option>
88
  <option value="galaxy">Spiral Galaxy</option>
89
- <option value="fireworks">Fireworks</option>
 
90
  <option value="sphere">Quantum Sphere</option>
 
91
  </select>
92
  </div>
93
 
94
- <div class="control-group">
95
- <label>Particle Color</label>
96
- <input type="color" id="color-picker" value="#00ffff">
97
  </div>
98
 
99
- <div class="status">
100
- <div id="status-dot" class="dot inactive"></div>
101
- <span id="status-text">Waiting for camera...</span>
102
  </div>
103
 
104
- <p style="font-size: 0.7rem; color: #666; margin-top: 15px;">
105
- Instruction: Show both hands. Move hands apart to expand. Clench fists to vibrate particles.
106
- </p>
 
 
 
107
  </div>
108
 
109
- <video id="input-video"></video>
 
110
 
111
  <script>
112
- // --- 1. CONFIGURATION ---
113
- const PARTICLE_COUNT = 15000;
114
- const PARTICLE_SIZE = 0.04;
115
-
116
- // State
117
- const state = {
118
- shape: 'heart',
119
- color: new THREE.Color(0x00ffff),
120
- handDistance: 1, // Multiplier for expansion
121
- handTension: 0, // Multiplier for jitter
122
- targetPositions: new Float32Array(PARTICLE_COUNT * 3),
123
  };
124
 
125
- // --- 2. THREE.JS SETUP ---
126
  const scene = new THREE.Scene();
127
- scene.fog = new THREE.FogExp2(0x050505, 0.05);
 
128
 
129
- const camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 1000);
130
- camera.position.z = 8;
131
 
132
- const renderer = new THREE.WebGLRenderer({ antialias: true, alpha: true });
133
  renderer.setSize(window.innerWidth, window.innerHeight);
134
  renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
135
  document.body.appendChild(renderer.domElement);
136
 
137
- // Particles
 
138
  const geometry = new THREE.BufferGeometry();
139
- const positions = new Float32Array(PARTICLE_COUNT * 3);
140
 
141
- // Initialize random positions
142
- for(let i=0; i<PARTICLE_COUNT*3; i++) {
143
- positions[i] = (Math.random() - 0.5) * 20;
144
  }
145
 
146
  geometry.setAttribute('position', new THREE.BufferAttribute(positions, 3));
147
 
148
  const material = new THREE.PointsMaterial({
149
- size: PARTICLE_SIZE,
150
- color: state.color,
151
- transparent: true,
152
- opacity: 0.8,
153
  blending: THREE.AdditiveBlending,
154
- depthWrite: false
 
 
155
  });
156
 
157
- const particles = new THREE.Points(geometry, material);
158
- scene.add(particles);
159
-
160
- // --- 3. SHAPE GENERATORS ---
161
- // Helper to map sphere coordinates
162
- function randomSpherePoint() {
163
- const u = Math.random();
164
- const v = Math.random();
165
- const theta = 2 * Math.PI * u;
166
- const phi = Math.acos(2 * v - 1);
167
- let r = 3;
168
- const x = r * Math.sin(phi) * Math.cos(theta);
169
- const y = r * Math.sin(phi) * Math.sin(theta);
170
- const z = r * Math.cos(phi);
171
- return {x, y, z};
172
- }
173
 
174
  const generators = {
175
- heart: (i) => {
176
- const t = Math.random() * Math.PI * 2;
177
- const u = Math.random() * Math.PI * 2; // density distribution
178
- // Heart formula
179
- let x = 16 * Math.pow(Math.sin(t), 3);
180
- let y = 13 * Math.cos(t) - 5 * Math.cos(2*t) - 2 * Math.cos(3*t) - Math.cos(4*t);
181
- let z = (Math.random()-0.5) * 4; // Thickness
182
-
183
- // Scale down
184
- return { x: x * 0.2, y: y * 0.2, z: z };
185
- },
186
  sphere: (i) => {
187
- return randomSpherePoint();
 
 
 
 
 
 
 
 
 
188
  },
189
- saturn: (i) => {
190
- // 70% Ring, 30% Planet
191
- if (Math.random() > 0.3) {
192
- // Ring
193
- const angle = Math.random() * Math.PI * 2;
194
- const r = 4 + Math.random() * 2;
195
- return {
196
- x: Math.cos(angle) * r,
197
- y: (Math.random() - 0.5) * 0.2,
198
- z: Math.sin(angle) * r
199
- };
200
- } else {
201
- // Planet
202
- const p = randomSpherePoint();
203
- return { x: p.x * 0.6, y: p.y * 0.6, z: p.z * 0.6 };
204
- }
205
  },
206
  galaxy: (i) => {
207
- const branches = 3;
208
- const spin = i / PARTICLE_COUNT * branches;
209
- const radius = (i / PARTICLE_COUNT) * 6;
210
  const angle = spin * Math.PI * 2;
211
- const randomOffset = (Math.random() - 0.5);
212
-
213
  return {
214
- x: Math.cos(angle) * radius + randomOffset,
215
- y: (Math.random() - 0.5) * (1 - radius/7), // Thicker at center
216
- z: Math.sin(angle) * radius + randomOffset
217
  };
218
  },
219
- fireworks: (i) => {
220
- const p = randomSpherePoint();
221
- const burst = Math.random() * 6;
222
- return { x: p.x * burst, y: p.y * burst, z: p.z * burst };
 
 
 
 
 
 
 
 
 
 
 
 
 
223
  }
224
  };
225
 
226
- function updateTargetShape() {
227
  const generator = generators[state.shape];
228
- for (let i = 0; i < PARTICLE_COUNT; i++) {
229
  const pos = generator(i);
230
- state.targetPositions[i * 3] = pos.x;
231
- state.targetPositions[i * 3 + 1] = pos.y;
232
- state.targetPositions[i * 3 + 2] = pos.z;
233
  }
234
  }
 
235
 
236
- // Initialize shape
237
- updateTargetShape();
 
 
238
 
239
- // --- 4. MEDIAPIPE HAND TRACKING ---
240
- const videoElement = document.getElementById('input-video');
241
-
242
- function onResults(results) {
243
- document.getElementById('loading').style.display = 'none';
244
- const statusDot = document.getElementById('status-dot');
245
- const statusText = document.getElementById('status-text');
246
 
247
  if (results.multiHandLandmarks && results.multiHandLandmarks.length > 0) {
248
- statusDot.classList.remove('inactive');
249
- statusText.innerText = "Tracking Active";
250
-
251
- // 1. Detect Expansion (Distance between two wrists if 2 hands present)
252
- if (results.multiHandLandmarks.length === 2) {
253
- const hand1 = results.multiHandLandmarks[0][0]; // Wrist
254
- const hand2 = results.multiHandLandmarks[1][0]; // Wrist
255
-
256
- // Calculate simple Euclidean distance in screen space
257
- const dx = hand1.x - hand2.x;
258
- const dy = hand1.y - hand2.y;
259
- const dist = Math.sqrt(dx*dx + dy*dy);
260
-
261
- // Map distance: 0.2 is close, 0.8 is far. Map to scale 0.5 to 2.0
262
- state.handDistance = THREE.MathUtils.mapLinear(dist, 0.1, 0.8, 0.5, 2.5);
263
- } else {
264
- // Default if 1 hand
265
- state.handDistance = 1;
266
- }
267
 
268
- // 2. Detect Tension (Closed Hand)
269
- // Measure distance between Wrist (0) and Middle Finger Tip (12)
270
- let totalOpenness = 0;
271
- results.multiHandLandmarks.forEach(landmarks => {
272
- const wrist = landmarks[0];
273
- const tip = landmarks[12];
274
- const d = Math.sqrt(Math.pow(wrist.x - tip.x, 2) + Math.pow(wrist.y - tip.y, 2));
275
- totalOpenness += d;
276
- });
277
 
278
- // Normalize openness approximately (0.1 is fist, 0.3+ is open)
279
- const avgOpenness = totalOpenness / results.multiHandLandmarks.length;
 
 
 
 
 
 
 
 
 
 
 
 
280
 
281
- // If openness is low (fist), tension is high
282
- if (avgOpenness < 0.15) {
283
- state.handTension = THREE.MathUtils.lerp(state.handTension, 1.0, 0.1);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
284
  } else {
285
- state.handTension = THREE.MathUtils.lerp(state.handTension, 0.0, 0.1);
286
  }
287
 
288
  } else {
289
- statusDot.classList.add('inactive');
290
- statusText.innerText = "No Hands Detected";
291
- state.handDistance = THREE.MathUtils.lerp(state.handDistance, 1, 0.05);
292
- state.handTension = THREE.MathUtils.lerp(state.handTension, 0, 0.05);
 
 
293
  }
294
  }
295
 
296
- const hands = new Hands({locateFile: (file) => {
297
- return `https://cdn.jsdelivr.net/npm/@mediapipe/hands/${file}`;
298
- }});
299
-
300
  hands.setOptions({
301
- maxNumHands: 2,
302
  modelComplexity: 1,
303
- minDetectionConfidence: 0.5,
304
- minTrackingConfidence: 0.5
305
- });
306
-
307
- hands.onResults(onResults);
308
-
309
- // Initialize Camera
310
- const cameraUtils = new Camera(videoElement, {
311
- onFrame: async () => {
312
- await hands.send({image: videoElement});
313
- },
314
- width: 640,
315
- height: 480
316
- });
317
- cameraUtils.start();
318
-
319
- // --- 5. UI INTERACTION ---
320
- document.getElementById('shape-select').addEventListener('change', (e) => {
321
- state.shape = e.target.value;
322
- updateTargetShape();
323
  });
 
324
 
325
- document.getElementById('color-picker').addEventListener('input', (e) => {
326
- material.color.set(e.target.value);
 
 
 
327
  });
 
328
 
329
- // --- 6. ANIMATION LOOP ---
330
  const clock = new THREE.Clock();
331
 
332
  function animate() {
333
  requestAnimationFrame(animate);
334
 
335
- const time = clock.getElapsedTime();
336
- const positions = particles.geometry.attributes.position.array;
 
337
 
338
- // Interaction smoothing
339
- const targetScale = state.handDistance;
340
- const jitterIntensity = state.handTension * 0.2; // How much they shake when fist is closed
 
 
341
 
342
- for (let i = 0; i < PARTICLE_COUNT; i++) {
 
 
 
 
 
 
343
  const ix = i * 3;
344
- const iy = i * 3 + 1;
345
- const iz = i * 3 + 2;
346
-
347
- // Get target base position
348
- let tx = state.targetPositions[ix];
349
- let ty = state.targetPositions[iy];
350
- let tz = state.targetPositions[iz];
351
-
352
- // Apply Scale (Hand Distance)
353
- tx *= targetScale;
354
- ty *= targetScale;
355
- tz *= targetScale;
356
-
357
- // Apply Jitter (Hand Tension)
358
- if (state.handTension > 0.1) {
359
- tx += (Math.random() - 0.5) * jitterIntensity;
360
- ty += (Math.random() - 0.5) * jitterIntensity;
361
- tz += (Math.random() - 0.5) * jitterIntensity;
362
- }
363
 
364
- // Simple Lerp for smooth transition
365
- positions[ix] += (tx - positions[ix]) * 0.05;
366
- positions[iy] += (ty - positions[iy]) * 0.05;
367
- positions[iz] += (tz - positions[iz]) * 0.05;
368
- }
369
 
370
- // Slight rotation for the whole system
371
- particles.rotation.y = time * 0.1;
372
-
373
- // Dynamic Wave Effect if idle
374
- if (state.handTension < 0.1) {
375
- // particles.rotation.z = Math.sin(time * 0.5) * 0.1;
 
 
 
376
  }
377
 
378
- particles.geometry.attributes.position.needsUpdate = true;
379
  renderer.render(scene, camera);
380
  }
381
 
382
  animate();
383
 
384
- // Resize Handler
385
  window.addEventListener('resize', () => {
386
  camera.aspect = window.innerWidth / window.innerHeight;
387
  camera.updateProjectionMatrix();
 
3
  <head>
4
  <meta charset="UTF-8">
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>Single Hand Particle System</title>
7
  <style>
8
+ body { margin: 0; overflow: hidden; background-color: #080808; font-family: 'Inter', system-ui, sans-serif; }
9
+ #webcam-feed { display: none; }
10
 
11
+ #glass-panel {
 
 
 
 
12
  position: absolute;
13
+ top: 24px;
14
+ left: 24px;
15
+ width: 300px;
16
+ padding: 24px;
17
+ background: rgba(20, 20, 20, 0.7);
18
+ backdrop-filter: blur(12px);
19
+ border-radius: 20px;
20
+ border: 1px solid rgba(255, 255, 255, 0.08);
21
+ color: #ffffff;
22
+ z-index: 100;
23
+ box-shadow: 0 8px 32px rgba(0, 0, 0, 0.4);
 
24
  }
25
 
26
+ h2 { font-size: 14px; text-transform: uppercase; letter-spacing: 2px; color: #888; margin: 0 0 16px 0; }
27
 
28
+ .control-row { margin-bottom: 16px; }
29
+ label { display: block; font-size: 12px; margin-bottom: 8px; color: #ccc; font-weight: 500; }
30
 
31
  select, input[type="color"] {
32
  width: 100%;
33
+ padding: 10px;
34
  border-radius: 8px;
35
+ border: 1px solid rgba(255,255,255,0.1);
36
+ background: rgba(0,0,0,0.3);
37
  color: white;
38
  outline: none;
39
+ font-size: 13px;
40
+ transition: border-color 0.2s;
41
  }
 
 
42
 
43
+ select:hover { border-color: #00ffff; }
44
+
45
+ .metric-display {
46
+ display: flex;
47
+ justify-content: space-between;
48
+ font-size: 11px;
49
+ color: #666;
50
+ margin-top: 4px;
51
+ }
52
+
53
+ .status-indicator {
54
+ margin-top: 20px;
55
+ padding-top: 20px;
56
+ border-top: 1px solid rgba(255,255,255,0.1);
57
  display: flex;
58
  align-items: center;
59
+ gap: 8px;
60
+ font-size: 12px;
61
+ color: #00ff9d;
62
  }
63
 
64
+ .led { width: 6px; height: 6px; background: #00ff9d; border-radius: 50%; box-shadow: 0 0 8px #00ff9d; }
65
+ .led.off { background: #ff3333; box-shadow: 0 0 8px #ff3333; }
66
 
67
+ #overlay-msg {
 
68
  position: absolute;
69
+ bottom: 30px;
70
+ width: 100%;
71
+ text-align: center;
72
+ color: rgba(255,255,255,0.4);
73
+ font-size: 12px;
74
  pointer-events: none;
75
  }
76
  </style>
77
  <script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/r128/three.min.js"></script>
78
  <script src="https://cdn.jsdelivr.net/npm/@mediapipe/camera_utils/camera_utils.js" crossorigin="anonymous"></script>
79
  <script src="https://cdn.jsdelivr.net/npm/@mediapipe/control_utils/control_utils.js" crossorigin="anonymous"></script>
 
80
  <script src="https://cdn.jsdelivr.net/npm/@mediapipe/hands/hands.js" crossorigin="anonymous"></script>
81
  </head>
82
  <body>
83
 
84
+ <div id="glass-panel">
85
+ <h2>Controller</h2>
 
 
86
 
87
+ <div class="control-row">
88
+ <label>Geometry</label>
89
+ <select id="geometry-selector">
 
 
90
  <option value="galaxy">Spiral Galaxy</option>
91
+ <option value="heart">Digital Heart</option>
92
+ <option value="dna">DNA Helix</option>
93
  <option value="sphere">Quantum Sphere</option>
94
+ <option value="cube">Hyper Cube</option>
95
  </select>
96
  </div>
97
 
98
+ <div class="control-row">
99
+ <label>Color Tone</label>
100
+ <input type="color" id="color-selector" value="#00ffff">
101
  </div>
102
 
103
+ <div class="status-indicator">
104
+ <div id="cam-led" class="led off"></div>
105
+ <span id="cam-status">Initializing AI...</span>
106
  </div>
107
 
108
+ <div style="margin-top: 15px; font-size: 11px; color: #888; line-height: 1.6;">
109
+ <strong>Gestures:</strong><br>
110
+ • 🤏 Pinch to Scale<br>
111
+ • ✋ Move to Rotate<br>
112
+ • ✊ Fist to Explode
113
+ </div>
114
  </div>
115
 
116
+ <div id="overlay-msg">Please allow camera access to interact</div>
117
+ <video id="webcam-feed"></video>
118
 
119
  <script>
120
+ const CONFIG = {
121
+ particleCount: 36000,
122
+ particleSize: 0.01,
123
+ baseColor: 0x00ffff,
124
+ camWidth: 640,
125
+ camHeight: 480
 
 
 
 
 
126
  };
127
 
 
128
  const scene = new THREE.Scene();
129
+ scene.background = new THREE.Color(0x050505);
130
+ scene.fog = new THREE.FogExp2(0x050505, 0.03);
131
 
132
+ const camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 100);
133
+ camera.position.z = 6;
134
 
135
+ const renderer = new THREE.WebGLRenderer({ antialias: true });
136
  renderer.setSize(window.innerWidth, window.innerHeight);
137
  renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
138
  document.body.appendChild(renderer.domElement);
139
 
140
+ const positions = new Float32Array(CONFIG.particleCount * 3);
141
+ const targetPositions = new Float32Array(CONFIG.particleCount * 3);
142
  const geometry = new THREE.BufferGeometry();
 
143
 
144
+ for(let i=0; i<CONFIG.particleCount * 3; i++) {
145
+ positions[i] = (Math.random() - 0.5) * 10;
146
+ targetPositions[i] = positions[i];
147
  }
148
 
149
  geometry.setAttribute('position', new THREE.BufferAttribute(positions, 3));
150
 
151
  const material = new THREE.PointsMaterial({
152
+ size: CONFIG.particleSize,
153
+ color: CONFIG.baseColor,
 
 
154
  blending: THREE.AdditiveBlending,
155
+ depthWrite: false,
156
+ transparent: true,
157
+ opacity: 0.8
158
  });
159
 
160
+ const particleSystem = new THREE.Points(geometry, material);
161
+ scene.add(particleSystem);
162
+
163
+ const state = {
164
+ shape: 'galaxy',
165
+ scale: 1.0,
166
+ rotationX: 0,
167
+ rotationY: 0,
168
+ chaos: 0,
169
+ smoothedScale: 1.0,
170
+ smoothedRotX: 0,
171
+ smoothedRotY: 0,
172
+ smoothedChaos: 0
173
+ };
 
 
174
 
175
  const generators = {
 
 
 
 
 
 
 
 
 
 
 
176
  sphere: (i) => {
177
+ const u = Math.random();
178
+ const v = Math.random();
179
+ const theta = 2 * Math.PI * u;
180
+ const phi = Math.acos(2 * v - 1);
181
+ const r = 2.5;
182
+ return {
183
+ x: r * Math.sin(phi) * Math.cos(theta),
184
+ y: r * Math.sin(phi) * Math.sin(theta),
185
+ z: r * Math.cos(phi)
186
+ };
187
  },
188
+ heart: (i) => {
189
+ const t = Math.random() * Math.PI * 2;
190
+ const x = 16 * Math.pow(Math.sin(t), 3);
191
+ const y = 13 * Math.cos(t) - 5 * Math.cos(2*t) - 2 * Math.cos(3*t) - Math.cos(4*t);
192
+ return { x: x * 0.15, y: y * 0.15, z: (Math.random()-0.5) * 2 };
 
 
 
 
 
 
 
 
 
 
 
193
  },
194
  galaxy: (i) => {
195
+ const arms = 5;
196
+ const spin = i / CONFIG.particleCount * arms;
197
+ const r = (i / CONFIG.particleCount) * 5;
198
  const angle = spin * Math.PI * 2;
199
+ const drift = Math.random() * 0.5;
 
200
  return {
201
+ x: Math.cos(angle) * r + drift,
202
+ y: (Math.random() - 0.5) * (2 - r/3),
203
+ z: Math.sin(angle) * r + drift
204
  };
205
  },
206
+ dna: (i) => {
207
+ const t = (i / CONFIG.particleCount) * 10 * Math.PI;
208
+ const radius = 1.5;
209
+ const strand = i % 2 === 0 ? 1 : -1;
210
+ return {
211
+ x: Math.cos(t + strand * Math.PI) * radius,
212
+ y: (i / CONFIG.particleCount - 0.5) * 10,
213
+ z: Math.sin(t + strand * Math.PI) * radius
214
+ };
215
+ },
216
+ cube: (i) => {
217
+ const s = 3;
218
+ return {
219
+ x: (Math.random() - 0.5) * s,
220
+ y: (Math.random() - 0.5) * s,
221
+ z: (Math.random() - 0.5) * s
222
+ };
223
  }
224
  };
225
 
226
+ function morphShape() {
227
  const generator = generators[state.shape];
228
+ for(let i=0; i<CONFIG.particleCount; i++) {
229
  const pos = generator(i);
230
+ targetPositions[i*3] = pos.x;
231
+ targetPositions[i*3+1] = pos.y;
232
+ targetPositions[i*3+2] = pos.z;
233
  }
234
  }
235
+ morphShape();
236
 
237
+ document.getElementById('geometry-selector').addEventListener('change', (e) => {
238
+ state.shape = e.target.value;
239
+ morphShape();
240
+ });
241
 
242
+ document.getElementById('color-selector').addEventListener('input', (e) => {
243
+ particleSystem.material.color.set(e.target.value);
244
+ });
245
+
246
+ function handleHandResults(results) {
247
+ const led = document.getElementById('cam-led');
248
+ const txt = document.getElementById('cam-status');
249
 
250
  if (results.multiHandLandmarks && results.multiHandLandmarks.length > 0) {
251
+ led.classList.remove('off');
252
+ txt.innerText = "Hand Active";
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
253
 
254
+ const hand = results.multiHandLandmarks[0];
 
 
 
 
 
 
 
 
255
 
256
+ // 1. Rotation (Hand Position)
257
+ // Wrist is landmark 0
258
+ const wrist = hand[0];
259
+ // Normalize -0.5 to 0.5
260
+ state.rotationY = (wrist.x - 0.5) * 3;
261
+ state.rotationX = (wrist.y - 0.5) * 3;
262
+
263
+ // 2. Scale (Pinch Distance: Thumb Tip 4 vs Index Tip 8)
264
+ const thumb = hand[4];
265
+ const index = hand[8];
266
+ const pinchDist = Math.sqrt(
267
+ Math.pow(thumb.x - index.x, 2) +
268
+ Math.pow(thumb.y - index.y, 2)
269
+ );
270
 
271
+ // Map 0.02 (close) -> 0.1 (small) to 0.15 (far) -> 2.0 (big)
272
+ state.scale = THREE.MathUtils.mapLinear(pinchDist, 0.02, 0.2, 0.5, 2.5);
273
+ state.scale = THREE.MathUtils.clamp(state.scale, 0.5, 3.0);
274
+
275
+ // 3. Chaos (Fist Detection)
276
+ // Check average distance of tips to wrist
277
+ const tips = [8, 12, 16, 20];
278
+ let avgDist = 0;
279
+ tips.forEach(t => {
280
+ const tip = hand[t];
281
+ avgDist += Math.sqrt(Math.pow(tip.x - wrist.x, 2) + Math.pow(tip.y - wrist.y, 2));
282
+ });
283
+ avgDist /= 4;
284
+
285
+ // If tips are close to wrist, it's a fist
286
+ if(avgDist < 0.12) {
287
+ state.chaos = 1.0; // Explosion
288
  } else {
289
+ state.chaos = 0.0;
290
  }
291
 
292
  } else {
293
+ led.classList.add('off');
294
+ txt.innerText = "No Hand Detected";
295
+ state.rotationX = 0;
296
+ state.rotationY = 0;
297
+ state.scale = 1.0;
298
+ state.chaos = 0;
299
  }
300
  }
301
 
302
+ const hands = new Hands({locateFile: (file) => `https://cdn.jsdelivr.net/npm/@mediapipe/hands/${file}`});
 
 
 
303
  hands.setOptions({
304
+ maxNumHands: 1,
305
  modelComplexity: 1,
306
+ minDetectionConfidence: 0.6,
307
+ minTrackingConfidence: 0.6
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
308
  });
309
+ hands.onResults(handleHandResults);
310
 
311
+ const videoElem = document.getElementById('webcam-feed');
312
+ const cameraObj = new Camera(videoElem, {
313
+ onFrame: async () => { await hands.send({image: videoElem}); },
314
+ width: CONFIG.camWidth,
315
+ height: CONFIG.camHeight
316
  });
317
+ cameraObj.start();
318
 
 
319
  const clock = new THREE.Clock();
320
 
321
  function animate() {
322
  requestAnimationFrame(animate);
323
 
324
+ const dt = clock.getElapsedTime();
325
+ const positionsAttr = particleSystem.geometry.attributes.position;
326
+ const posArray = positionsAttr.array;
327
 
328
+ // Smooth Physics
329
+ state.smoothedScale += (state.scale - state.smoothedScale) * 0.1;
330
+ state.smoothedRotX += (state.rotationX - state.smoothedRotX) * 0.1;
331
+ state.smoothedRotY += (state.rotationY - state.smoothedRotY) * 0.1;
332
+ state.smoothedChaos += (state.chaos - state.smoothedChaos) * 0.1;
333
 
334
+ particleSystem.rotation.y = state.smoothedRotY + dt * 0.1;
335
+ particleSystem.rotation.x = state.smoothedRotX;
336
+
337
+ const expansion = state.smoothedScale;
338
+ const jitter = state.smoothedChaos * 0.8;
339
+
340
+ for(let i=0; i<CONFIG.particleCount; i++) {
341
  const ix = i * 3;
342
+ const iy = ix + 1;
343
+ const iz = ix + 2;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
344
 
345
+ let tx = targetPositions[ix] * expansion;
346
+ let ty = targetPositions[iy] * expansion;
347
+ let tz = targetPositions[iz] * expansion;
 
 
348
 
349
+ if(jitter > 0.01) {
350
+ tx += (Math.random() - 0.5) * jitter * 5;
351
+ ty += (Math.random() - 0.5) * jitter * 5;
352
+ tz += (Math.random() - 0.5) * jitter * 5;
353
+ }
354
+
355
+ posArray[ix] += (tx - posArray[ix]) * 0.1;
356
+ posArray[iy] += (ty - posArray[iy]) * 0.1;
357
+ posArray[iz] += (tz - posArray[iz]) * 0.1;
358
  }
359
 
360
+ positionsAttr.needsUpdate = true;
361
  renderer.render(scene, camera);
362
  }
363
 
364
  animate();
365
 
 
366
  window.addEventListener('resize', () => {
367
  camera.aspect = window.innerWidth / window.innerHeight;
368
  camera.updateProjectionMatrix();