JamesToth commited on
Commit
0dbf181
·
verified ·
1 Parent(s): 3aa7e84

Update index.html

Browse files
Files changed (1) hide show
  1. index.html +400 -325
index.html CHANGED
@@ -3,421 +3,496 @@
3
  <head>
4
  <meta charset="UTF-8">
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
- <title>Pro CPU Particle System</title>
7
  <style>
8
- body { margin: 0; overflow: hidden; background: #000; font-family: 'Courier New', Courier, monospace; }
 
 
 
9
 
 
10
  #hud {
11
- position: absolute;
12
- top: 20px;
13
- left: 20px;
14
- color: rgba(255, 255, 255, 0.8);
15
- pointer-events: none;
16
- z-index: 10;
 
 
17
  }
 
 
18
 
19
- h1 { margin: 0; font-size: 14px; text-transform: uppercase; letter-spacing: 2px; }
20
- .meta { font-size: 10px; color: #555; margin-top: 5px; }
 
21
 
22
- #gesture-alert {
23
- position: absolute;
24
- top: 50%; left: 50%;
25
- transform: translate(-50%, -50%);
26
- font-size: 2rem;
27
- color: white;
28
- opacity: 0;
29
- transition: opacity 0.5s;
30
- text-shadow: 0 0 20px rgba(0,255,255,0.8);
31
- pointer-events: none;
32
  }
 
 
 
 
33
 
34
- #loading {
35
- position: absolute;
36
- bottom: 20px; left: 20px;
37
- color: #00ff88;
38
- font-size: 12px;
 
39
  }
40
 
41
  video { display: none; }
42
  </style>
 
43
  <script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/r128/three.min.js"></script>
44
- <script src="https://cdn.jsdelivr.net/npm/@mediapipe/camera_utils/camera_utils.js" crossorigin="anonymous"></script>
45
- <script src="https://cdn.jsdelivr.net/npm/@mediapipe/control_utils/control_utils.js" crossorigin="anonymous"></script>
46
- <script src="https://cdn.jsdelivr.net/npm/@mediapipe/hands/hands.js" crossorigin="anonymous"></script>
47
  </head>
48
  <body>
49
 
50
  <div id="hud">
51
- <h1>Kinetic Particle Engine</h1>
52
- <div class="meta">CPU OPTIMIZED // PHYSICS ENABLED</div>
53
- <div class="meta" id="mode-display">MODE: GALAXY</div>
54
- <div class="meta" style="margin-top:15px; color:#888;">
55
- [GESTURES]<br>
56
- SWIPE LEFT/RIGHT: Change Shape<br>
57
- • PINCH: Scale<br>
58
- FIST: Gravity Well (Attract)
 
 
59
  </div>
60
  </div>
61
 
62
- <div id="gesture-alert">SWIPE DETECTED</div>
63
- <div id="loading">SYSTEM INITIALIZING...</div>
64
  <video id="input-video"></video>
65
 
66
  <script>
67
- /**
68
- * ------------------------------------------------------------------
69
- * 1. UTILITIES & GENERATORS
70
- * ------------------------------------------------------------------
71
- */
72
-
73
- // Generate a soft glow texture programmatically (saves loading images)
74
- function createGlowTexture() {
75
- const canvas = document.createElement('canvas');
76
- canvas.width = 32; canvas.height = 32;
77
- const ctx = canvas.getContext('2d');
78
- const grad = ctx.createRadialGradient(16, 16, 0, 16, 16, 16);
79
- grad.addColorStop(0, 'rgba(255, 255, 255, 1)');
80
- grad.addColorStop(0.2, 'rgba(255, 255, 255, 0.8)');
81
- grad.addColorStop(0.5, 'rgba(255, 255, 255, 0.2)');
82
- grad.addColorStop(1, 'rgba(0, 0, 0, 0)');
83
- ctx.fillStyle = grad;
84
- ctx.fillRect(0, 0, 32, 32);
85
- const texture = new THREE.CanvasTexture(canvas);
86
- return texture;
87
- }
88
 
89
- // Shape Formulas
90
- const Shapes = {
91
- galaxy: (i, total) => {
92
- const arms = 3;
93
- const spin = i / total * arms;
94
- const r = (i / total) * 8;
95
- const angle = spin * Math.PI * 2;
96
- return {
97
- x: Math.cos(angle) * r,
98
- y: (Math.random() - 0.5) * (10 - r), // Thicker center
99
- z: Math.sin(angle) * r
100
- };
101
- },
102
- sphere: (i, total) => {
103
- const phi = Math.acos(-1 + (2 * i) / total);
104
- const theta = Math.sqrt(total * Math.PI) * phi;
105
- const r = 5;
106
- return {
107
- x: r * Math.cos(theta) * Math.sin(phi),
108
- y: r * Math.sin(theta) * Math.sin(phi),
109
- z: r * Math.cos(phi)
110
- };
111
- },
112
- heart: (i, total) => {
113
- const t = Math.random() * Math.PI * 2;
114
- // 3D Heart approximation
115
- const x = 16 * Math.pow(Math.sin(t), 3);
116
- const y = 13 * Math.cos(t) - 5 * Math.cos(2*t) - 2 * Math.cos(3*t) - Math.cos(4*t);
117
- const scale = 0.35;
118
- return {
119
- x: x * scale,
120
- y: y * scale,
121
- z: (Math.random()-0.5) * 4
122
- };
123
  },
124
- tornado: (i, total) => {
125
- const angle = i * 0.1;
126
- const y = (i / total) * 12 - 6;
127
- const r = 1 + (y + 6) * 0.3;
128
- return {
129
- x: Math.cos(angle) * r,
130
- y: y,
131
- z: Math.sin(angle) * r
132
- };
133
- }
134
  };
135
 
136
- /**
137
- * ------------------------------------------------------------------
138
- * 2. CORE ENGINE SETUP
139
- * ------------------------------------------------------------------
140
- */
141
- const PARTICLE_COUNT = 12000; // Optimized for CPU physics
142
  const scene = new THREE.Scene();
143
- scene.fog = new THREE.FogExp2(0x000000, 0.04);
144
 
145
  const camera = new THREE.PerspectiveCamera(75, window.innerWidth/window.innerHeight, 0.1, 100);
146
- camera.position.z = 12;
147
 
148
  const renderer = new THREE.WebGLRenderer({ antialias: false, powerPreference: "high-performance" });
149
  renderer.setSize(window.innerWidth, window.innerHeight);
150
  renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
151
  document.body.appendChild(renderer.domElement);
152
 
153
- /**
154
- * ------------------------------------------------------------------
155
- * 3. PARTICLE SYSTEM (PHYSICS BASED)
156
- * ------------------------------------------------------------------
157
- */
158
- const geometry = new THREE.BufferGeometry();
159
-
160
- // Arrays for Double Buffering / Physics
161
- const posArray = new Float32Array(PARTICLE_COUNT * 3);
162
- const targetArray = new Float32Array(PARTICLE_COUNT * 3);
163
- const velocityArray = new Float32Array(PARTICLE_COUNT * 3); // Velocity for physics
164
- const colorsArray = new Float32Array(PARTICLE_COUNT * 3);
165
-
166
- const baseColor = new THREE.Color(0x00aaff);
167
-
168
- for(let i=0; i<PARTICLE_COUNT; i++) {
169
- const i3 = i*3;
170
- posArray[i3] = (Math.random()-0.5)*20;
171
- posArray[i3+1] = (Math.random()-0.5)*20;
172
- posArray[i3+2] = (Math.random()-0.5)*20;
173
-
174
- targetArray[i3] = posArray[i3];
175
-
176
- colorsArray[i3] = baseColor.r;
177
- colorsArray[i3+1] = baseColor.g;
178
- colorsArray[i3+2] = baseColor.b;
179
  }
180
 
181
- geometry.setAttribute('position', new THREE.BufferAttribute(posArray, 3));
182
- geometry.setAttribute('color', new THREE.BufferAttribute(colorsArray, 3));
183
 
184
- const material = new THREE.PointsMaterial({
185
- size: 0.15,
186
- map: createGlowTexture(), // Use generated texture
187
  vertexColors: true,
188
  transparent: true,
189
- opacity: 0.9,
190
  blending: THREE.AdditiveBlending,
191
  depthWrite: false
192
  });
193
 
194
- const particles = new THREE.Points(geometry, material);
195
  scene.add(particles);
196
 
197
- /**
198
- * ------------------------------------------------------------------
199
- * 4. LOGIC & STATE
200
- * ------------------------------------------------------------------
201
- */
202
- const AppState = {
203
- currentShape: 'galaxy',
204
- shapeKeys: Object.keys(Shapes),
205
- shapeIndex: 0,
206
- hand: { x: 0, y: 0, z: 0, active: false, pinch: 0, fist: false },
207
- handVelocity: { x: 0, y: 0 }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
208
  };
209
 
210
- function switchShape(direction) {
211
- if(direction === 'next') {
212
- AppState.shapeIndex = (AppState.shapeIndex + 1) % AppState.shapeKeys.length;
213
- } else {
214
- AppState.shapeIndex = (AppState.shapeIndex - 1 + AppState.shapeKeys.length) % AppState.shapeKeys.length;
215
- }
216
 
217
- AppState.currentShape = AppState.shapeKeys[AppState.shapeIndex];
218
- document.getElementById('mode-display').innerText = `MODE: ${AppState.currentShape.toUpperCase()}`;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
219
 
220
- // Show UI feedback
221
- const alert = document.getElementById('gesture-alert');
222
- alert.innerText = `${AppState.currentShape.toUpperCase()}`;
223
- alert.style.opacity = 1;
224
- setTimeout(() => alert.style.opacity = 0, 1000);
225
-
226
- // Recalculate targets
227
- const func = Shapes[AppState.currentShape];
228
- for(let i=0; i<PARTICLE_COUNT; i++) {
229
- const p = func(i, PARTICLE_COUNT);
230
- targetArray[i*3] = p.x;
231
- targetArray[i*3+1] = p.y;
232
- targetArray[i*3+2] = p.z;
233
  }
234
- }
235
 
236
- // Init first shape
237
- switchShape('next');
238
 
239
- /**
240
- * ------------------------------------------------------------------
241
- * 5. COMPUTER VISION (THROTTLED)
242
- * ------------------------------------------------------------------
243
- */
244
- const videoElement = document.getElementById('input-video');
245
  let lastHandX = 0;
246
-
247
- // We will only process MediaPipe every 2 frames to save CPU for rendering
248
- let frameSkipCounter = 0;
249
 
250
  const hands = new Hands({locateFile: (file) => `https://cdn.jsdelivr.net/npm/@mediapipe/hands/${file}`});
251
- hands.setOptions({
252
- maxNumHands: 1,
253
- modelComplexity: 0, // Lite model for speed
254
- minDetectionConfidence: 0.5,
255
- minTrackingConfidence: 0.5
256
- });
257
-
258
  hands.onResults(results => {
259
- const loading = document.getElementById('loading');
260
- if(loading) loading.style.display = 'none';
261
-
262
- if (results.multiHandLandmarks && results.multiHandLandmarks.length > 0) {
263
- const landmarks = results.multiHandLandmarks[0];
264
- AppState.hand.active = true;
 
 
 
 
 
 
 
265
 
266
- // 1. Position Mapping (Normalized -1 to 1)
267
- // Invert X because webcam is mirrored
268
- const hx = -(landmarks[9].x - 0.5) * 10;
269
- const hy = -(landmarks[9].y - 0.5) * 8;
270
 
271
- // Calculate Velocity for gestures
272
- AppState.handVelocity.x = hx - lastHandX;
273
- lastHandX = hx;
274
-
275
- // Smooth hand position
276
- AppState.hand.x += (hx - AppState.hand.x) * 0.2;
277
- AppState.hand.y += (hy - AppState.hand.y) * 0.2;
278
-
279
- // 2. Pinch Detection (Thumb Tip vs Index Tip)
280
- const pinchDist = Math.hypot(landmarks[4].x - landmarks[8].x, landmarks[4].y - landmarks[8].y);
281
- AppState.hand.pinch = pinchDist; // <0.05 is pinch
282
-
283
- // 3. Fist Detection (Tip to Wrist)
284
- const wrist = landmarks[0];
285
- const midTip = landmarks[12];
286
- const dist = Math.hypot(wrist.x - midTip.x, wrist.y - midTip.y);
287
- AppState.hand.fist = dist < 0.2; // Threshold for fist
288
-
289
- // 4. Swipe Detection
290
- if (Math.abs(AppState.handVelocity.x) > 0.6) {
291
- // Debounce swipe
292
- if (!AppState.swiping) {
293
- AppState.swiping = true;
294
- switchShape(AppState.handVelocity.x > 0 ? 'next' : 'prev');
295
- setTimeout(() => AppState.swiping = false, 500);
296
- }
297
- }
298
 
299
  } else {
300
- AppState.hand.active = false;
 
 
 
301
  }
302
  });
303
 
304
- const cameraUtils = new Camera(videoElement, {
305
  onFrame: async () => {
306
- // Throttling Logic: Run AI every 2nd frame only
307
- frameSkipCounter++;
308
- if(frameSkipCounter % 2 === 0) {
309
- await hands.send({image: videoElement});
310
- }
311
  },
312
- width: 640,
313
- height: 480
314
  });
315
- cameraUtils.start();
316
 
317
- /**
318
- * ------------------------------------------------------------------
319
- * 6. PHYSICS LOOP
320
- * ------------------------------------------------------------------
321
- */
322
  const clock = new THREE.Clock();
323
-
324
  function animate() {
325
  requestAnimationFrame(animate);
326
-
 
327
  const time = clock.getElapsedTime();
328
- const positions = particles.geometry.attributes.position.array;
329
- const colors = particles.geometry.attributes.color.array;
330
-
331
- // Interaction Variables
332
- let globalScale = 1.0;
333
- if(AppState.hand.active) {
334
- // Map pinch to scale: 0.02->0.5, 0.2->1.5
335
- globalScale = THREE.MathUtils.mapLinear(AppState.hand.pinch, 0.02, 0.2, 0.5, 1.8);
 
 
 
 
 
 
 
 
 
 
336
  }
337
 
338
- // Particle Loop
339
- for (let i = 0; i < PARTICLE_COUNT; i++) {
340
- const i3 = i * 3;
341
-
342
- // 1. Get Target
343
- let tx = targetArray[i3] * globalScale;
344
- let ty = targetArray[i3+1] * globalScale;
345
- let tz = targetArray[i3+2] * globalScale;
346
-
347
- // 2. Hand Repulsion / Attraction Physics
348
- if (AppState.hand.active) {
349
- const dx = positions[i3] - AppState.hand.x;
350
- const dy = positions[i3+1] - AppState.hand.y;
351
- const distSq = dx*dx + dy*dy;
352
-
353
- // Repulsion Radius
354
- if (distSq < 4) {
355
- const force = (4 - distSq) * 0.1;
356
-
357
- if(AppState.hand.fist) {
358
- // Attraction (Black Hole)
359
- tx -= dx * force * 5;
360
- ty -= dy * force * 5;
361
- } else {
362
- // Repulsion (Push away)
363
- tx += dx * force;
364
- ty += dy * force;
365
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
366
  }
367
  }
368
 
369
- // 3. Spring Physics Integration
370
- // Acceleration = (Target - Current) * SpringStrength
371
- const ax = (tx - positions[i3]) * 0.05; // Spring strength
372
- const ay = (ty - positions[i3+1]) * 0.05;
373
- const az = (tz - positions[i3+2]) * 0.05;
374
-
375
- // Update Velocity (Inertia)
376
- velocityArray[i3] += ax;
377
- velocityArray[i3+1] += ay;
378
- velocityArray[i3+2] += az;
379
-
380
- // Friction (Damping)
381
- velocityArray[i3] *= 0.92;
382
- velocityArray[i3+1] *= 0.92;
383
- velocityArray[i3+2] *= 0.92;
384
-
385
- // Apply Velocity
386
- positions[i3] += velocityArray[i3];
387
- positions[i3+1] += velocityArray[i3+1];
388
- positions[i3+2] += velocityArray[i3+2];
389
-
390
- // 4. Dynamic Coloring based on Speed
391
- const speed = Math.abs(velocityArray[i3]) + Math.abs(velocityArray[i3+1]);
392
- if (speed > 0.1) {
393
- // Moving fast -> White/Cyan
394
- colors[i3] = 0.8; colors[i3+1] = 1.0; colors[i3+2] = 1.0;
395
  } else {
396
- // Resting -> Base Color
397
- colors[i3] += (baseColor.r - colors[i3]) * 0.1;
398
- colors[i3+1] += (baseColor.g - colors[i3+1]) * 0.1;
399
- colors[i3+2] += (baseColor.b - colors[i3+2]) * 0.1;
400
  }
401
  }
402
 
403
- // Global idle rotation
404
- if(!AppState.hand.active) {
405
- particles.rotation.y = time * 0.1;
 
406
  }
407
 
408
- particles.geometry.attributes.position.needsUpdate = true;
409
- particles.geometry.attributes.color.needsUpdate = true;
 
 
 
410
  renderer.render(scene, camera);
 
 
 
411
  }
412
-
413
  animate();
414
 
415
- // Resize Handler
416
- window.addEventListener('resize', () => {
417
- camera.aspect = window.innerWidth / window.innerHeight;
418
  camera.updateProjectionMatrix();
419
  renderer.setSize(window.innerWidth, window.innerHeight);
420
- });
 
421
  </script>
422
  </body>
423
  </html>
 
3
  <head>
4
  <meta charset="UTF-8">
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>Omni-Hand Particle Engine</title>
7
  <style>
8
+ body {
9
+ margin: 0; overflow: hidden; background-color: #000;
10
+ font-family: 'Segoe UI', monospace; user-select: none;
11
+ }
12
 
13
+ /* HUD UI */
14
  #hud {
15
+ position: absolute; top: 20px; left: 20px;
16
+ width: 300px; padding: 20px;
17
+ background: rgba(10,12,16, 0.85);
18
+ backdrop-filter: blur(12px);
19
+ border: 1px solid rgba(255,255,255,0.15);
20
+ border-left: 4px solid #00ffcc;
21
+ border-radius: 8px;
22
+ color: #fff; pointer-events: none; z-index: 10;
23
  }
24
+
25
+ h1 { margin: 0 0 10px 0; font-size: 16px; letter-spacing: 1px; color: #00ffcc; text-transform: uppercase; }
26
 
27
+ .row { display: flex; justify-content: space-between; font-size: 11px; margin-bottom: 5px; color: #aaa; }
28
+ .val { font-family: 'Courier New', monospace; color: #fff; font-weight: bold; }
29
+ .highlight { color: #00ffcc; }
30
 
31
+ #detected-gesture {
32
+ margin-top: 15px; padding-top: 15px; border-top: 1px solid rgba(255,255,255,0.1);
33
+ font-size: 18px; text-align: center; color: #fff; font-weight: 300;
 
 
 
 
 
 
 
34
  }
35
+ #detected-gesture span { display: block; font-size: 10px; color: #666; margin-top: 4px; letter-spacing: 2px; text-transform: uppercase;}
36
+
37
+ #loading { position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%); color: #00ffcc; font-size: 14px; animation: blink 1s infinite; }
38
+ @keyframes blink { 50% { opacity: 0.5; } }
39
 
40
+ /* Toast Notification for Shape/Color Changes */
41
+ #toast {
42
+ position: absolute; bottom: 100px; left: 0; width: 100%;
43
+ text-align: center; font-size: 24px; color: white;
44
+ text-shadow: 0 0 20px #00ffcc; opacity: 0; transition: opacity 0.5s;
45
+ pointer-events: none;
46
  }
47
 
48
  video { display: none; }
49
  </style>
50
+
51
  <script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/r128/three.min.js"></script>
52
+ <script src="https://cdn.jsdelivr.net/npm/@mediapipe/camera_utils/camera_utils.js"></script>
53
+ <script src="https://cdn.jsdelivr.net/npm/@mediapipe/control_utils/control_utils.js"></script>
54
+ <script src="https://cdn.jsdelivr.net/npm/@mediapipe/hands/hands.js"></script>
55
  </head>
56
  <body>
57
 
58
  <div id="hud">
59
+ <h1>OMNI-CONTROL ENGINE</h1>
60
+
61
+ <div class="row"><span>SHAPE</span><span id="ui-shape" class="val">GALAXY</span></div>
62
+ <div class="row"><span>COLOR</span><span id="ui-color" class="val">CYBER</span></div>
63
+ <div class="row"><span>SIZE</span><span id="ui-size" class="val">1.0x</span></div>
64
+ <div class="row"><span>FPS</span><span id="ui-fps" class="val">60</span></div>
65
+
66
+ <div id="detected-gesture">
67
+ WAITING FOR HAND...
68
+ <span>SYSTEM STATUS</span>
69
  </div>
70
  </div>
71
 
72
+ <div id="toast">MODE CHANGE</div>
73
+ <div id="loading">INITIALIZING NEURAL NET...</div>
74
  <video id="input-video"></video>
75
 
76
  <script>
77
+ // ==========================================
78
+ // 1. SYSTEM CONFIG
79
+ // ==========================================
80
+ const CONFIG = {
81
+ count: 15000,
82
+ camWidth: 640,
83
+ camHeight: 480,
84
+ throttleAI: 2 // Save CPU
85
+ };
 
 
 
 
 
 
 
 
 
 
 
 
86
 
87
+ const COLORS = [
88
+ { name: 'CYBER', base: 0x00aaff, high: 0xffffff },
89
+ { name: 'INFERNO', base: 0xff3300, high: 0xffaa00 },
90
+ { name: 'POISON', base: 0x8800ff, high: 0x00ff00 },
91
+ { name: 'MATRIX', base: 0x003300, high: 0x00ff00 },
92
+ { name: 'COTTON', base: 0xffaabb, high: 0xaabbff },
93
+ ];
94
+
95
+ const State = {
96
+ active: false,
97
+ gesture: 'NONE',
98
+ hand: { x: 0, y: 0, z: 0, angle: 0 },
99
+ params: {
100
+ shapeIdx: 0,
101
+ colorIdx: 0,
102
+ size: 0.15,
103
+ autoSpin: true,
104
+ freeze: false,
105
+ turbulence: 0,
106
+ trail: false,
107
+ reset: false
 
 
 
 
 
 
 
 
 
 
 
 
 
108
  },
109
+ cooldown: 0
 
 
 
 
 
 
 
 
 
110
  };
111
 
112
+ // ==========================================
113
+ // 2. VISUAL ENGINE (THREE.JS)
114
+ // ==========================================
 
 
 
115
  const scene = new THREE.Scene();
116
+ scene.fog = new THREE.FogExp2(0x000000, 0.03);
117
 
118
  const camera = new THREE.PerspectiveCamera(75, window.innerWidth/window.innerHeight, 0.1, 100);
119
+ camera.position.z = 10;
120
 
121
  const renderer = new THREE.WebGLRenderer({ antialias: false, powerPreference: "high-performance" });
122
  renderer.setSize(window.innerWidth, window.innerHeight);
123
  renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
124
  document.body.appendChild(renderer.domElement);
125
 
126
+ // Texture Gen
127
+ const canvas = document.createElement('canvas');
128
+ canvas.width = 32; canvas.height = 32;
129
+ const ctx = canvas.getContext('2d');
130
+ const grad = ctx.createRadialGradient(16,16,0,16,16,16);
131
+ grad.addColorStop(0, 'white');
132
+ grad.addColorStop(1, 'rgba(255,255,255,0)');
133
+ ctx.fillStyle = grad; ctx.fillRect(0,0,32,32);
134
+ const texture = new THREE.CanvasTexture(canvas);
135
+
136
+ // Particles
137
+ const geo = new THREE.BufferGeometry();
138
+ const pos = new Float32Array(CONFIG.count * 3);
139
+ const tar = new Float32Array(CONFIG.count * 3);
140
+ const vel = new Float32Array(CONFIG.count * 3);
141
+ const col = new Float32Array(CONFIG.count * 3);
142
+ const extra = new Float32Array(CONFIG.count); // For trails/size variation
143
+
144
+ for(let i=0; i<CONFIG.count*3; i++) {
145
+ pos[i] = (Math.random()-0.5)*50;
146
+ tar[i] = pos[i];
147
+ vel[i] = 0;
148
+ col[i] = 1;
 
 
 
149
  }
150
 
151
+ geo.setAttribute('position', new THREE.BufferAttribute(pos, 3));
152
+ geo.setAttribute('color', new THREE.BufferAttribute(col, 3));
153
 
154
+ const mat = new THREE.PointsMaterial({
155
+ size: State.params.size,
156
+ map: texture,
157
  vertexColors: true,
158
  transparent: true,
159
+ opacity: 0.8,
160
  blending: THREE.AdditiveBlending,
161
  depthWrite: false
162
  });
163
 
164
+ const particles = new THREE.Points(geo, mat);
165
  scene.add(particles);
166
 
167
+ // ==========================================
168
+ // 3. SHAPE LIBRARY
169
+ // ==========================================
170
+ const Shapes = [
171
+ { name: 'GALAXY', func: (i) => {
172
+ const r = (i/CONFIG.count)*8;
173
+ const a = (i/CONFIG.count)*Math.PI*8;
174
+ return { x: Math.cos(a)*r, y: (Math.random()-0.5), z: Math.sin(a)*r };
175
+ }},
176
+ { name: 'SPHERE', func: (i) => {
177
+ const phi = Math.acos(-1 + (2*i)/CONFIG.count);
178
+ const theta = Math.sqrt(CONFIG.count * Math.PI) * phi;
179
+ return { x: 5*Math.cos(theta)*Math.sin(phi), y: 5*Math.sin(theta)*Math.sin(phi), z: 5*Math.cos(phi) };
180
+ }},
181
+ { name: 'HEART', func: (i) => {
182
+ const t = (i/CONFIG.count)*Math.PI*2;
183
+ const x = 16*Math.pow(Math.sin(t),3);
184
+ const y = 13*Math.cos(t)-5*Math.cos(2*t)-2*Math.cos(3*t)-Math.cos(4*t);
185
+ return { x: x*0.3, y: y*0.3, z: (Math.random()-0.5)*2 };
186
+ }},
187
+ { name: 'CUBE', func: (i) => {
188
+ const s = 3; // size
189
+ const ax = i % 3; // 0=x, 1=y, 2=z plane logic simplified
190
+ return { x: (Math.random()-0.5)*10, y: (Math.random()-0.5)*10, z: (Math.random()-0.5)*10 };
191
+ }},
192
+ { name: 'RING', func: (i) => {
193
+ const a = (i/CONFIG.count)*Math.PI*2;
194
+ return { x: Math.cos(a)*7, y: (Math.random()-0.5)*2, z: Math.sin(a)*7 };
195
+ }}
196
+ ];
197
+
198
+ function setShape(idx) {
199
+ const s = Shapes[idx];
200
+ document.getElementById('ui-shape').innerText = s.name;
201
+ showToast(s.name);
202
+ for(let i=0; i<CONFIG.count; i++) {
203
+ const p = s.func(i);
204
+ tar[i*3] = p.x; tar[i*3+1] = p.y; tar[i*3+2] = p.z;
205
+ }
206
+ }
207
+ setShape(0);
208
+
209
+ function setColor(idx) {
210
+ document.getElementById('ui-color').innerText = COLORS[idx].name;
211
+ showToast("COLOR: " + COLORS[idx].name);
212
+ }
213
+
214
+ function showToast(msg) {
215
+ const t = document.getElementById('toast');
216
+ t.innerText = msg; t.style.opacity = 1;
217
+ setTimeout(() => t.style.opacity = 0, 1000);
218
+ }
219
+
220
+ // ==========================================
221
+ // 4. GESTURE RECOGNITION LOGIC
222
+ // ==========================================
223
+ const Gesture = {
224
+ isFist: (lm) => {
225
+ // Tips (8,12,16,20) closer to wrist(0) than PIP joints (6,10,14,18)
226
+ return lm[8].y > lm[6].y && lm[12].y > lm[10].y && lm[16].y > lm[14].y && lm[20].y > lm[18].y;
227
+ },
228
+ isIndex: (lm) => lm[8].y < lm[6].y && lm[12].y > lm[10].y && lm[16].y > lm[14].y,
229
+ isPinky: (lm) => lm[20].y < lm[18].y && lm[16].y > lm[14].y && lm[8].y > lm[6].y,
230
+ isOkay: (lm) => {
231
+ const dist = Math.hypot(lm[4].x - lm[8].x, lm[4].y - lm[8].y);
232
+ return dist < 0.05 && lm[12].y < lm[10].y; // Tips touch + Middle up
233
+ },
234
+ isVictory: (lm) => lm[8].y < lm[6].y && lm[12].y < lm[10].y && lm[16].y > lm[14].y,
235
+ isThree: (lm) => lm[8].y < lm[6].y && lm[12].y < lm[10].y && lm[16].y < lm[14].y && lm[20].y > lm[18].y,
236
+ isFour: (lm) => lm[8].y < lm[6].y && lm[12].y < lm[10].y && lm[16].y < lm[14].y && lm[20].y < lm[18].y && lm[4].x < lm[3].x, // Thumb tucked
237
+ isOpen: (lm) => lm[8].y < lm[6].y && lm[12].y < lm[10].y && lm[16].y < lm[14].y && lm[20].y < lm[18].y,
238
+ isRock: (lm) => lm[8].y < lm[6].y && lm[20].y < lm[18].y && lm[12].y > lm[10].y && lm[16].y > lm[14].y,
239
+ isShaka: (lm) => lm[4].x > lm[3].x && lm[20].y < lm[18].y && lm[8].y > lm[6].y, // Thumb out, pinky up, others down
240
  };
241
 
242
+ function processGesture(lm, velX) {
243
+ let g = "UNKNOWN";
 
 
 
 
244
 
245
+ // 1. Swipe Check (High Priority)
246
+ if(Math.abs(velX) > 0.8 && State.cooldown <= 0) {
247
+ State.params.shapeIdx = (State.params.shapeIdx + 1) % Shapes.length;
248
+ setShape(State.params.shapeIdx);
249
+ State.cooldown = 20;
250
+ return "SWIPE CHANGE";
251
+ }
252
+
253
+ // 2. Pinch Check (Scaling)
254
+ const pinchDist = Math.hypot(lm[4].x - lm[8].x, lm[4].y - lm[8].y);
255
+ if(pinchDist < 0.1 && !Gesture.isFist(lm)) {
256
+ // Map Pinch Y position to scale
257
+ // Higher hand = bigger scale
258
+ const newSize = THREE.MathUtils.mapLinear(lm[4].y, 0, 1, 2.0, 0.5);
259
+ State.params.scaleTarget = newSize; // We'll lerp to this
260
+ g = "PINCH ZOOM";
261
+ }
262
+
263
+ // 3. Static Poses
264
+ if (Gesture.isFist(lm)) g = "FIST (GRAVITY)";
265
+ else if (Gesture.isOkay(lm)) g = "OKAY (FREEZE)";
266
+ else if (Gesture.isRock(lm)) g = "ROCK (TRAILS)";
267
+ else if (Gesture.isVictory(lm)) g = "VICTORY (CHAOS)";
268
+ else if (Gesture.isShaka(lm)) g = "SHAKA (RESET)";
269
+ else if (Gesture.isThree(lm)) {
270
+ if(State.cooldown <= 0) {
271
+ State.params.colorIdx = (State.params.colorIdx + 1) % COLORS.length;
272
+ setColor(State.params.colorIdx);
273
+ State.cooldown = 30;
274
+ }
275
+ g = "3-FINGER (COLOR)";
276
+ }
277
+ else if (Gesture.isFour(lm)) {
278
+ if(State.cooldown <= 0) {
279
+ State.params.autoSpin = !State.params.autoSpin;
280
+ showToast(State.params.autoSpin ? "SPIN: ON" : "SPIN: OFF");
281
+ State.cooldown = 30;
282
+ }
283
+ g = "4-FINGER (SPIN)";
284
+ }
285
+ else if (Gesture.isPinky(lm)) g = "PINKY (VORTEX)";
286
+ else if (Gesture.isIndex(lm)) g = "INDEX (CURSOR)";
287
+ else if (Gesture.isOpen(lm)) g = "PALM (SHIELD)";
288
 
289
+ // 4. Thumb Orientation (Size)
290
+ // Check relative y of thumb tip vs IP joint
291
+ if(g === "UNKNOWN" || g === "FIST (GRAVITY)") {
292
+ const dy = lm[4].y - lm[3].y;
293
+ if(dy < -0.05) { // Thumb pointing up
294
+ State.params.size = Math.min(State.params.size + 0.005, 0.4);
295
+ g = "THUMB UP (GROW)";
296
+ } else if (dy > 0.05) {
297
+ State.params.size = Math.max(State.params.size - 0.005, 0.02);
298
+ g = "THUMB DOWN (SHRINK)";
299
+ }
 
 
300
  }
 
301
 
302
+ return g;
303
+ }
304
 
305
+ // ==========================================
306
+ // 5. MEDIA PIPE LOOP
307
+ // ==========================================
308
+ const videoElem = document.getElementById('input-video');
309
+ const hudGesture = document.getElementById('detected-gesture');
 
310
  let lastHandX = 0;
311
+ let frames = 0;
 
 
312
 
313
  const hands = new Hands({locateFile: (file) => `https://cdn.jsdelivr.net/npm/@mediapipe/hands/${file}`});
314
+ hands.setOptions({maxNumHands: 1, minDetectionConfidence: 0.6, minTrackingConfidence: 0.6});
315
+
 
 
 
 
 
316
  hands.onResults(results => {
317
+ document.getElementById('loading').style.display = 'none';
318
+
319
+ if(results.multiHandLandmarks.length > 0) {
320
+ State.active = true;
321
+ const lm = results.multiHandLandmarks[0];
322
+
323
+ // Map coordinates (Mirror X)
324
+ const x = (1 - lm[9].x) * 20 - 10;
325
+ const y = -(lm[9].y - 0.5) * 15;
326
+ const z = (lm[9].z) * 20; // Depth approx
327
+
328
+ const velX = x - lastHandX;
329
+ lastHandX = x;
330
 
331
+ // Detect Gesture
332
+ const g = processGesture(lm, velX);
333
+ State.gesture = g;
 
334
 
335
+ // Update HUD
336
+ hudGesture.innerHTML = `${g}<span>DETECTED</span>`;
337
+ if(g !== "NONE") hudGesture.style.borderColor = "#00ffcc";
338
+
339
+ // Update State Hands
340
+ State.hand.x += (x - State.hand.x) * 0.3;
341
+ State.hand.y += (y - State.hand.y) * 0.3;
342
+ State.hand.z = z;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
343
 
344
  } else {
345
+ State.active = false;
346
+ State.gesture = "SEARCHING";
347
+ hudGesture.innerHTML = `SEARCHING<span>SHOW HAND</span>`;
348
+ hudGesture.style.borderColor = "#555";
349
  }
350
  });
351
 
352
+ const cam = new Camera(videoElem, {
353
  onFrame: async () => {
354
+ frames++;
355
+ if(frames % CONFIG.throttleAI === 0) await hands.send({image: videoElem});
 
 
 
356
  },
357
+ width: CONFIG.camWidth, height: CONFIG.camHeight
 
358
  });
359
+ cam.start();
360
 
361
+ // ==========================================
362
+ // 6. PHYSICS ENGINE
363
+ // ==========================================
 
 
364
  const clock = new THREE.Clock();
365
+
366
  function animate() {
367
  requestAnimationFrame(animate);
368
+
369
+ const dt = clock.getDelta();
370
  const time = clock.getElapsedTime();
371
+
372
+ if(State.cooldown > 0) State.cooldown--;
373
+
374
+ // Update Size from gesture
375
+ mat.size = State.params.size;
376
+ document.getElementById('ui-size').innerText = State.params.size.toFixed(2) + "x";
377
+
378
+ // Global Params
379
+ let damping = 0.92;
380
+ let scale = 1.0;
381
+
382
+ // --- GESTURE APPLICATION ---
383
+ if(State.active) {
384
+ if(State.gesture.includes("PINCH")) {
385
+ // Lerp global scale
386
+ // Note: we'd need a global scaler, applying it to targets for now
387
+ }
388
+ if(State.gesture.includes("FREEZE")) damping = 0.5; // High friction
389
  }
390
 
391
+ const cTheme = COLORS[State.params.colorIdx];
392
+ const rBase = (cTheme.base >> 16 & 255) / 255;
393
+ const gBase = (cTheme.base >> 8 & 255) / 255;
394
+ const bBase = (cTheme.base & 255) / 255;
395
+
396
+ for(let i=0; i<CONFIG.count; i++) {
397
+ const i3 = i*3;
398
+ let tx = tar[i3]; let ty = tar[i3+1]; let tz = tar[i3+2];
399
+
400
+ // Interaction Physics
401
+ if(State.active) {
402
+ const dx = pos[i3] - State.hand.x;
403
+ const dy = pos[i3+1] - State.hand.y;
404
+ const dz = pos[i3+2] - State.hand.z * 2; // Depth
405
+ const distSq = dx*dx + dy*dy + dz*dz;
406
+ const dist = Math.sqrt(distSq);
407
+
408
+ // 1. SHIELD (Repulsion)
409
+ if(State.gesture.includes("PALM") && dist < 5) {
410
+ const f = (5 - dist) * 2.0;
411
+ tx += (dx/dist) * f;
412
+ ty += (dy/dist) * f;
413
+ tz += (dz/dist) * f;
414
+ }
415
+
416
+ // 2. GRAVITY (Fist/Index)
417
+ if(State.gesture.includes("FIST") && dist < 12) {
418
+ tx -= dx * 0.8; // Strong pull
419
+ ty -= dy * 0.8;
420
+ tz -= dz * 0.8;
421
+ } else if(State.gesture.includes("INDEX") && dist < 8) {
422
+ tx -= dx * 0.2; // Weak pull
423
+ ty -= dy * 0.2;
424
+ }
425
+
426
+ // 3. VORTEX (Pinky)
427
+ if(State.gesture.includes("PINKY")) {
428
+ // Tangent force
429
+ tx += -dy * 0.05;
430
+ ty += dx * 0.05;
431
+ }
432
+
433
+ // 4. TURBULENCE (Victory)
434
+ if(State.gesture.includes("VICTORY")) {
435
+ tx += (Math.random()-0.5)*2;
436
+ ty += (Math.random()-0.5)*2;
437
+ }
438
+
439
+ // 5. RESET (Shaka)
440
+ if(State.gesture.includes("SHAKA")) {
441
+ tx = (Math.random()-0.5)*50;
442
+ ty = (Math.random()-0.5)*50;
443
  }
444
  }
445
 
446
+ // Spring Physics
447
+ if(!State.gesture.includes("FREEZE")) {
448
+ const ax = (tx - pos[i3]) * 0.05;
449
+ const ay = (ty - pos[i3+1]) * 0.05;
450
+ const az = (tz - pos[i3+2]) * 0.05;
451
+
452
+ vel[i3] += ax; vel[i3+1] += ay; vel[i3+2] += az;
453
+ vel[i3] *= damping; vel[i3+1] *= damping; vel[i3+2] *= damping;
454
+
455
+ pos[i3] += vel[i3]; pos[i3+1] += vel[i3+1]; pos[i3+2] += vel[i3+2];
456
+ }
457
+
458
+ // Coloring (Speed based)
459
+ const speed = Math.abs(vel[i3]) + Math.abs(vel[i3+1]);
460
+ const hot = Math.min(speed*2, 1);
461
+
462
+ // Trail logic check
463
+ if(State.gesture.includes("TRAILS")) {
464
+ col[i3] = 1; col[i3+1] = 1; col[i3+2] = 1; // All white
 
 
 
 
 
 
 
465
  } else {
466
+ col[i3] = rBase + hot;
467
+ col[i3+1] = gBase + hot;
468
+ col[i3+2] = bBase + hot;
 
469
  }
470
  }
471
 
472
+ // Auto Rotation
473
+ if(State.params.autoSpin && !State.gesture.includes("PINKY")) {
474
+ particles.rotation.y = time * 0.15;
475
+ particles.rotation.z = Math.sin(time*0.2) * 0.1;
476
  }
477
 
478
+ // Camera Z-Roll for fun
479
+ camera.rotation.z = (State.hand.x * -0.02);
480
+
481
+ geo.attributes.position.needsUpdate = true;
482
+ geo.attributes.color.needsUpdate = true;
483
  renderer.render(scene, camera);
484
+
485
+ // FPS Counter (approx)
486
+ document.getElementById('ui-fps').innerText = Math.round(1/dt);
487
  }
 
488
  animate();
489
 
490
+ window.onresize = () => {
491
+ camera.aspect = window.innerWidth/window.innerHeight;
 
492
  camera.updateProjectionMatrix();
493
  renderer.setSize(window.innerWidth, window.innerHeight);
494
+ };
495
+
496
  </script>
497
  </body>
498
  </html>