Hehe / index.html
sanch1tx's picture
Update index.html
312e40c verified
<!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;
/* Critical for mobile: prevents pulling-to-refresh or scrolling */
touch-action: none;
-webkit-tap-highlight-color: transparent;
}
#canvas-container {
position: absolute;
top: 0;
left: 0;
width: 100vw;
height: 100vh;
z-index: 1;
}
/* UI Overlay */
#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 */
#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);
}
/* Video element for MediaPipe (Hidden but required in DOM) */
.input_video {
display: none;
/* Crucial for iOS/Mobile to not go fullscreen */
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>
<!-- Three.js -->
<script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/r128/three.min.js"></script>
<!-- MediaPipe Hands -->
<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>
<!-- UI Layer -->
<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>
<!-- Start Overlay -->
<div id="start-screen">
<button id="start-btn">Start Experience</button>
<div id="loading"><div class="spinner"></div>Initializing HD Core & AI...</div>
</div>
<!-- Hidden Video Input - attributes critical for mobile/iframes -->
<video class="input_video" playsinline muted autoplay></video>
<!-- 3D Canvas -->
<div id="canvas-container"></div>
<script>
// --- Configuration ---
const CONFIG = {
particleCount: 15000,
particleSize: 0.15,
baseSpeed: 0.05,
attractionStrength: 0.08,
repulsionStrength: 0.2,
colors: [0x00d2ff, 0x3a7bd5, 0xff00ff, 0x00ffaa]
};
// --- State Management ---
const state = {
handActive: false,
handPosition: new THREE.Vector3(0, 0, 0), // Normalized -1 to 1
isFist: false,
gestureCooldown: 0,
currentShapeIndex: 0,
targetShape: 'sphere'
};
const shapes = ['sphere', 'heart', 'saturn', 'helix', 'torus'];
// --- Three.js Globals ---
let scene, camera, renderer, particles, geometry, material;
let positions, targets, colors, velocities;
let time = 0;
// --- Initialization ---
function initThree() {
const container = document.getElementById('canvas-container');
// Scene
scene = new THREE.Scene();
scene.fog = new THREE.FogExp2(0x050505, 0.02);
// Camera
camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 100);
camera.position.z = 5;
// Renderer - HD Setup
renderer = new THREE.WebGLRenderer({
antialias: true,
alpha: true,
powerPreference: "high-performance"
});
renderer.setSize(window.innerWidth, window.innerHeight);
// Critical for HD on Mobile: Use device pixel ratio, but cap at 3 to prevent overheating
renderer.setPixelRatio(Math.min(window.devicePixelRatio, 3));
container.appendChild(renderer.domElement);
// Create High-Res Texture for Particles
const sprite = generateSprite();
// Geometry
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++) {
// Initial positions (random cube)
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;
// Colors
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
material = new THREE.PointsMaterial({
size: CONFIG.particleSize,
map: sprite,
vertexColors: true,
blending: THREE.AdditiveBlending,
depthWrite: false,
transparent: true,
opacity: 0.85 // Slightly higher opacity for better visibility on small screens
});
particles = new THREE.Points(geometry, material);
scene.add(particles);
// Generate initial target shape
generateTargetShape('sphere');
// Listeners
window.addEventListener('resize', onWindowResize, false);
// Touch listeners for mobile mouse-fallback
document.addEventListener('mousemove', onMouseMove, false);
document.addEventListener('touchmove', onTouchMove, { passive: false });
document.addEventListener('touchstart', onTouchStart, { passive: false });
document.addEventListener('touchend', onTouchEnd, { passive: false });
}
// --- Helper: Generate High Definition Particle Texture ---
function generateSprite() {
const canvas = document.createElement('canvas');
// Increased to 128x128 for HD clarity on Retina displays
canvas.width = 128;
canvas.height = 128;
const context = canvas.getContext('2d');
// Center is 64, 64
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;
}
// --- Shape Generators ---
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;
// Spread particles inside the volume
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>`;
}
// --- Physics & Animation Loop ---
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);
}
// --- Input Handling ---
function onWindowResize() {
camera.aspect = window.innerWidth / window.innerHeight;
camera.updateProjectionMatrix();
renderer.setSize(window.innerWidth, window.innerHeight);
}
// Mouse Handlers
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(); });
// Touch Handlers for Mobile (without Camera)
function onTouchMove(e) {
if (!state.handActive && e.touches.length > 0) {
e.preventDefault(); // Prevent scroll
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; // Tap simulates fist/attract
}
}
function onTouchEnd(e) {
if (!state.handActive) state.isFist = false;
}
function cycleShape() {
state.currentShapeIndex = (state.currentShapeIndex + 1) % shapes.length;
generateTargetShape(shapes[state.currentShapeIndex]);
}
// --- MediaPipe Implementation ---
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';
// Init 3D world immediately so user sees something while AI loads
initThree();
animate();
const cameraUtils = new Camera(videoElement, {
onFrame: async () => {
await hands.send({image: videoElement});
},
width: 640, // Standard reliable resolution
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>