Spaces:
Running
Running
| <html lang="en"> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
| <title>Pro CPU Particle System</title> | |
| <style> | |
| body { margin: 0; overflow: hidden; background: #000; font-family: 'Courier New', Courier, monospace; } | |
| #hud { | |
| position: absolute; | |
| top: 20px; | |
| left: 20px; | |
| color: rgba(255, 255, 255, 0.8); | |
| pointer-events: none; | |
| z-index: 10; | |
| } | |
| h1 { margin: 0; font-size: 14px; text-transform: uppercase; letter-spacing: 2px; } | |
| .meta { font-size: 10px; color: #555; margin-top: 5px; } | |
| #gesture-alert { | |
| position: absolute; | |
| top: 50%; left: 50%; | |
| transform: translate(-50%, -50%); | |
| font-size: 2rem; | |
| color: white; | |
| opacity: 0; | |
| transition: opacity 0.5s; | |
| text-shadow: 0 0 20px rgba(0,255,255,0.8); | |
| pointer-events: none; | |
| } | |
| #loading { | |
| position: absolute; | |
| bottom: 20px; left: 20px; | |
| color: #00ff88; | |
| font-size: 12px; | |
| } | |
| video { display: none; } | |
| </style> | |
| <script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/r128/three.min.js"></script> | |
| <script src="https://cdn.jsdelivr.net/npm/@mediapipe/camera_utils/camera_utils.js" crossorigin="anonymous"></script> | |
| <script src="https://cdn.jsdelivr.net/npm/@mediapipe/control_utils/control_utils.js" crossorigin="anonymous"></script> | |
| <script src="https://cdn.jsdelivr.net/npm/@mediapipe/hands/hands.js" crossorigin="anonymous"></script> | |
| </head> | |
| <body> | |
| <div id="hud"> | |
| <h1>Kinetic Particle Engine</h1> | |
| <div class="meta">CPU OPTIMIZED // PHYSICS ENABLED</div> | |
| <div class="meta" id="mode-display">MODE: GALAXY</div> | |
| <div class="meta" style="margin-top:15px; color:#888;"> | |
| [GESTURES]<br> | |
| • SWIPE LEFT/RIGHT: Change Shape<br> | |
| • PINCH: Scale<br> | |
| • FIST: Gravity Well (Attract) | |
| </div> | |
| </div> | |
| <div id="gesture-alert">SWIPE DETECTED</div> | |
| <div id="loading">SYSTEM INITIALIZING...</div> | |
| <video id="input-video"></video> | |
| <script> | |
| /** | |
| * ------------------------------------------------------------------ | |
| * 1. UTILITIES & GENERATORS | |
| * ------------------------------------------------------------------ | |
| */ | |
| // Generate a soft glow texture programmatically (saves loading images) | |
| function createGlowTexture() { | |
| const canvas = document.createElement('canvas'); | |
| canvas.width = 32; canvas.height = 32; | |
| const ctx = canvas.getContext('2d'); | |
| const grad = ctx.createRadialGradient(16, 16, 0, 16, 16, 16); | |
| grad.addColorStop(0, 'rgba(255, 255, 255, 1)'); | |
| grad.addColorStop(0.2, 'rgba(255, 255, 255, 0.8)'); | |
| grad.addColorStop(0.5, 'rgba(255, 255, 255, 0.2)'); | |
| grad.addColorStop(1, 'rgba(0, 0, 0, 0)'); | |
| ctx.fillStyle = grad; | |
| ctx.fillRect(0, 0, 32, 32); | |
| const texture = new THREE.CanvasTexture(canvas); | |
| return texture; | |
| } | |
| // Shape Formulas | |
| const Shapes = { | |
| galaxy: (i, total) => { | |
| const arms = 3; | |
| const spin = i / total * arms; | |
| const r = (i / total) * 8; | |
| const angle = spin * Math.PI * 2; | |
| return { | |
| x: Math.cos(angle) * r, | |
| y: (Math.random() - 0.5) * (10 - r), // Thicker center | |
| z: Math.sin(angle) * r | |
| }; | |
| }, | |
| sphere: (i, total) => { | |
| const phi = Math.acos(-1 + (2 * i) / total); | |
| const theta = Math.sqrt(total * Math.PI) * phi; | |
| const r = 5; | |
| return { | |
| x: r * Math.cos(theta) * Math.sin(phi), | |
| y: r * Math.sin(theta) * Math.sin(phi), | |
| z: r * Math.cos(phi) | |
| }; | |
| }, | |
| heart: (i, total) => { | |
| const t = Math.random() * Math.PI * 2; | |
| // 3D Heart approximation | |
| const x = 16 * Math.pow(Math.sin(t), 3); | |
| const y = 13 * Math.cos(t) - 5 * Math.cos(2*t) - 2 * Math.cos(3*t) - Math.cos(4*t); | |
| const scale = 0.35; | |
| return { | |
| x: x * scale, | |
| y: y * scale, | |
| z: (Math.random()-0.5) * 4 | |
| }; | |
| }, | |
| tornado: (i, total) => { | |
| const angle = i * 0.1; | |
| const y = (i / total) * 12 - 6; | |
| const r = 1 + (y + 6) * 0.3; | |
| return { | |
| x: Math.cos(angle) * r, | |
| y: y, | |
| z: Math.sin(angle) * r | |
| }; | |
| } | |
| }; | |
| /** | |
| * ------------------------------------------------------------------ | |
| * 2. CORE ENGINE SETUP | |
| * ------------------------------------------------------------------ | |
| */ | |
| const PARTICLE_COUNT = 12000; // Optimized for CPU physics | |
| const scene = new THREE.Scene(); | |
| scene.fog = new THREE.FogExp2(0x000000, 0.04); | |
| const camera = new THREE.PerspectiveCamera(75, window.innerWidth/window.innerHeight, 0.1, 100); | |
| camera.position.z = 12; | |
| const renderer = new THREE.WebGLRenderer({ antialias: false, powerPreference: "high-performance" }); | |
| renderer.setSize(window.innerWidth, window.innerHeight); | |
| renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2)); | |
| document.body.appendChild(renderer.domElement); | |
| /** | |
| * ------------------------------------------------------------------ | |
| * 3. PARTICLE SYSTEM (PHYSICS BASED) | |
| * ------------------------------------------------------------------ | |
| */ | |
| const geometry = new THREE.BufferGeometry(); | |
| // Arrays for Double Buffering / Physics | |
| const posArray = new Float32Array(PARTICLE_COUNT * 3); | |
| const targetArray = new Float32Array(PARTICLE_COUNT * 3); | |
| const velocityArray = new Float32Array(PARTICLE_COUNT * 3); // Velocity for physics | |
| const colorsArray = new Float32Array(PARTICLE_COUNT * 3); | |
| const baseColor = new THREE.Color(0x00aaff); | |
| for(let i=0; i<PARTICLE_COUNT; i++) { | |
| const i3 = i*3; | |
| posArray[i3] = (Math.random()-0.5)*20; | |
| posArray[i3+1] = (Math.random()-0.5)*20; | |
| posArray[i3+2] = (Math.random()-0.5)*20; | |
| targetArray[i3] = posArray[i3]; | |
| colorsArray[i3] = baseColor.r; | |
| colorsArray[i3+1] = baseColor.g; | |
| colorsArray[i3+2] = baseColor.b; | |
| } | |
| geometry.setAttribute('position', new THREE.BufferAttribute(posArray, 3)); | |
| geometry.setAttribute('color', new THREE.BufferAttribute(colorsArray, 3)); | |
| const material = new THREE.PointsMaterial({ | |
| size: 0.15, | |
| map: createGlowTexture(), // Use generated texture | |
| vertexColors: true, | |
| transparent: true, | |
| opacity: 0.9, | |
| blending: THREE.AdditiveBlending, | |
| depthWrite: false | |
| }); | |
| const particles = new THREE.Points(geometry, material); | |
| scene.add(particles); | |
| /** | |
| * ------------------------------------------------------------------ | |
| * 4. LOGIC & STATE | |
| * ------------------------------------------------------------------ | |
| */ | |
| const AppState = { | |
| currentShape: 'galaxy', | |
| shapeKeys: Object.keys(Shapes), | |
| shapeIndex: 0, | |
| hand: { x: 0, y: 0, z: 0, active: false, pinch: 0, fist: false }, | |
| handVelocity: { x: 0, y: 0 } | |
| }; | |
| function switchShape(direction) { | |
| if(direction === 'next') { | |
| AppState.shapeIndex = (AppState.shapeIndex + 1) % AppState.shapeKeys.length; | |
| } else { | |
| AppState.shapeIndex = (AppState.shapeIndex - 1 + AppState.shapeKeys.length) % AppState.shapeKeys.length; | |
| } | |
| AppState.currentShape = AppState.shapeKeys[AppState.shapeIndex]; | |
| document.getElementById('mode-display').innerText = `MODE: ${AppState.currentShape.toUpperCase()}`; | |
| // Show UI feedback | |
| const alert = document.getElementById('gesture-alert'); | |
| alert.innerText = `${AppState.currentShape.toUpperCase()}`; | |
| alert.style.opacity = 1; | |
| setTimeout(() => alert.style.opacity = 0, 1000); | |
| // Recalculate targets | |
| const func = Shapes[AppState.currentShape]; | |
| for(let i=0; i<PARTICLE_COUNT; i++) { | |
| const p = func(i, PARTICLE_COUNT); | |
| targetArray[i*3] = p.x; | |
| targetArray[i*3+1] = p.y; | |
| targetArray[i*3+2] = p.z; | |
| } | |
| } | |
| // Init first shape | |
| switchShape('next'); | |
| /** | |
| * ------------------------------------------------------------------ | |
| * 5. COMPUTER VISION (THROTTLED) | |
| * ------------------------------------------------------------------ | |
| */ | |
| const videoElement = document.getElementById('input-video'); | |
| let lastHandX = 0; | |
| // We will only process MediaPipe every 2 frames to save CPU for rendering | |
| let frameSkipCounter = 0; | |
| const hands = new Hands({locateFile: (file) => `https://cdn.jsdelivr.net/npm/@mediapipe/hands/${file}`}); | |
| hands.setOptions({ | |
| maxNumHands: 1, | |
| modelComplexity: 0, // Lite model for speed | |
| minDetectionConfidence: 0.5, | |
| minTrackingConfidence: 0.5 | |
| }); | |
| hands.onResults(results => { | |
| const loading = document.getElementById('loading'); | |
| if(loading) loading.style.display = 'none'; | |
| if (results.multiHandLandmarks && results.multiHandLandmarks.length > 0) { | |
| const landmarks = results.multiHandLandmarks[0]; | |
| AppState.hand.active = true; | |
| // 1. Position Mapping (Normalized -1 to 1) | |
| // Invert X because webcam is mirrored | |
| const hx = -(landmarks[9].x - 0.5) * 10; | |
| const hy = -(landmarks[9].y - 0.5) * 8; | |
| // Calculate Velocity for gestures | |
| AppState.handVelocity.x = hx - lastHandX; | |
| lastHandX = hx; | |
| // Smooth hand position | |
| AppState.hand.x += (hx - AppState.hand.x) * 0.2; | |
| AppState.hand.y += (hy - AppState.hand.y) * 0.2; | |
| // 2. Pinch Detection (Thumb Tip vs Index Tip) | |
| const pinchDist = Math.hypot(landmarks[4].x - landmarks[8].x, landmarks[4].y - landmarks[8].y); | |
| AppState.hand.pinch = pinchDist; // <0.05 is pinch | |
| // 3. Fist Detection (Tip to Wrist) | |
| const wrist = landmarks[0]; | |
| const midTip = landmarks[12]; | |
| const dist = Math.hypot(wrist.x - midTip.x, wrist.y - midTip.y); | |
| AppState.hand.fist = dist < 0.2; // Threshold for fist | |
| // 4. Swipe Detection | |
| if (Math.abs(AppState.handVelocity.x) > 0.6) { | |
| // Debounce swipe | |
| if (!AppState.swiping) { | |
| AppState.swiping = true; | |
| switchShape(AppState.handVelocity.x > 0 ? 'next' : 'prev'); | |
| setTimeout(() => AppState.swiping = false, 500); | |
| } | |
| } | |
| } else { | |
| AppState.hand.active = false; | |
| } | |
| }); | |
| const cameraUtils = new Camera(videoElement, { | |
| onFrame: async () => { | |
| // Throttling Logic: Run AI every 2nd frame only | |
| frameSkipCounter++; | |
| if(frameSkipCounter % 2 === 0) { | |
| await hands.send({image: videoElement}); | |
| } | |
| }, | |
| width: 640, | |
| height: 480 | |
| }); | |
| cameraUtils.start(); | |
| /** | |
| * ------------------------------------------------------------------ | |
| * 6. PHYSICS LOOP | |
| * ------------------------------------------------------------------ | |
| */ | |
| const clock = new THREE.Clock(); | |
| function animate() { | |
| requestAnimationFrame(animate); | |
| const time = clock.getElapsedTime(); | |
| const positions = particles.geometry.attributes.position.array; | |
| const colors = particles.geometry.attributes.color.array; | |
| // Interaction Variables | |
| let globalScale = 1.0; | |
| if(AppState.hand.active) { | |
| // Map pinch to scale: 0.02->0.5, 0.2->1.5 | |
| globalScale = THREE.MathUtils.mapLinear(AppState.hand.pinch, 0.02, 0.2, 0.5, 1.8); | |
| } | |
| // Particle Loop | |
| for (let i = 0; i < PARTICLE_COUNT; i++) { | |
| const i3 = i * 3; | |
| // 1. Get Target | |
| let tx = targetArray[i3] * globalScale; | |
| let ty = targetArray[i3+1] * globalScale; | |
| let tz = targetArray[i3+2] * globalScale; | |
| // 2. Hand Repulsion / Attraction Physics | |
| if (AppState.hand.active) { | |
| const dx = positions[i3] - AppState.hand.x; | |
| const dy = positions[i3+1] - AppState.hand.y; | |
| const distSq = dx*dx + dy*dy; | |
| // Repulsion Radius | |
| if (distSq < 4) { | |
| const force = (4 - distSq) * 0.1; | |
| if(AppState.hand.fist) { | |
| // Attraction (Black Hole) | |
| tx -= dx * force * 5; | |
| ty -= dy * force * 5; | |
| } else { | |
| // Repulsion (Push away) | |
| tx += dx * force; | |
| ty += dy * force; | |
| } | |
| } | |
| } | |
| // 3. Spring Physics Integration | |
| // Acceleration = (Target - Current) * SpringStrength | |
| const ax = (tx - positions[i3]) * 0.05; // Spring strength | |
| const ay = (ty - positions[i3+1]) * 0.05; | |
| const az = (tz - positions[i3+2]) * 0.05; | |
| // Update Velocity (Inertia) | |
| velocityArray[i3] += ax; | |
| velocityArray[i3+1] += ay; | |
| velocityArray[i3+2] += az; | |
| // Friction (Damping) | |
| velocityArray[i3] *= 0.92; | |
| velocityArray[i3+1] *= 0.92; | |
| velocityArray[i3+2] *= 0.92; | |
| // Apply Velocity | |
| positions[i3] += velocityArray[i3]; | |
| positions[i3+1] += velocityArray[i3+1]; | |
| positions[i3+2] += velocityArray[i3+2]; | |
| // 4. Dynamic Coloring based on Speed | |
| const speed = Math.abs(velocityArray[i3]) + Math.abs(velocityArray[i3+1]); | |
| if (speed > 0.1) { | |
| // Moving fast -> White/Cyan | |
| colors[i3] = 0.8; colors[i3+1] = 1.0; colors[i3+2] = 1.0; | |
| } else { | |
| // Resting -> Base Color | |
| colors[i3] += (baseColor.r - colors[i3]) * 0.1; | |
| colors[i3+1] += (baseColor.g - colors[i3+1]) * 0.1; | |
| colors[i3+2] += (baseColor.b - colors[i3+2]) * 0.1; | |
| } | |
| } | |
| // Global idle rotation | |
| if(!AppState.hand.active) { | |
| particles.rotation.y = time * 0.1; | |
| } | |
| particles.geometry.attributes.position.needsUpdate = true; | |
| particles.geometry.attributes.color.needsUpdate = true; | |
| renderer.render(scene, camera); | |
| } | |
| animate(); | |
| // Resize Handler | |
| window.addEventListener('resize', () => { | |
| camera.aspect = window.innerWidth / window.innerHeight; | |
| camera.updateProjectionMatrix(); | |
| renderer.setSize(window.innerWidth, window.innerHeight); | |
| }); | |
| </script> | |
| </body> | |
| </html> | |