3
3
< head >
4
4
< meta charset ="UTF-8 " />
5
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
- # 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 ">
51
9
</ head >
52
10
< body >
11
+
53
12
< video id ="webcam " autoplay muted playsinline > </ video >
54
13
< canvas id ="canvas "> </ canvas >
55
14
< div id ="three-canvas "> </ div >
56
15
< img id ="recycle-bin " src ="recyclebin.png " alt ="Recycle Bin " />
57
16
< div id ="instructions ">
58
17
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 >
61
20
Move a shape into the recycle bin to delete it
62
21
</ 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 >
63
23
64
24
< script src ="https://cdn.jsdelivr.net/npm/@mediapipe/hands/hands.js "> </ script >
65
25
< script src ="https://cdn.jsdelivr.net/npm/@mediapipe/camera_utils/camera_utils.js "> </ script >
66
26
< 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 ;
173
27
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 >
283
29
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 >
296
31
297
- initThree ( ) ;
298
- initCamera ( ) ;
299
- </ script >
300
- </ body >
301
32
</ html >
0 commit comments