Particle3d_v002 / index.html
JamesToth's picture
Update index.html
1d6eb75 verified
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Single Hand Particle System</title>
<style>
body { margin: 0; overflow: hidden; background-color: #080808; font-family: 'Inter', system-ui, sans-serif; }
#webcam-feed { display: none; }
#glass-panel {
position: absolute;
top: 24px;
left: 24px;
width: 300px;
padding: 24px;
background: rgba(20, 20, 20, 0.7);
backdrop-filter: blur(12px);
border-radius: 20px;
border: 1px solid rgba(255, 255, 255, 0.08);
color: #ffffff;
z-index: 100;
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.4);
}
h2 { font-size: 14px; text-transform: uppercase; letter-spacing: 2px; color: #888; margin: 0 0 16px 0; }
.control-row { margin-bottom: 16px; }
label { display: block; font-size: 12px; margin-bottom: 8px; color: #ccc; font-weight: 500; }
select, input[type="color"] {
width: 100%;
padding: 10px;
border-radius: 8px;
border: 1px solid rgba(255,255,255,0.1);
background: rgba(0,0,0,0.3);
color: white;
outline: none;
font-size: 13px;
transition: border-color 0.2s;
}
select:hover { border-color: #00ffff; }
.metric-display {
display: flex;
justify-content: space-between;
font-size: 11px;
color: #666;
margin-top: 4px;
}
.status-indicator {
margin-top: 20px;
padding-top: 20px;
border-top: 1px solid rgba(255,255,255,0.1);
display: flex;
align-items: center;
gap: 8px;
font-size: 12px;
color: #00ff9d;
}
.led { width: 6px; height: 6px; background: #00ff9d; border-radius: 50%; box-shadow: 0 0 8px #00ff9d; }
.led.off { background: #ff3333; box-shadow: 0 0 8px #ff3333; }
#overlay-msg {
position: absolute;
bottom: 30px;
width: 100%;
text-align: center;
color: rgba(255,255,255,0.4);
font-size: 12px;
pointer-events: 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="glass-panel">
<h2>Controller</h2>
<div class="control-row">
<label>Geometry</label>
<select id="geometry-selector">
<option value="galaxy">Spiral Galaxy</option>
<option value="heart">Digital Heart</option>
<option value="dna">DNA Helix</option>
<option value="sphere">Quantum Sphere</option>
<option value="cube">Hyper Cube</option>
</select>
</div>
<div class="control-row">
<label>Color Tone</label>
<input type="color" id="color-selector" value="#00ffff">
</div>
<div class="status-indicator">
<div id="cam-led" class="led off"></div>
<span id="cam-status">Initializing AI...</span>
</div>
<div style="margin-top: 15px; font-size: 11px; color: #888; line-height: 1.6;">
<strong>Gestures:</strong><br>
• 🤏 Pinch to Scale<br>
• ✋ Move to Rotate<br>
• ✊ Fist to Explode
</div>
</div>
<div id="overlay-msg">Please allow camera access to interact</div>
<video id="webcam-feed"></video>
<script>
const CONFIG = {
particleCount: 36000,
particleSize: 0.01,
baseColor: 0x00ffff,
camWidth: 640,
camHeight: 480
};
const scene = new THREE.Scene();
scene.background = new THREE.Color(0x050505);
scene.fog = new THREE.FogExp2(0x050505, 0.03);
const camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 100);
camera.position.z = 6;
const renderer = new THREE.WebGLRenderer({ antialias: true });
renderer.setSize(window.innerWidth, window.innerHeight);
renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
document.body.appendChild(renderer.domElement);
const positions = new Float32Array(CONFIG.particleCount * 3);
const targetPositions = new Float32Array(CONFIG.particleCount * 3);
const geometry = new THREE.BufferGeometry();
for(let i=0; i<CONFIG.particleCount * 3; i++) {
positions[i] = (Math.random() - 0.5) * 10;
targetPositions[i] = positions[i];
}
geometry.setAttribute('position', new THREE.BufferAttribute(positions, 3));
const material = new THREE.PointsMaterial({
size: CONFIG.particleSize,
color: CONFIG.baseColor,
blending: THREE.AdditiveBlending,
depthWrite: false,
transparent: true,
opacity: 0.8
});
const particleSystem = new THREE.Points(geometry, material);
scene.add(particleSystem);
const state = {
shape: 'galaxy',
scale: 1.0,
rotationX: 0,
rotationY: 0,
chaos: 0,
smoothedScale: 1.0,
smoothedRotX: 0,
smoothedRotY: 0,
smoothedChaos: 0
};
const generators = {
sphere: (i) => {
const u = Math.random();
const v = Math.random();
const theta = 2 * Math.PI * u;
const phi = Math.acos(2 * v - 1);
const r = 2.5;
return {
x: r * Math.sin(phi) * Math.cos(theta),
y: r * Math.sin(phi) * Math.sin(theta),
z: r * Math.cos(phi)
};
},
heart: (i) => {
const t = Math.random() * Math.PI * 2;
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);
return { x: x * 0.15, y: y * 0.15, z: (Math.random()-0.5) * 2 };
},
galaxy: (i) => {
const arms = 5;
const spin = i / CONFIG.particleCount * arms;
const r = (i / CONFIG.particleCount) * 5;
const angle = spin * Math.PI * 2;
const drift = Math.random() * 0.5;
return {
x: Math.cos(angle) * r + drift,
y: (Math.random() - 0.5) * (2 - r/3),
z: Math.sin(angle) * r + drift
};
},
dna: (i) => {
const t = (i / CONFIG.particleCount) * 10 * Math.PI;
const radius = 1.5;
const strand = i % 2 === 0 ? 1 : -1;
return {
x: Math.cos(t + strand * Math.PI) * radius,
y: (i / CONFIG.particleCount - 0.5) * 10,
z: Math.sin(t + strand * Math.PI) * radius
};
},
cube: (i) => {
const s = 3;
return {
x: (Math.random() - 0.5) * s,
y: (Math.random() - 0.5) * s,
z: (Math.random() - 0.5) * s
};
}
};
function morphShape() {
const generator = generators[state.shape];
for(let i=0; i<CONFIG.particleCount; i++) {
const pos = generator(i);
targetPositions[i*3] = pos.x;
targetPositions[i*3+1] = pos.y;
targetPositions[i*3+2] = pos.z;
}
}
morphShape();
document.getElementById('geometry-selector').addEventListener('change', (e) => {
state.shape = e.target.value;
morphShape();
});
document.getElementById('color-selector').addEventListener('input', (e) => {
particleSystem.material.color.set(e.target.value);
});
function handleHandResults(results) {
const led = document.getElementById('cam-led');
const txt = document.getElementById('cam-status');
if (results.multiHandLandmarks && results.multiHandLandmarks.length > 0) {
led.classList.remove('off');
txt.innerText = "Hand Active";
const hand = results.multiHandLandmarks[0];
// 1. Rotation (Hand Position)
// Wrist is landmark 0
const wrist = hand[0];
// Normalize -0.5 to 0.5
state.rotationY = (wrist.x - 0.5) * 3;
state.rotationX = (wrist.y - 0.5) * 3;
// 2. Scale (Pinch Distance: Thumb Tip 4 vs Index Tip 8)
const thumb = hand[4];
const index = hand[8];
const pinchDist = Math.sqrt(
Math.pow(thumb.x - index.x, 2) +
Math.pow(thumb.y - index.y, 2)
);
// Map 0.02 (close) -> 0.1 (small) to 0.15 (far) -> 2.0 (big)
state.scale = THREE.MathUtils.mapLinear(pinchDist, 0.02, 0.2, 0.5, 2.5);
state.scale = THREE.MathUtils.clamp(state.scale, 0.5, 3.0);
// 3. Chaos (Fist Detection)
// Check average distance of tips to wrist
const tips = [8, 12, 16, 20];
let avgDist = 0;
tips.forEach(t => {
const tip = hand[t];
avgDist += Math.sqrt(Math.pow(tip.x - wrist.x, 2) + Math.pow(tip.y - wrist.y, 2));
});
avgDist /= 4;
// If tips are close to wrist, it's a fist
if(avgDist < 0.12) {
state.chaos = 1.0; // Explosion
} else {
state.chaos = 0.0;
}
} else {
led.classList.add('off');
txt.innerText = "No Hand Detected";
state.rotationX = 0;
state.rotationY = 0;
state.scale = 1.0;
state.chaos = 0;
}
}
const hands = new Hands({locateFile: (file) => `https://cdn.jsdelivr.net/npm/@mediapipe/hands/${file}`});
hands.setOptions({
maxNumHands: 1,
modelComplexity: 1,
minDetectionConfidence: 0.6,
minTrackingConfidence: 0.6
});
hands.onResults(handleHandResults);
const videoElem = document.getElementById('webcam-feed');
const cameraObj = new Camera(videoElem, {
onFrame: async () => { await hands.send({image: videoElem}); },
width: CONFIG.camWidth,
height: CONFIG.camHeight
});
cameraObj.start();
const clock = new THREE.Clock();
function animate() {
requestAnimationFrame(animate);
const dt = clock.getElapsedTime();
const positionsAttr = particleSystem.geometry.attributes.position;
const posArray = positionsAttr.array;
// Smooth Physics
state.smoothedScale += (state.scale - state.smoothedScale) * 0.1;
state.smoothedRotX += (state.rotationX - state.smoothedRotX) * 0.1;
state.smoothedRotY += (state.rotationY - state.smoothedRotY) * 0.1;
state.smoothedChaos += (state.chaos - state.smoothedChaos) * 0.1;
particleSystem.rotation.y = state.smoothedRotY + dt * 0.1;
particleSystem.rotation.x = state.smoothedRotX;
const expansion = state.smoothedScale;
const jitter = state.smoothedChaos * 0.8;
for(let i=0; i<CONFIG.particleCount; i++) {
const ix = i * 3;
const iy = ix + 1;
const iz = ix + 2;
let tx = targetPositions[ix] * expansion;
let ty = targetPositions[iy] * expansion;
let tz = targetPositions[iz] * expansion;
if(jitter > 0.01) {
tx += (Math.random() - 0.5) * jitter * 5;
ty += (Math.random() - 0.5) * jitter * 5;
tz += (Math.random() - 0.5) * jitter * 5;
}
posArray[ix] += (tx - posArray[ix]) * 0.1;
posArray[iy] += (ty - posArray[iy]) * 0.1;
posArray[iz] += (tz - posArray[iz]) * 0.1;
}
positionsAttr.needsUpdate = true;
renderer.render(scene, camera);
}
animate();
window.addEventListener('resize', () => {
camera.aspect = window.innerWidth / window.innerHeight;
camera.updateProjectionMatrix();
renderer.setSize(window.innerWidth, window.innerHeight);
});
</script>
</body>
</html>