Skip to content

Commit 388531d

Browse files
Add support for firefox
1 parent b6603af commit 388531d

File tree

1 file changed

+170
-112
lines changed

1 file changed

+170
-112
lines changed

game.js

+170-112
Original file line numberDiff line numberDiff line change
@@ -141,128 +141,186 @@ async function setupHandTracking() {
141141
let lastHandPosition = null;
142142
let noHandFrames = 0;
143143
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);
145145
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+
148157
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);
153179

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+
});
222221

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+
}
228227
}
229228

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+
}
251237
});
252238

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+
254291
try {
255-
await camera.start();
292+
const success = await startCamera();
293+
if (!success) {
294+
throw new Error('Failed to initialize camera');
295+
}
256296
} 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);
266324
}
267325
}
268326

0 commit comments

Comments
 (0)