Skip to content
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
195 changes: 195 additions & 0 deletions index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,195 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<title>Hand Particle System</title>
<meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no">

<style>
body {
margin: 0;
overflow: hidden;
background: black;
font-family: Arial, sans-serif;
}
#info {
position: fixed;
bottom: 10px;
width: 100%;
text-align: center;
color: #fff;
font-size: 13px;
opacity: 0.8;
}
video {
display: none;
}
</style>
</head>
<body>

<div id="info">
Open palm = color • Pinch = expand • Wave = change shape
</div>

<video id="video" playsinline></video>

<!-- Three.js -->
<script src="https://cdn.jsdelivr.net/npm/[email protected]/build/three.min.js"></script>

<!-- MediaPipe -->
<script src="https://cdn.jsdelivr.net/npm/@mediapipe/hands/hands.js"></script>
<script src="https://cdn.jsdelivr.net/npm/@mediapipe/camera_utils/camera_utils.js"></script>

<script>
/* ---------------- THREE.JS SETUP ---------------- */
const scene = new THREE.Scene();

const camera = new THREE.PerspectiveCamera(
60,
window.innerWidth / window.innerHeight,
0.1,
1000
);
camera.position.z = 120;

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);

/* ---------------- PARTICLES ---------------- */
const COUNT = 4000;
const geometry = new THREE.BufferGeometry();
const positions = new Float32Array(COUNT * 3);
const colors = new Float32Array(COUNT * 3);

for (let i = 0; i < COUNT; i++) {
positions[i * 3] = (Math.random() - 0.5) * 50;
positions[i * 3 + 1] = (Math.random() - 0.5) * 50;
positions[i * 3 + 2] = (Math.random() - 0.5) * 50;

colors[i * 3] = Math.random();
colors[i * 3 + 1] = Math.random();
colors[i * 3 + 2] = Math.random();
}

geometry.setAttribute("position", new THREE.BufferAttribute(positions, 3));
geometry.setAttribute("color", new THREE.BufferAttribute(colors, 3));

const material = new THREE.PointsMaterial({
size: 1.5,
vertexColors: true
});

const particles = new THREE.Points(geometry, material);
scene.add(particles);

/* ---------------- SHAPES ---------------- */
let template = 0;
function applyShape(scale) {
for (let i = 0; i < COUNT; i++) {
const a = i / COUNT * Math.PI * 2;

let x, y, z;
if (template === 0) { // Circle
x = Math.cos(a) * 30;
y = Math.sin(a) * 30;
z = 0;
} else if (template === 1) { // Heart
x = 16 * Math.pow(Math.sin(a), 3);
y = 13 * Math.cos(a) - 5 * Math.cos(2 * a);
z = 0;
} else { // Saturn
x = Math.cos(a) * 40;
y = Math.sin(a) * 10;
z = Math.sin(a) * 40;
}

positions[i * 3] = x * scale;
positions[i * 3 + 1] = y * scale;
positions[i * 3 + 2] = z * scale;
}
geometry.attributes.position.needsUpdate = true;
}

/* ---------------- MEDIAPIPE HANDS ---------------- */
const video = document.getElementById("video");

const hands = new Hands({
locateFile: file =>
`https://cdn.jsdelivr.net/npm/@mediapipe/hands/${file}`
});

hands.setOptions({
maxNumHands: 1,
modelComplexity: 0,
minDetectionConfidence: 0.6,
minTrackingConfidence: 0.6
});

let spread = 1;
let lastSwitch = 0;

hands.onResults(results => {
if (!results.multiHandLandmarks) return;

const lm = results.multiHandLandmarks[0];
const thumb = lm[4];
const index = lm[8];
const palm = lm[0];

/* Pinch = expand */
const pinch = Math.hypot(thumb.x - index.x, thumb.y - index.y);
spread = THREE.MathUtils.clamp(1 + (0.2 - pinch) * 8, 0.8, 4);
applyShape(spread);

/* Open palm = color change */
if (palm.y < index.y - 0.05) {
for (let i = 0; i < COUNT * 3; i++) {
colors[i] = Math.random();
}
geometry.attributes.color.needsUpdate = true;
}

/* Wave = template switch */
const now = Date.now();
if (Math.abs(palm.x - index.x) > 0.25 && now - lastSwitch > 800) {
template = (template + 1) % 3;
lastSwitch = now;
}
});

/* ---------------- CAMERA ---------------- */
const cameraFeed = new Camera(video, {
onFrame: async () => {
await hands.send({ image: video });
},
width: 640,
height: 480
});
cameraFeed.start();

/* ---------------- ANIMATION ---------------- */
let lastTime = 0;
function animate(time) {
if (time - lastTime < 33) return; // ~30 FPS (mobile safe)
lastTime = time;

requestAnimationFrame(animate);
particles.rotation.y += 0.0015;
particles.rotation.x += 0.0008;
renderer.render(scene, camera);
}
animate();

/* ---------------- RESIZE ---------------- */
window.addEventListener("resize", () => {
camera.aspect = window.innerWidth / window.innerHeight;
camera.updateProjectionMatrix();
renderer.setSize(window.innerWidth, window.innerHeight);
});
</script>

</body>
</html>