Skip to content

Commit e8a809c

Browse files
committed
first commit
0 parents  commit e8a809c

2 files changed

Lines changed: 288 additions & 0 deletions

File tree

index.html

Lines changed: 288 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,288 @@
1+
<!DOCTYPE html>
2+
<html lang="en">
3+
<head>
4+
<meta charset="UTF-8" />
5+
<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+
</style>
39+
</head>
40+
<body>
41+
<video id="webcam" autoplay muted playsinline></video>
42+
<canvas id="canvas"></canvas>
43+
<div id="three-canvas"></div>
44+
<img id="recycle-bin" src="recyclebin.png" alt="Recycle Bin" />
45+
46+
<script src="https://cdn.jsdelivr.net/npm/@mediapipe/hands/hands.js"></script>
47+
<script src="https://cdn.jsdelivr.net/npm/@mediapipe/camera_utils/camera_utils.js"></script>
48+
<script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/r128/three.min.js"></script>
49+
<script>
50+
let video = document.getElementById('webcam');
51+
let canvas = document.getElementById('canvas');
52+
let ctx = canvas.getContext('2d');
53+
let scene, camera, renderer;
54+
let shapes = [];
55+
let currentShape = null;
56+
let isPinching = false;
57+
let shapeScale = 1;
58+
let originalDistance = null;
59+
let selectedShape = null;
60+
let shapeCreatedThisPinch = false;
61+
let lastShapeCreationTime = 0;
62+
const shapeCreationCooldown = 1000;
63+
64+
const initThree = () => {
65+
scene = new THREE.Scene();
66+
camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 1000);
67+
camera.position.z = 5;
68+
renderer = new THREE.WebGLRenderer({ alpha: true });
69+
renderer.setSize(window.innerWidth, window.innerHeight);
70+
document.getElementById('three-canvas').appendChild(renderer.domElement);
71+
const light = new THREE.AmbientLight(0xffffff, 1);
72+
scene.add(light);
73+
animate();
74+
};
75+
76+
const animate = () => {
77+
requestAnimationFrame(animate);
78+
shapes.forEach(shape => {
79+
if (shape !== selectedShape) {
80+
shape.rotation.x += 0.01;
81+
shape.rotation.y += 0.01;
82+
}
83+
});
84+
renderer.render(scene, camera);
85+
};
86+
87+
const neonColors = [0xFF00FF, 0x00FFFF, 0xFF3300, 0x39FF14, 0xFF0099, 0x00FF00, 0xFF6600, 0xFFFF00];
88+
let colorIndex = 0;
89+
90+
const getNextNeonColor = () => {
91+
const color = neonColors[colorIndex];
92+
colorIndex = (colorIndex + 1) % neonColors.length;
93+
return color;
94+
};
95+
96+
const createRandomShape = (position) => {
97+
const geometries = [
98+
new THREE.BoxGeometry(),
99+
new THREE.SphereGeometry(0.5, 32, 32),
100+
new THREE.ConeGeometry(0.5, 1, 32),
101+
new THREE.CylinderGeometry(0.5, 0.5, 1, 32)
102+
];
103+
const geometry = geometries[Math.floor(Math.random() * geometries.length)];
104+
const color = getNextNeonColor();
105+
const group = new THREE.Group();
106+
107+
const material = new THREE.MeshBasicMaterial({ color: color, transparent: true, opacity: 0.5 });
108+
const fillMesh = new THREE.Mesh(geometry, material);
109+
110+
const wireframeMaterial = new THREE.MeshBasicMaterial({ color: 0xffffff, wireframe: true });
111+
const wireframeMesh = new THREE.Mesh(geometry, wireframeMaterial);
112+
113+
group.add(fillMesh);
114+
group.add(wireframeMesh);
115+
group.position.copy(position);
116+
scene.add(group);
117+
118+
shapes.push(group);
119+
return group;
120+
};
121+
122+
const get3DCoords = (normX, normY) => {
123+
const x = (normX - 0.5) * 10;
124+
const y = (0.5 - normY) * 10;
125+
return new THREE.Vector3(x, y, 0);
126+
};
127+
128+
const isPinch = (landmarks) => {
129+
const d = (a, b) => Math.hypot(a.x - b.x, a.y - b.y, a.z - b.z);
130+
return d(landmarks[4], landmarks[8]) < 0.07;
131+
};
132+
133+
const areIndexFingersClose = (l, r) => {
134+
const d = (a, b) => Math.hypot(a.x - b.x, a.y - b.y);
135+
return d(l[8], r[8]) < 0.12;
136+
};
137+
138+
const isFist = (landmarks) => {
139+
const d = (a, b) => Math.hypot(a.x - b.x, a.y - b.y, a.z - b.z);
140+
return d(landmarks[4], landmarks[8]) < 0.05 && d(landmarks[4], landmarks[12]) < 0.07;
141+
};
142+
143+
const findNearestShape = (position) => {
144+
let minDist = Infinity;
145+
let closest = null;
146+
shapes.forEach(shape => {
147+
const dist = shape.position.distanceTo(position);
148+
if (dist < 1.5 && dist < minDist) {
149+
minDist = dist;
150+
closest = shape;
151+
}
152+
});
153+
return closest;
154+
};
155+
156+
const isInRecycleBinZone = (position) => {
157+
const vector = position.clone().project(camera);
158+
const screenX = ((vector.x + 1) / 2) * window.innerWidth;
159+
const screenY = ((-vector.y + 1) / 2) * window.innerHeight;
160+
161+
const binWidth = 160;
162+
const binHeight = 160;
163+
const binLeft = window.innerWidth - 60 - binWidth;
164+
const binTop = window.innerHeight - 60 - binHeight;
165+
const binRight = binLeft + binWidth;
166+
const binBottom = binTop + binHeight;
167+
168+
const adjustedX = window.innerWidth - screenX;
169+
170+
return adjustedX >= binLeft && adjustedX <= binRight && screenY >= binTop && screenY <= binBottom;
171+
};
172+
173+
const hands = new Hands({ locateFile: file => `https://cdn.jsdelivr.net/npm/@mediapipe/hands/${file}` });
174+
hands.setOptions({ maxNumHands: 2, modelComplexity: 1, minDetectionConfidence: 0.7, minTrackingConfidence: 0.7 });
175+
176+
hands.onResults(results => {
177+
ctx.clearRect(0, 0, canvas.width, canvas.height);
178+
const recycleBin = document.getElementById('recycle-bin');
179+
180+
for (const landmarks of results.multiHandLandmarks) {
181+
const drawCircle = (landmark) => {
182+
ctx.beginPath();
183+
ctx.arc(landmark.x * canvas.width, landmark.y * canvas.height, 10, 0, 2 * Math.PI);
184+
ctx.fillStyle = 'rgba(0, 255, 255, 0.7)';
185+
ctx.fill();
186+
};
187+
drawCircle(landmarks[4]); // Thumb tip
188+
drawCircle(landmarks[8]); // Index tip
189+
}
190+
191+
// Existing shape interaction and gesture logic...
192+
if (results.multiHandLandmarks.length === 2) {
193+
const [l, r] = results.multiHandLandmarks;
194+
const leftPinch = isPinch(l);
195+
const rightPinch = isPinch(r);
196+
const indexesClose = areIndexFingersClose(l, r);
197+
198+
if (leftPinch && rightPinch) {
199+
const left = l[8];
200+
const right = r[8];
201+
const centerX = (left.x + right.x) / 2;
202+
const centerY = (left.y + right.y) / 2;
203+
const distance = Math.hypot(left.x - right.x, left.y - right.y);
204+
205+
if (!isPinching) {
206+
const now = Date.now();
207+
if (!shapeCreatedThisPinch && indexesClose && now - lastShapeCreationTime > shapeCreationCooldown) {
208+
currentShape = createRandomShape(get3DCoords(centerX, centerY));
209+
lastShapeCreationTime = now;
210+
shapeCreatedThisPinch = true;
211+
originalDistance = distance;
212+
}
213+
} else if (currentShape && originalDistance) {
214+
shapeScale = distance / originalDistance;
215+
currentShape.scale.set(shapeScale, shapeScale, shapeScale);
216+
}
217+
isPinching = true;
218+
recycleBin.classList.remove('active');
219+
return;
220+
}
221+
}
222+
223+
isPinching = false;
224+
shapeCreatedThisPinch = false;
225+
originalDistance = null;
226+
currentShape = null;
227+
228+
if (results.multiHandLandmarks.length > 0) {
229+
for (const landmarks of results.multiHandLandmarks) {
230+
const indexTip = landmarks[8];
231+
const position = get3DCoords(indexTip.x, indexTip.y);
232+
233+
if (isFist(landmarks)) {
234+
if (!selectedShape) {
235+
selectedShape = findNearestShape(position);
236+
}
237+
if (selectedShape) {
238+
selectedShape.position.copy(position);
239+
240+
const inBin = isInRecycleBinZone(selectedShape.position);
241+
selectedShape.children.forEach(child => {
242+
if (child.material && child.material.wireframe) {
243+
child.material.color.set(inBin ? 0xff0000 : 0xffffff);
244+
}
245+
});
246+
if (inBin) {
247+
recycleBin.classList.add('active');
248+
} else {
249+
recycleBin.classList.remove('active');
250+
}
251+
}
252+
} else {
253+
if (selectedShape && isInRecycleBinZone(selectedShape.position)) {
254+
scene.remove(selectedShape);
255+
shapes = shapes.filter(s => s !== selectedShape);
256+
}
257+
selectedShape = null;
258+
recycleBin.classList.remove('active');
259+
}
260+
}
261+
} else {
262+
if (selectedShape && isInRecycleBinZone(selectedShape.position)) {
263+
scene.remove(selectedShape);
264+
shapes = shapes.filter(s => s !== selectedShape);
265+
}
266+
selectedShape = null;
267+
recycleBin.classList.remove('active');
268+
}
269+
});
270+
271+
const initCamera = async () => {
272+
const stream = await navigator.mediaDevices.getUserMedia({ video: { width: 1280, height: 720 } });
273+
video.srcObject = stream;
274+
await new Promise(resolve => video.onloadedmetadata = resolve);
275+
canvas.width = video.videoWidth;
276+
canvas.height = video.videoHeight;
277+
new Camera(video, {
278+
onFrame: async () => await hands.send({ image: video }),
279+
width: video.videoWidth,
280+
height: video.videoHeight
281+
}).start();
282+
};
283+
284+
initThree();
285+
initCamera();
286+
</script>
287+
</body>
288+
</html>

recyclebin.png

14.6 KB
Loading

0 commit comments

Comments
 (0)