Skip to content

Commit 9740e44

Browse files
committed
split into separate files
1 parent 67a0825 commit 9740e44

File tree

3 files changed

+296
-278
lines changed

3 files changed

+296
-278
lines changed

index.html

Lines changed: 9 additions & 278 deletions
Original file line numberDiff line numberDiff line change
@@ -3,299 +3,30 @@
33
<head>
44
<meta charset="UTF-8" />
55
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
6-
<title>MediaPipe + Three.js Shape Creator</title>
7-
<style>
8-
body, html {
9-
margin: 0;
10-
padding: 0;
11-
overflow: hidden;
12-
background: #000;
13-
}
14-
#webcam, #canvas, #three-canvas {
15-
position: absolute;
16-
width: 100%;
17-
height: 100%;
18-
top: 0;
19-
left: 0;
20-
object-fit: cover;
21-
pointer-events: none;
22-
transform: scaleX(-1);
23-
}
24-
#recycle-bin {
25-
position: absolute;
26-
bottom: 60px;
27-
right: 60px;
28-
width: 160px;
29-
height: 160px;
30-
z-index: 20;
31-
pointer-events: none;
32-
}
33-
#recycle-bin.active {
34-
filter: drop-shadow(0 0 10px #ff0000);
35-
transform: scale(1.1);
36-
transition: transform 0.2s, filter 0.2s;
37-
}
38-
#instructions {
39-
position: absolute;
40-
top: 5px;
41-
left: 5px;
42-
color: white;
43-
background: rgba(0, 0, 0, 0.5);
44-
padding: 10px 15px;
45-
/* border-radius: 10px; */
46-
font-family: sans-serif;
47-
font-size: 14px;
48-
z-index: 30;
49-
}
50-
</style>
6+
<title>MediaPipe / Three.js Shape Creator</title>
7+
<script defer src="https://cloud.umami.is/script.js" data-website-id="eb59c81c-27cb-4e1d-9e8c-bfbe70c48cd9"></script>
8+
<link rel="stylesheet" href="styles.css">
519
</head>
5210
<body>
11+
5312
<video id="webcam" autoplay muted playsinline></video>
5413
<canvas id="canvas"></canvas>
5514
<div id="three-canvas"></div>
5615
<img id="recycle-bin" src="recyclebin.png" alt="Recycle Bin" />
5716
<div id="instructions">
5817
Bring hands close and pinch to create a shape<br>
59-
Move hands apart (while pinching) to make the shape larger<br>
60-
Hover over a shape and pinch to move it<br>
18+
> Move hands apart to make the shape larger<br>
19+
Hover over a shape / pinch to move it<br>
6120
Move a shape into the recycle bin to delete it
6221
</div>
22+
<p id="links-para"><a href="https://x.com/measure_plan" target="_blank">Twitter</a> | <a href="https://www.instagram.com/stereo.drift/" target="_blank">Instagram</a> | <a href="https://github.com/collidingScopes/threejs-handtracking-101" target="_blank">Source Code</a> | <a href="https://buymeacoffee.com/stereodrift" target="_blank">❤️</a></p>
6323

6424
<script src="https://cdn.jsdelivr.net/npm/@mediapipe/hands/hands.js"></script>
6525
<script src="https://cdn.jsdelivr.net/npm/@mediapipe/camera_utils/camera_utils.js"></script>
6626
<script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/r128/three.min.js"></script>
67-
<script>
68-
let video = document.getElementById('webcam');
69-
let canvas = document.getElementById('canvas');
70-
let ctx = canvas.getContext('2d');
71-
let scene, camera, renderer;
72-
let shapes = [];
73-
let currentShape = null;
74-
let isPinching = false;
75-
let shapeScale = 1;
76-
let originalDistance = null;
77-
let selectedShape = null;
78-
let shapeCreatedThisPinch = false;
79-
let lastShapeCreationTime = 0;
80-
const shapeCreationCooldown = 1000;
81-
82-
const initThree = () => {
83-
scene = new THREE.Scene();
84-
camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 1000);
85-
camera.position.z = 5;
86-
renderer = new THREE.WebGLRenderer({ alpha: true });
87-
renderer.setSize(window.innerWidth, window.innerHeight);
88-
document.getElementById('three-canvas').appendChild(renderer.domElement);
89-
const light = new THREE.AmbientLight(0xffffff, 1);
90-
scene.add(light);
91-
animate();
92-
};
93-
94-
const animate = () => {
95-
requestAnimationFrame(animate);
96-
shapes.forEach(shape => {
97-
if (shape !== selectedShape) {
98-
shape.rotation.x += 0.01;
99-
shape.rotation.y += 0.01;
100-
}
101-
});
102-
renderer.render(scene, camera);
103-
};
104-
105-
const neonColors = [0xFF00FF, 0x00FFFF, 0xFF3300, 0x39FF14, 0xFF0099, 0x00FF00, 0xFF6600, 0xFFFF00];
106-
let colorIndex = 0;
107-
108-
const getNextNeonColor = () => {
109-
const color = neonColors[colorIndex];
110-
colorIndex = (colorIndex + 1) % neonColors.length;
111-
return color;
112-
};
113-
114-
const createRandomShape = (position) => {
115-
const geometries = [
116-
new THREE.BoxGeometry(),
117-
new THREE.SphereGeometry(0.5, 32, 32),
118-
new THREE.ConeGeometry(0.5, 1, 32),
119-
new THREE.CylinderGeometry(0.5, 0.5, 1, 32)
120-
];
121-
const geometry = geometries[Math.floor(Math.random() * geometries.length)];
122-
const color = getNextNeonColor();
123-
const group = new THREE.Group();
124-
125-
const material = new THREE.MeshBasicMaterial({ color: color, transparent: true, opacity: 0.5 });
126-
const fillMesh = new THREE.Mesh(geometry, material);
127-
128-
const wireframeMaterial = new THREE.MeshBasicMaterial({ color: 0xffffff, wireframe: true });
129-
const wireframeMesh = new THREE.Mesh(geometry, wireframeMaterial);
130-
131-
group.add(fillMesh);
132-
group.add(wireframeMesh);
133-
group.position.copy(position);
134-
scene.add(group);
135-
136-
shapes.push(group);
137-
return group;
138-
};
139-
140-
const get3DCoords = (normX, normY) => {
141-
const x = (normX - 0.5) * 10;
142-
const y = (0.5 - normY) * 10;
143-
return new THREE.Vector3(x, y, 0);
144-
};
145-
146-
const isPinch = (landmarks) => {
147-
const d = (a, b) => Math.hypot(a.x - b.x, a.y - b.y, a.z - b.z);
148-
return d(landmarks[4], landmarks[8]) < 0.06;
149-
};
150-
151-
const areIndexFingersClose = (l, r) => {
152-
const d = (a, b) => Math.hypot(a.x - b.x, a.y - b.y);
153-
return d(l[8], r[8]) < 0.12;
154-
};
155-
156-
const findNearestShape = (position) => {
157-
let minDist = Infinity;
158-
let closest = null;
159-
shapes.forEach(shape => {
160-
const dist = shape.position.distanceTo(position);
161-
if (dist < 1.5 && dist < minDist) {
162-
minDist = dist;
163-
closest = shape;
164-
}
165-
});
166-
return closest;
167-
};
168-
169-
const isInRecycleBinZone = (position) => {
170-
const vector = position.clone().project(camera);
171-
const screenX = ((vector.x + 1) / 2) * window.innerWidth;
172-
const screenY = ((-vector.y + 1) / 2) * window.innerHeight;
17327

174-
const binWidth = 160;
175-
const binHeight = 160;
176-
const binLeft = window.innerWidth - 60 - binWidth;
177-
const binTop = window.innerHeight - 60 - binHeight;
178-
const binRight = binLeft + binWidth;
179-
const binBottom = binTop + binHeight;
180-
181-
const adjustedX = window.innerWidth - screenX;
182-
183-
return adjustedX >= binLeft && adjustedX <= binRight && screenY >= binTop && screenY <= binBottom;
184-
};
185-
186-
const hands = new Hands({ locateFile: file => `https://cdn.jsdelivr.net/npm/@mediapipe/hands/${file}` });
187-
hands.setOptions({ maxNumHands: 2, modelComplexity: 1, minDetectionConfidence: 0.7, minTrackingConfidence: 0.7 });
188-
189-
hands.onResults(results => {
190-
ctx.clearRect(0, 0, canvas.width, canvas.height);
191-
const recycleBin = document.getElementById('recycle-bin');
192-
193-
for (const landmarks of results.multiHandLandmarks) {
194-
const drawCircle = (landmark) => {
195-
ctx.beginPath();
196-
ctx.arc(landmark.x * canvas.width, landmark.y * canvas.height, 10, 0, 2 * Math.PI);
197-
ctx.fillStyle = 'rgba(0, 255, 255, 0.7)';
198-
ctx.fill();
199-
};
200-
drawCircle(landmarks[4]); // Thumb tip
201-
drawCircle(landmarks[8]); // Index tip
202-
}
203-
204-
// Existing shape interaction and gesture logic...
205-
if (results.multiHandLandmarks.length === 2) {
206-
const [l, r] = results.multiHandLandmarks;
207-
const leftPinch = isPinch(l);
208-
const rightPinch = isPinch(r);
209-
const indexesClose = areIndexFingersClose(l, r);
210-
211-
if (leftPinch && rightPinch) {
212-
const left = l[8];
213-
const right = r[8];
214-
const centerX = (left.x + right.x) / 2;
215-
const centerY = (left.y + right.y) / 2;
216-
const distance = Math.hypot(left.x - right.x, left.y - right.y);
217-
218-
if (!isPinching) {
219-
const now = Date.now();
220-
if (!shapeCreatedThisPinch && indexesClose && now - lastShapeCreationTime > shapeCreationCooldown) {
221-
currentShape = createRandomShape(get3DCoords(centerX, centerY));
222-
lastShapeCreationTime = now;
223-
shapeCreatedThisPinch = true;
224-
originalDistance = distance;
225-
}
226-
} else if (currentShape && originalDistance) {
227-
shapeScale = distance / originalDistance;
228-
currentShape.scale.set(shapeScale, shapeScale, shapeScale);
229-
}
230-
isPinching = true;
231-
recycleBin.classList.remove('active');
232-
return;
233-
}
234-
}
235-
236-
isPinching = false;
237-
shapeCreatedThisPinch = false;
238-
originalDistance = null;
239-
currentShape = null;
240-
241-
if (results.multiHandLandmarks.length > 0) {
242-
for (const landmarks of results.multiHandLandmarks) {
243-
const indexTip = landmarks[8];
244-
const position = get3DCoords(indexTip.x, indexTip.y);
245-
246-
if (isPinch(landmarks)) {
247-
if (!selectedShape) {
248-
selectedShape = findNearestShape(position);
249-
}
250-
if (selectedShape) {
251-
selectedShape.position.copy(position);
252-
253-
const inBin = isInRecycleBinZone(selectedShape.position);
254-
selectedShape.children.forEach(child => {
255-
if (child.material && child.material.wireframe) {
256-
child.material.color.set(inBin ? 0xff0000 : 0xffffff);
257-
}
258-
});
259-
if (inBin) {
260-
recycleBin.classList.add('active');
261-
} else {
262-
recycleBin.classList.remove('active');
263-
}
264-
}
265-
} else {
266-
if (selectedShape && isInRecycleBinZone(selectedShape.position)) {
267-
scene.remove(selectedShape);
268-
shapes = shapes.filter(s => s !== selectedShape);
269-
}
270-
selectedShape = null;
271-
recycleBin.classList.remove('active');
272-
}
273-
}
274-
} else {
275-
if (selectedShape && isInRecycleBinZone(selectedShape.position)) {
276-
scene.remove(selectedShape);
277-
shapes = shapes.filter(s => s !== selectedShape);
278-
}
279-
selectedShape = null;
280-
recycleBin.classList.remove('active');
281-
}
282-
});
28+
</body>
28329

284-
const initCamera = async () => {
285-
const stream = await navigator.mediaDevices.getUserMedia({ video: { width: 1280, height: 720 } });
286-
video.srcObject = stream;
287-
await new Promise(resolve => video.onloadedmetadata = resolve);
288-
canvas.width = video.videoWidth;
289-
canvas.height = video.videoHeight;
290-
new Camera(video, {
291-
onFrame: async () => await hands.send({ image: video }),
292-
width: video.videoWidth,
293-
height: video.videoHeight
294-
}).start();
295-
};
30+
<script src="main.js"></script>
29631

297-
initThree();
298-
initCamera();
299-
</script>
300-
</body>
30132
</html>

0 commit comments

Comments
 (0)