@@ -141,128 +141,186 @@ async function setupHandTracking() {
141
141
let lastHandPosition = null ;
142
142
let noHandFrames = 0 ;
143
143
const NO_HAND_THRESHOLD = 30 ;
144
- let positionBuffer = new Array ( 5 ) . fill ( null ) ; // Buffer for position smoothing
144
+ let positionBuffer = new Array ( 5 ) . fill ( null ) ;
145
145
let lastProcessedTime = 0 ;
146
- const PROCESS_INTERVAL = 1000 / 30 ; // Limit to 30 FPS max
147
-
146
+ const PROCESS_INTERVAL = 1000 / 30 ;
147
+ let videoStream = null ;
148
+ let isProcessingFrame = false ;
149
+
150
+ const isFirefox = navigator . userAgent . toLowerCase ( ) . indexOf ( 'firefox' ) > - 1 ;
151
+
152
+ // Ensure video element is properly configured
153
+ video . autoplay = true ;
154
+ video . playsInline = true ;
155
+ video . muted = true ;
156
+
148
157
async function initializeHandTracking ( ) {
149
- try {
150
- hands = new window . Hands ( {
151
- locateFile : ( file ) => `https://cdn.jsdelivr.net/npm/@mediapipe/hands/${ file } `
152
- } ) ;
158
+ try {
159
+ hands = new window . Hands ( {
160
+ locateFile : ( file ) => `https://cdn.jsdelivr.net/npm/@mediapipe/hands/${ file } `
161
+ } ) ;
162
+
163
+ hands . setOptions ( {
164
+ maxNumHands : 1 ,
165
+ modelComplexity : 0 ,
166
+ minDetectionConfidence : 0.2 ,
167
+ minTrackingConfidence : 0.2 ,
168
+ } ) ;
169
+
170
+ hands . onResults ( ( results ) => {
171
+ const now = performance . now ( ) ;
172
+ if ( now - lastProcessedTime < PROCESS_INTERVAL ) return ;
173
+ lastProcessedTime = now ;
174
+
175
+ if ( results . multiHandLandmarks ?. [ 0 ] ) {
176
+ noHandFrames = 0 ;
177
+ const rawX = results . multiHandLandmarks [ 0 ] [ 0 ] . x ;
178
+ const palmX = 1.4 - ( rawX * 1.8 ) ;
153
179
154
- hands . setOptions ( {
155
- maxNumHands : 1 ,
156
- modelComplexity : 0 , // Keep lowest complexity for speed
157
- minDetectionConfidence : 0.2 , // Lower threshold for faster detection
158
- minTrackingConfidence : 0.2 , // Lower threshold for smoother tracking
159
- } ) ;
160
-
161
- hands . onResults ( ( results ) => {
162
- const now = performance . now ( ) ;
163
-
164
- // Throttle processing to maintain consistent frame rate
165
- if ( now - lastProcessedTime < PROCESS_INTERVAL ) return ;
166
- lastProcessedTime = now ;
167
-
168
- if ( results . multiHandLandmarks ?. [ 0 ] ) {
169
- noHandFrames = 0 ;
170
-
171
- // Calculate hand position with improved sensitivity
172
- const rawX = results . multiHandLandmarks [ 0 ] [ 0 ] . x ;
173
- const palmX = 1.4 - ( rawX * 1.8 ) ;
174
-
175
- // Update position buffer
176
- positionBuffer . shift ( ) ;
177
- positionBuffer . push ( palmX ) ;
178
-
179
- // Calculate smoothed position using weighted average
180
- const weights = [ 0.1 , 0.15 , 0.2 , 0.25 , 0.3 ] ; // More weight to recent positions
181
- let smoothedX = 0 ;
182
- let totalWeight = 0 ;
183
-
184
- for ( let i = 0 ; i < positionBuffer . length ; i ++ ) {
185
- if ( positionBuffer [ i ] !== null ) {
186
- smoothedX += positionBuffer [ i ] * weights [ i ] ;
187
- totalWeight += weights [ i ] ;
188
- }
189
- }
190
-
191
- if ( totalWeight > 0 ) {
192
- smoothedX /= totalWeight ;
193
- lastHandPosition = smoothedX ;
194
-
195
- // Apply exponential smoothing for extra fluidity
196
- const alpha = 0.5 ; // Smoothing factor
197
- const currentPaddleX = ( gameState . paddle . x + gameState . paddle . width / 2 ) / CANVAS_WIDTH ;
198
- smoothedX = ( alpha * smoothedX ) + ( ( 1 - alpha ) * currentPaddleX ) ;
199
-
200
- // Update paddle position with improved bounds checking
201
- const targetX = ( smoothedX * CANVAS_WIDTH ) - ( gameState . paddle . width / 2 ) ;
202
- gameState . paddle . x = Math . max (
203
- - 50 ,
204
- Math . min ( CANVAS_WIDTH - gameState . paddle . width + 50 , targetX )
205
- ) ;
206
-
207
- // Reset video border to show tracking is working
208
- video . style . border = "2px solid #3a4c4e" ;
209
-
210
- if ( ! gameState . gameStarted && gameState . modalDismissed ) {
211
- gameState . gameStarted = true ;
212
- }
213
- }
214
- } else {
215
- noHandFrames ++ ;
216
- if ( noHandFrames > NO_HAND_THRESHOLD ) {
217
- // Visual feedback that tracking is lost
218
- video . style . border = "6px solid rgb(225, 21, 21)" ;
219
- }
220
- }
221
- } ) ;
180
+ positionBuffer . shift ( ) ;
181
+ positionBuffer . push ( palmX ) ;
182
+
183
+ const weights = [ 0.1 , 0.15 , 0.2 , 0.25 , 0.3 ] ;
184
+ let smoothedX = 0 ;
185
+ let totalWeight = 0 ;
186
+
187
+ for ( let i = 0 ; i < positionBuffer . length ; i ++ ) {
188
+ if ( positionBuffer [ i ] !== null ) {
189
+ smoothedX += positionBuffer [ i ] * weights [ i ] ;
190
+ totalWeight += weights [ i ] ;
191
+ }
192
+ }
193
+
194
+ if ( totalWeight > 0 ) {
195
+ smoothedX /= totalWeight ;
196
+ lastHandPosition = smoothedX ;
197
+
198
+ const alpha = 0.5 ;
199
+ const currentPaddleX = ( gameState . paddle . x + gameState . paddle . width / 2 ) / CANVAS_WIDTH ;
200
+ smoothedX = ( alpha * smoothedX ) + ( ( 1 - alpha ) * currentPaddleX ) ;
201
+
202
+ const targetX = ( smoothedX * CANVAS_WIDTH ) - ( gameState . paddle . width / 2 ) ;
203
+ gameState . paddle . x = Math . max (
204
+ - 50 ,
205
+ Math . min ( CANVAS_WIDTH - gameState . paddle . width + 50 , targetX )
206
+ ) ;
207
+
208
+ video . style . border = "2px solid #3a4c4e" ;
209
+
210
+ if ( ! gameState . gameStarted && gameState . modalDismissed ) {
211
+ gameState . gameStarted = true ;
212
+ }
213
+ }
214
+ } else {
215
+ noHandFrames ++ ;
216
+ if ( noHandFrames > NO_HAND_THRESHOLD ) {
217
+ video . style . border = "6px solid rgb(225, 21, 21)" ;
218
+ }
219
+ }
220
+ } ) ;
222
221
223
- return hands ;
224
- } catch ( error ) {
225
- console . error ( 'Error initializing hand tracking:' , error ) ;
226
- return null ;
227
- }
222
+ return hands ;
223
+ } catch ( error ) {
224
+ console . error ( 'Error initializing hand tracking:' , error ) ;
225
+ return null ;
226
+ }
228
227
}
229
228
230
- // Initialize camera with error handling
231
- const camera = new window . Camera ( video , {
232
- onFrame : async ( ) => {
233
- try {
234
- if ( ! hands ) {
235
- hands = await initializeHandTracking ( ) ;
236
- }
237
- if ( hands ) {
238
- await hands . send ( { image : video } ) ;
239
- }
240
- } catch ( error ) {
241
- console . error ( 'Error in camera frame:' , error ) ;
242
- hands = null ; // Reset hands so it will reinitialize
243
- // Try to reinitialize after a short delay
244
- setTimeout ( async ( ) => {
245
- hands = await initializeHandTracking ( ) ;
246
- } , 1000 ) ;
247
- }
248
- } ,
249
- width : 640 ,
250
- height : 480
229
+ // Firefox-friendly constraints
230
+ const getConstraints = ( ) => ( {
231
+ video : {
232
+ width : { min : 320 , ideal : 640 , max : 1280 } ,
233
+ height : { min : 240 , ideal : 480 , max : 720 } ,
234
+ frameRate : { min : 15 , ideal : 30 , max : 60 } ,
235
+ facingMode : "user"
236
+ }
251
237
} ) ;
252
238
253
- // Add error handling for camera start
239
+ async function startCamera ( ) {
240
+ try {
241
+ // Check for permissions first
242
+ const permission = await navigator . permissions . query ( { name : 'camera' } ) ;
243
+ if ( permission . state === 'denied' ) {
244
+ throw new Error ( 'Camera permission denied' ) ;
245
+ }
246
+
247
+ // Get user media stream
248
+ videoStream = await navigator . mediaDevices . getUserMedia ( getConstraints ( ) ) ;
249
+
250
+ // Set up video element
251
+ video . srcObject = videoStream ;
252
+ await new Promise ( ( resolve , reject ) => {
253
+ video . onloadedmetadata = resolve ;
254
+ video . onerror = reject ;
255
+ } ) ;
256
+
257
+ // Wait for video to start playing
258
+ await video . play ( ) ;
259
+
260
+ // Initialize hand tracking
261
+ hands = await initializeHandTracking ( ) ;
262
+
263
+ // Start frame processing loop
264
+ requestAnimationFrame ( processFrame ) ;
265
+
266
+ return true ;
267
+ } catch ( error ) {
268
+ console . error ( 'Error starting camera:' , error ) ;
269
+ throw error ;
270
+ }
271
+ }
272
+
273
+ async function processFrame ( ) {
274
+ if ( ! hands || ! videoStream || isProcessingFrame ) {
275
+ requestAnimationFrame ( processFrame ) ;
276
+ return ;
277
+ }
278
+
279
+ isProcessingFrame = true ;
280
+
281
+ try {
282
+ await hands . send ( { image : video } ) ;
283
+ } catch ( error ) {
284
+ console . error ( 'Error processing frame:' , error ) ;
285
+ }
286
+
287
+ isProcessingFrame = false ;
288
+ requestAnimationFrame ( processFrame ) ;
289
+ }
290
+
254
291
try {
255
- await camera . start ( ) ;
292
+ const success = await startCamera ( ) ;
293
+ if ( ! success ) {
294
+ throw new Error ( 'Failed to initialize camera' ) ;
295
+ }
256
296
} catch ( error ) {
257
- console . error ( 'Error starting camera:' , error ) ;
258
- // Try to restart camera after a delay
259
- setTimeout ( async ( ) => {
260
- try {
261
- await camera . start ( ) ;
262
- } catch ( error ) {
263
- console . error ( 'Failed to restart camera:' , error ) ;
264
- }
265
- } , 2000 ) ;
297
+ console . error ( 'Setup error:' , error ) ;
298
+
299
+ const errorMessage = document . createElement ( 'div' ) ;
300
+ errorMessage . style . position = 'absolute' ;
301
+ errorMessage . style . top = '50%' ;
302
+ errorMessage . style . left = '50%' ;
303
+ errorMessage . style . transform = 'translate(-50%, -50%)' ;
304
+ errorMessage . style . color = 'white' ;
305
+ errorMessage . style . textAlign = 'center' ;
306
+ errorMessage . style . backgroundColor = 'rgba(0,0,0,0.8)' ;
307
+ errorMessage . style . padding = '20px' ;
308
+ errorMessage . style . borderRadius = '10px' ;
309
+ errorMessage . innerHTML = `
310
+ <p style="color: #ff4444; font-size: 18px; margin-bottom: 15px;">Camera Access Error</p>
311
+ <p>Unable to access camera. Please ensure:</p>
312
+ <ul style="text-align: left; margin: 10px 0;">
313
+ <li>Camera permissions are enabled in Firefox settings</li>
314
+ <li>Firefox is up to date</li>
315
+ <li>No other applications are using your camera</li>
316
+ <li>You're using HTTPS if accessing from a web server</li>
317
+ </ul>
318
+ <p style="margin-top: 15px;">
319
+ Try refreshing the page or checking Firefox's camera permissions at:<br>
320
+ about:preferences#privacy (scroll to Camera section)
321
+ </p>
322
+ ` ;
323
+ document . querySelector ( '.game-container' ) . appendChild ( errorMessage ) ;
266
324
}
267
325
}
268
326
0 commit comments