| <!DOCTYPE html> |
| <html lang="en"> |
| <head> |
| <meta charset="UTF-8"> |
| <meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no, maximum-scale=1.0"> |
| <title>Gesture Controlled 3D Particles (HD)</title> |
| <style> |
| body { |
| margin: 0; |
| overflow: hidden; |
| background-color: #050505; |
| font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; |
| color: white; |
| user-select: none; |
| |
| touch-action: none; |
| -webkit-tap-highlight-color: transparent; |
| } |
| |
| #canvas-container { |
| position: absolute; |
| top: 0; |
| left: 0; |
| width: 100vw; |
| height: 100vh; |
| z-index: 1; |
| } |
| |
| |
| #ui-layer { |
| position: absolute; |
| top: 20px; |
| left: 20px; |
| z-index: 10; |
| pointer-events: none; |
| } |
| |
| h1 { |
| margin: 0; |
| font-weight: 300; |
| letter-spacing: 2px; |
| font-size: 1.5rem; |
| text-shadow: 0 0 10px rgba(0, 255, 255, 0.5); |
| } |
| |
| #status { |
| font-size: 0.9rem; |
| color: #aaa; |
| margin-top: 5px; |
| } |
| |
| #controls-hint { |
| margin-top: 15px; |
| font-size: 0.85rem; |
| line-height: 1.6; |
| background: rgba(0, 0, 0, 0.5); |
| padding: 10px; |
| border-radius: 8px; |
| border-left: 3px solid #00d2ff; |
| backdrop-filter: blur(5px); |
| -webkit-backdrop-filter: blur(5px); |
| } |
| |
| .highlight { |
| color: #00d2ff; |
| font-weight: bold; |
| } |
| |
| |
| #start-screen { |
| position: absolute; |
| top: 0; |
| left: 0; |
| width: 100%; |
| height: 100%; |
| background: rgba(0, 0, 0, 0.95); |
| display: flex; |
| flex-direction: column; |
| justify-content: center; |
| align-items: center; |
| z-index: 100; |
| transition: opacity 0.5s ease; |
| text-align: center; |
| padding: 20px; |
| box-sizing: border-box; |
| } |
| |
| #start-btn { |
| padding: 15px 40px; |
| font-size: 1.2rem; |
| background: linear-gradient(45deg, #00d2ff, #3a7bd5); |
| border: none; |
| color: white; |
| border-radius: 30px; |
| cursor: pointer; |
| box-shadow: 0 0 20px rgba(0, 210, 255, 0.5); |
| transition: transform 0.2s, box-shadow 0.2s; |
| text-transform: uppercase; |
| letter-spacing: 2px; |
| margin-bottom: 20px; |
| } |
| |
| #start-btn:active { |
| transform: scale(0.95); |
| } |
| |
| |
| .input_video { |
| display: none; |
| |
| width: 1px; |
| height: 1px; |
| opacity: 0; |
| } |
| |
| #loading { |
| margin-top: 20px; |
| display: none; |
| font-size: 1.1rem; |
| } |
| |
| .spinner { |
| width: 20px; |
| height: 20px; |
| border: 3px solid rgba(255,255,255,0.3); |
| border-radius: 50%; |
| border-top-color: #fff; |
| animation: spin 1s ease-in-out infinite; |
| display: inline-block; |
| margin-right: 10px; |
| vertical-align: middle; |
| } |
| |
| @keyframes spin { |
| to { transform: rotate(360deg); } |
| } |
| </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/drawing_utils/drawing_utils.js" crossorigin="anonymous"></script> |
| <script src="https://cdn.jsdelivr.net/npm/@mediapipe/hands/hands.js" crossorigin="anonymous"></script> |
| </head> |
| <body> |
|
|
| |
| <div id="ui-layer"> |
| <h1>ETHERIAL PARTICLES HD</h1> |
| <div id="status">Waiting for start...</div> |
| <div id="controls-hint"> |
| <span class="highlight">☝ Index:</span> Attract<br> |
| <span class="highlight">✊ Fist:</span> Explosion<br> |
| <span class="highlight">✌ Peace:</span> Next Shape<br> |
| <span class="highlight">🖱 Mouse:</span> Works too |
| </div> |
| </div> |
|
|
| |
| <div id="start-screen"> |
| <button id="start-btn">Start Experience</button> |
| <div id="loading"><div class="spinner"></div>Initializing HD Core & AI...</div> |
| </div> |
|
|
| |
| <video class="input_video" playsinline muted autoplay></video> |
| |
| |
| <div id="canvas-container"></div> |
|
|
| <script> |
| |
| const CONFIG = { |
| particleCount: 15000, |
| particleSize: 0.15, |
| baseSpeed: 0.05, |
| attractionStrength: 0.08, |
| repulsionStrength: 0.2, |
| colors: [0x00d2ff, 0x3a7bd5, 0xff00ff, 0x00ffaa] |
| }; |
| |
| |
| const state = { |
| handActive: false, |
| handPosition: new THREE.Vector3(0, 0, 0), |
| isFist: false, |
| gestureCooldown: 0, |
| currentShapeIndex: 0, |
| targetShape: 'sphere' |
| }; |
| |
| const shapes = ['sphere', 'heart', 'saturn', 'helix', 'torus']; |
| |
| |
| let scene, camera, renderer, particles, geometry, material; |
| let positions, targets, colors, velocities; |
| let time = 0; |
| |
| |
| function initThree() { |
| const container = document.getElementById('canvas-container'); |
| |
| |
| scene = new THREE.Scene(); |
| scene.fog = new THREE.FogExp2(0x050505, 0.02); |
| |
| |
| camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 100); |
| camera.position.z = 5; |
| |
| |
| renderer = new THREE.WebGLRenderer({ |
| antialias: true, |
| alpha: true, |
| powerPreference: "high-performance" |
| }); |
| renderer.setSize(window.innerWidth, window.innerHeight); |
| |
| |
| renderer.setPixelRatio(Math.min(window.devicePixelRatio, 3)); |
| |
| container.appendChild(renderer.domElement); |
| |
| |
| const sprite = generateSprite(); |
| |
| |
| geometry = new THREE.BufferGeometry(); |
| positions = new Float32Array(CONFIG.particleCount * 3); |
| targets = new Float32Array(CONFIG.particleCount * 3); |
| colors = new Float32Array(CONFIG.particleCount * 3); |
| velocities = new Float32Array(CONFIG.particleCount * 3); |
| |
| const colorObj = new THREE.Color(); |
| |
| for (let i = 0; i < CONFIG.particleCount; i++) { |
| |
| positions[i * 3] = (Math.random() - 0.5) * 10; |
| positions[i * 3 + 1] = (Math.random() - 0.5) * 10; |
| positions[i * 3 + 2] = (Math.random() - 0.5) * 10; |
| |
| |
| colorObj.setHex(CONFIG.colors[Math.floor(Math.random() * CONFIG.colors.length)]); |
| colors[i * 3] = colorObj.r; |
| colors[i * 3 + 1] = colorObj.g; |
| colors[i * 3 + 2] = colorObj.b; |
| |
| velocities[i*3] = 0; |
| velocities[i*3+1] = 0; |
| velocities[i*3+2] = 0; |
| } |
| |
| geometry.setAttribute('position', new THREE.BufferAttribute(positions, 3)); |
| geometry.setAttribute('color', new THREE.BufferAttribute(colors, 3)); |
| |
| |
| material = new THREE.PointsMaterial({ |
| size: CONFIG.particleSize, |
| map: sprite, |
| vertexColors: true, |
| blending: THREE.AdditiveBlending, |
| depthWrite: false, |
| transparent: true, |
| opacity: 0.85 |
| }); |
| |
| particles = new THREE.Points(geometry, material); |
| scene.add(particles); |
| |
| |
| generateTargetShape('sphere'); |
| |
| |
| window.addEventListener('resize', onWindowResize, false); |
| |
| document.addEventListener('mousemove', onMouseMove, false); |
| document.addEventListener('touchmove', onTouchMove, { passive: false }); |
| document.addEventListener('touchstart', onTouchStart, { passive: false }); |
| document.addEventListener('touchend', onTouchEnd, { passive: false }); |
| } |
| |
| |
| function generateSprite() { |
| const canvas = document.createElement('canvas'); |
| |
| canvas.width = 128; |
| canvas.height = 128; |
| const context = canvas.getContext('2d'); |
| |
| const gradient = context.createRadialGradient(64, 64, 0, 64, 64, 64); |
| gradient.addColorStop(0, 'rgba(255,255,255,1)'); |
| gradient.addColorStop(0.2, 'rgba(255,255,255,0.9)'); |
| gradient.addColorStop(0.5, 'rgba(255,255,255,0.3)'); |
| gradient.addColorStop(1, 'rgba(0,0,0,0)'); |
| context.fillStyle = gradient; |
| context.fillRect(0, 0, 128, 128); |
| const texture = new THREE.Texture(canvas); |
| texture.needsUpdate = true; |
| return texture; |
| } |
| |
| |
| function generateTargetShape(type) { |
| state.targetShape = type; |
| const cnt = CONFIG.particleCount; |
| |
| for (let i = 0; i < cnt; i++) { |
| let x, y, z; |
| const idx = i * 3; |
| |
| if (type === 'sphere') { |
| const r = 2.5; |
| const theta = Math.random() * Math.PI * 2; |
| const phi = Math.acos(2 * Math.random() - 1); |
| x = r * Math.sin(phi) * Math.cos(theta); |
| y = r * Math.sin(phi) * Math.sin(theta); |
| z = r * Math.cos(phi); |
| } |
| else if (type === 'heart') { |
| const t = Math.random() * Math.PI * 2; |
| |
| x = 16 * Math.pow(Math.sin(t), 3); |
| y = 13 * Math.cos(t) - 5 * Math.cos(2*t) - 2 * Math.cos(3*t) - Math.cos(4*t); |
| z = (Math.random() - 0.5) * 4; |
| x *= 0.12; y *= 0.12; z *= 0.5; |
| y += 0.5; |
| } |
| else if (type === 'saturn') { |
| const isRing = Math.random() > 0.4; |
| if (isRing) { |
| const angle = Math.random() * Math.PI * 2; |
| const dist = 3 + Math.random() * 1.5; |
| x = Math.cos(angle) * dist; |
| z = Math.sin(angle) * dist; |
| y = (Math.random() - 0.5) * 0.1; |
| const tilt = 0.4; |
| const yt = y * Math.cos(tilt) - z * Math.sin(tilt); |
| const zt = y * Math.sin(tilt) + z * Math.cos(tilt); |
| y = yt; z = zt; |
| } else { |
| const r = 1.5; |
| const theta = Math.random() * Math.PI * 2; |
| const phi = Math.acos(2 * Math.random() - 1); |
| x = r * Math.sin(phi) * Math.cos(theta); |
| y = r * Math.sin(phi) * Math.sin(theta); |
| z = r * Math.cos(phi); |
| } |
| } |
| else if (type === 'helix') { |
| const t = (i / cnt) * Math.PI * 20; |
| const r = 1.5; |
| x = Math.cos(t) * r; |
| z = Math.sin(t) * r; |
| y = (i / cnt) * 6 - 3; |
| if (i % 2 === 0) { |
| x = Math.cos(t + Math.PI) * r; |
| z = Math.sin(t + Math.PI) * r; |
| } |
| x += (Math.random() - 0.5) * 0.2; |
| z += (Math.random() - 0.5) * 0.2; |
| } |
| else if (type === 'torus') { |
| const u = Math.random() * Math.PI * 2; |
| const v = Math.random() * Math.PI * 2; |
| const R = 2.5; |
| const r = 0.8; |
| x = (R + r * Math.cos(v)) * Math.cos(u); |
| y = (R + r * Math.cos(v)) * Math.sin(u); |
| z = r * Math.sin(v); |
| } |
| |
| targets[idx] = x; |
| targets[idx + 1] = y; |
| targets[idx + 2] = z; |
| } |
| |
| const shapeName = type.charAt(0).toUpperCase() + type.slice(1); |
| document.getElementById('status').innerHTML = `Shape: <span style="color:#00d2ff">${shapeName}</span>`; |
| } |
| |
| |
| function animate() { |
| requestAnimationFrame(animate); |
| |
| time += 0.005; |
| |
| let targetX = 0, targetY = 0; |
| |
| if (state.handActive) { |
| targetX = state.handPosition.x * 4; |
| targetY = state.handPosition.y * 3; |
| } else { |
| targetX = Math.sin(time) * 1; |
| targetY = Math.cos(time * 0.7) * 1; |
| } |
| |
| const positionsArr = geometry.attributes.position.array; |
| |
| if (state.gestureCooldown > 0) state.gestureCooldown--; |
| |
| for (let i = 0; i < CONFIG.particleCount; i++) { |
| const px = positionsArr[i * 3]; |
| const py = positionsArr[i * 3 + 1]; |
| const pz = positionsArr[i * 3 + 2]; |
| |
| const tx = targets[i * 3]; |
| const ty = targets[i * 3 + 1]; |
| const tz = targets[i * 3 + 2]; |
| |
| let vx = (tx - px) * CONFIG.baseSpeed; |
| let vy = (ty - py) * CONFIG.baseSpeed; |
| let vz = (tz - pz) * CONFIG.baseSpeed; |
| |
| const dx = px - targetX; |
| const dy = py - targetY; |
| const dz = pz - 0; |
| |
| const distSq = dx*dx + dy*dy + dz*dz; |
| const dist = Math.sqrt(distSq); |
| |
| if (state.handActive && dist < 2.5) { |
| if (state.isFist) { |
| const force = CONFIG.repulsionStrength / (dist + 0.1); |
| vx += dx * force * 5; |
| vy += dy * force * 5; |
| vz += dz * force * 5; |
| } else { |
| const force = CONFIG.attractionStrength / (dist + 0.5); |
| vx -= dx * force; |
| vy -= dy * force; |
| vz -= dz * force; |
| |
| vx += -dy * force * 0.5; |
| vy += dx * force * 0.5; |
| } |
| } |
| |
| vx += (Math.random() - 0.5) * 0.01; |
| vy += (Math.random() - 0.5) * 0.01; |
| vz += (Math.random() - 0.5) * 0.01; |
| |
| positionsArr[i * 3] += vx; |
| positionsArr[i * 3 + 1] += vy; |
| positionsArr[i * 3 + 2] += vz; |
| } |
| |
| geometry.attributes.position.needsUpdate = true; |
| |
| particles.rotation.y += 0.001; |
| particles.rotation.z = Math.sin(time * 0.2) * 0.05; |
| |
| renderer.render(scene, camera); |
| } |
| |
| |
| |
| function onWindowResize() { |
| camera.aspect = window.innerWidth / window.innerHeight; |
| camera.updateProjectionMatrix(); |
| renderer.setSize(window.innerWidth, window.innerHeight); |
| } |
| |
| |
| function onMouseMove(event) { |
| if (!state.handActive) { |
| state.handPosition.x = (event.clientX / window.innerWidth) * 2 - 1; |
| state.handPosition.y = -(event.clientY / window.innerHeight) * 2 + 1; |
| state.isFist = (event.buttons === 1); |
| } |
| } |
| document.addEventListener('mousedown', () => { if(!state.handActive) state.isFist = true; }); |
| document.addEventListener('mouseup', () => { if(!state.handActive) state.isFist = false; }); |
| document.addEventListener('keydown', (e) => { if(e.code === 'Space') cycleShape(); }); |
| |
| |
| function onTouchMove(e) { |
| if (!state.handActive && e.touches.length > 0) { |
| e.preventDefault(); |
| const touch = e.touches[0]; |
| state.handPosition.x = (touch.clientX / window.innerWidth) * 2 - 1; |
| state.handPosition.y = -(touch.clientY / window.innerHeight) * 2 + 1; |
| } |
| } |
| function onTouchStart(e) { |
| if (!state.handActive) { |
| e.preventDefault(); |
| state.isFist = true; |
| } |
| } |
| function onTouchEnd(e) { |
| if (!state.handActive) state.isFist = false; |
| } |
| |
| function cycleShape() { |
| state.currentShapeIndex = (state.currentShapeIndex + 1) % shapes.length; |
| generateTargetShape(shapes[state.currentShapeIndex]); |
| } |
| |
| |
| |
| const videoElement = document.querySelector('.input_video'); |
| const startBtn = document.getElementById('start-btn'); |
| const loading = document.getElementById('loading'); |
| const startScreen = document.getElementById('start-screen'); |
| const statusEl = document.getElementById('status'); |
| |
| function onResults(results) { |
| if (results.multiHandLandmarks && results.multiHandLandmarks.length > 0) { |
| state.handActive = true; |
| const landmarks = results.multiHandLandmarks[0]; |
| |
| const x = (1 - landmarks[9].x) * 2 - 1; |
| const y = -(landmarks[9].y * 2 - 1); |
| |
| state.handPosition.x += (x - state.handPosition.x) * 0.2; |
| state.handPosition.y += (y - state.handPosition.y) * 0.2; |
| |
| const isIndexOpen = landmarks[8].y < landmarks[6].y; |
| const isMiddleOpen = landmarks[12].y < landmarks[10].y; |
| const isRingOpen = landmarks[16].y < landmarks[14].y; |
| const isPinkyOpen = landmarks[20].y < landmarks[18].y; |
| |
| const openCount = [isIndexOpen, isMiddleOpen, isRingOpen, isPinkyOpen].filter(Boolean).length; |
| |
| if (openCount <= 1) { |
| state.isFist = true; |
| } else { |
| state.isFist = false; |
| } |
| |
| if (isIndexOpen && isMiddleOpen && !isRingOpen && !isPinkyOpen) { |
| if (state.gestureCooldown === 0) { |
| cycleShape(); |
| state.gestureCooldown = 60; |
| } |
| } |
| } else { |
| state.handActive = false; |
| } |
| } |
| |
| const hands = new Hands({locateFile: (file) => { |
| return `https://cdn.jsdelivr.net/npm/@mediapipe/hands/${file}`; |
| }}); |
| |
| hands.setOptions({ |
| maxNumHands: 1, |
| modelComplexity: 1, |
| minDetectionConfidence: 0.5, |
| minTrackingConfidence: 0.5 |
| }); |
| |
| hands.onResults(onResults); |
| |
| startBtn.addEventListener('click', () => { |
| startBtn.style.display = 'none'; |
| loading.style.display = 'block'; |
| |
| |
| initThree(); |
| animate(); |
| |
| const cameraUtils = new Camera(videoElement, { |
| onFrame: async () => { |
| await hands.send({image: videoElement}); |
| }, |
| width: 640, |
| height: 480 |
| }); |
| |
| cameraUtils.start() |
| .then(() => { |
| startScreen.style.opacity = 0; |
| setTimeout(() => startScreen.style.display = 'none', 500); |
| statusEl.innerText = "Camera Active. Show hand."; |
| }) |
| .catch(err => { |
| console.error("Camera Error:", err); |
| loading.innerHTML = "Camera access denied.<br>Using touch/mouse mode."; |
| setTimeout(() => { |
| startScreen.style.opacity = 0; |
| setTimeout(() => startScreen.style.display = 'none', 500); |
| statusEl.innerText = "Mouse/Touch Mode Active"; |
| }, 2000); |
| }); |
| }); |
| |
| </script> |
| </body> |
| </html> |
|
|
|
|