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 >
0 commit comments