UNPKG

auraehealth-facescan

Version:

Face scanning package

827 lines (743 loc) 28.5 kB
// facescan.js window.initializeFaceScan = function (config) { const videoElement = config.videoElement; const canvasElement = config.canvasElement; const canvasCtx = canvasElement.getContext('2d'); const colorDisplay = config.colorDisplayElement; const errorMessageElement = config.errorMessageElement; const progressBox1 = config.progressBox1; const progressBox2 = config.progressBox2; const startScanButton = config.startScanButton; const backToHomeButton = config.backButton; const progressCircle = config.progressCircleElement; const progressValue = config.progressValueElement; const scanProgressMessage = config.scanProgressMessageElement; const container = config.container; const EYE_AR_THRESH = 0.35; // Try a higher threshold const EYE_AR_CONSEC_FRAMES = 2; // Check more consecutive frames const BLINK_TIMEOUT = 10000; const BLINK_COOLDOWN = 500; let faceMesh, camera; let scanPaused = false; let analysisActive = false; let rgbValues = { r: [], g: [], b: [] }; let frameCount = 0; let startTime; let capturedFrame = null; let fpsStartTime = null; let currentFacingMode = 'user'; let blinkDetected = false; let lastBlinkTime = 0; let eyeStates = []; let progressInterval = null; const switchCameraButton = document.createElement('button'); switchCameraButton.className = 'camera-switch-button'; switchCameraButton.innerHTML = ` <svg width="56" height="56" viewBox="0 0 56 56" fill="none" xmlns="http://www.w3.org/2000/svg"> <circle cx="28" cy="28" r="28" fill="#4F4D4D" fill-opacity="0.83"/> <path d="M21.3852 14.2882C22.0978 11.2588 24.8688 9 28.1786 9C31.4884 9 34.2594 11.2588 34.9719 14.2882L35.2696 14.3489C39.8225 15.2786 43.4481 18.6476 44.6347 23.0513C45.598 26.626 45.598 30.3843 44.6347 33.959C43.4481 38.3627 39.8225 41.7318 35.2696 42.6614L34.3997 42.839C30.2964 43.6769 26.0608 43.6769 21.9575 42.839L21.0875 42.6614C16.5347 41.7318 12.9091 38.3627 11.7225 33.959C10.7592 30.3843 10.7592 26.626 11.7225 23.0513C12.9091 18.6476 16.5347 15.2786 21.0875 14.3489L21.3852 14.2882Z" stroke="#E0DFDF" stroke-width="1.5" stroke-linejoin="round"/> <path d="M25.7636 26.8114H22.6199C22.4809 26.8114 22.3476 26.7562 22.2494 26.658C22.1511 26.5597 22.0959 26.4264 22.0959 26.2875V23.1437C22.0959 23.0047 22.1511 22.8714 22.2494 22.7732C22.3476 22.6749 22.4809 22.6197 22.6199 22.6197C22.7588 22.6197 22.8921 22.6749 22.9904 22.7732C23.0886 22.8714 23.1438 23.0047 23.1438 23.1437V25.0227L24.102 24.0645C25.2706 22.8901 26.8577 22.2276 28.5145 22.2228H28.5492C30.1919 22.2186 31.7701 22.8617 32.942 24.0128C33.0377 24.1107 33.0912 24.2421 33.0912 24.379C33.0913 24.5159 33.0377 24.6474 32.942 24.7453C32.8463 24.8432 32.7162 24.8997 32.5793 24.9029C32.4424 24.906 32.3098 24.8555 32.2097 24.7621C31.2329 23.8033 29.9179 23.2675 28.5492 23.2707H28.5197C27.1392 23.275 25.8167 23.8269 24.8428 24.8053L23.8846 25.7635H25.7636C25.9026 25.7635 26.0359 25.8187 26.1341 25.917C26.2324 26.0152 26.2876 26.1485 26.2876 26.2875C26.2876 26.4264 26.2324 26.5597 26.1341 26.658C26.0359 26.7562 25.9026 26.8114 25.7636 26.8114ZM34.1471 29.9552H31.0033C30.8643 29.9552 30.7311 30.0104 30.6328 30.1087C30.5345 30.207 30.4793 30.3402 30.4793 30.4792C30.4793 30.6182 30.5345 30.7514 30.6328 30.8497C30.7311 30.948 30.8643 31.0032 31.0033 31.0032H32.8824L31.9242 31.9614C30.9504 32.9396 29.6282 33.4915 28.2479 33.4959H28.2184C26.8497 33.4991 25.5347 32.9634 24.5579 32.0046C24.5091 31.9547 24.4508 31.915 24.3864 31.8879C24.3221 31.8608 24.253 31.8469 24.1831 31.8469C24.1133 31.8469 24.0442 31.8608 23.9799 31.8879C23.9155 31.915 23.8572 31.9547 23.8084 32.0046C23.7596 32.0546 23.7213 32.1137 23.6957 32.1787C23.6701 32.2436 23.6577 32.3131 23.6593 32.3829C23.6609 32.4527 23.6765 32.5214 23.705 32.5851C23.7336 32.6489 23.7746 32.7062 23.8256 32.7539C24.9975 33.905 26.5757 34.5481 28.2184 34.5439H28.2525C29.909 34.5388 31.4959 33.8764 32.6643 32.7021L33.6231 31.7439V33.623C33.6231 33.7619 33.6783 33.8952 33.7766 33.9935C33.8749 34.0917 34.0081 34.147 34.1471 34.147C34.2861 34.147 34.4193 34.0917 34.5176 33.9935C34.6159 33.8952 34.6711 33.7619 34.6711 33.623V30.4792C34.6711 30.3402 34.6159 30.207 34.5176 30.1087C34.4193 30.0104 34.2861 29.9552 34.1471 29.9552Z" fill="#FCF0F0"/> </svg> `; const CONFIG = { requiredFields: ['uid', 'email', 'name', 'gender'], numberFields: ['waist', 'height', 'weight', 'age'], }; if (container) { container.appendChild(switchCameraButton); } // Get and validate user data from URL function validateAndGetUserData() { const urlParams = new URLSearchParams(window.location.search); const userData = { waist: Number(urlParams.get('waist')) || null, height: Number(urlParams.get('height')) || null, weight: Number(urlParams.get('weight')) || null, age: Number(urlParams.get('age')) || null, email: urlParams.get('email') || null, name: urlParams.get('name') || null, gender: urlParams.get('gender') || null, uid: urlParams.get('uid') || null, }; // Check required fields const missingFields = CONFIG.requiredFields.filter( (field) => !userData[field] ); if (missingFields.length > 0) { throw new Error( `Missing required parameters: ${missingFields.join(', ')}` ); } // Validate number fields (if they are provided) CONFIG.numberFields.forEach((field) => { const value = urlParams.get(field); if (value && isNaN(Number(value))) { throw new Error(`Invalid value for ${field}: must be a number`); } }); return userData; } // Initialize application with validated user data function initializeApp() { try { // Validate and get user data const userData = validateAndGetUserData(); // Destructure user data into global variables ({ waist: userWaist, height: userHeight, weight: userWeight, age: userAge, email: userEmail, name: userName, gender: userGender, uid: userUid, } = userData); console.log(`User data: ${userData}`); // Initialize face mesh and camera showLoadingIndicator(); initializeFaceMesh(); initializeCamera() .then(() => { setTimeout(() => { hideLoadingIndicator(); document.getElementsByClassName('progress-box-1')[0].style.display = 'block'; }, 2000); }) .catch((error) => { console.error('Error initializing camera:'); hideLoadingIndicator(); showErrorDialog('Failed to start camera: ' + error.message); }); } catch (error) { console.error('Initialization error:', error); showErrorDialog(error.message); // Optionally disable scan functionality if (startScanButton) { startScanButton.disabled = true; } } } function sendMessageToFlutter(message) { window.parent.postMessage(message, '*'); } function showErrorDialog(message) { const errorDialog = document.getElementById('error-dialog'); const errorDialogMessage = document.getElementById('error-dialog-message'); errorDialogMessage.textContent = message; errorDialog.style.display = 'block'; // Add a class to identify auto-closeable errors if (message.includes('closer') || message.includes('straight')) { errorDialog.classList.add('auto-closeable'); } else { errorDialog.classList.remove('auto-closeable'); } } function closeErrorDialog() { const errorDialog = document.getElementById('error-dialog'); errorDialog.style.display = 'none'; // If this was an auto-closeable error and scan was active, continue if (errorDialog.classList.contains('auto-closeable') && analysisActive) { errorDialog.classList.remove('auto-closeable'); } } function updateVideoTransform() { // Always mirror for front camera, never for back camera if (currentFacingMode === 'user') { videoElement.style.transform = 'scaleX(-1)'; canvasElement.style.transform = 'scaleX(-1)'; } else { videoElement.style.transform = 'none'; canvasElement.style.transform = 'none'; } } // Initialize face mesh function initializeFaceMesh() { try { faceMesh = new FaceMesh({ locateFile: (file) => { return `https://cdn.jsdelivr.net/npm/@mediapipe/face_mesh/${file}`; }, }); faceMesh.setOptions({ maxNumFaces: 1, refineLandmarks: true, minDetectionConfidence: 0.5, minTrackingConfidence: 0.5, }); faceMesh.onResults(onResults); } catch (error) { console.error('Error initializing FaceMesh:'); } } function hideLoadingIndicator() { document.getElementById('loading-indicator').style.display = 'none'; document.querySelector('body').style.overflow = 'auto'; } function showLoadingIndicator() { document.getElementById('loading-indicator').style.display = 'block'; document.querySelector('body').style.overflow = 'hidden'; } function showNetworkErrorOverlay() { document.getElementById('network-error-overlay').style.display = 'flex'; } // Function to hide network error overlay function hideNetworkErrorOverlay() { document.getElementById('network-error-overlay').style.display = 'none'; } // Add event listener to the "Back to Home" button in the error overlay document.addEventListener('DOMContentLoaded', function () { document .getElementById('network-error-home-button') .addEventListener('click', function () { hideNetworkErrorOverlay(); sendMessageToFlutter('backToHomePage'); }); }); // Function to capture, compress, and convert frame to Base64 function captureCompressAndEncode(image, quality = 0.5) { return new Promise((resolve) => { const canvas = document.createElement('canvas'); canvas.width = image.width; canvas.height = image.height; const ctx = canvas.getContext('2d'); ctx.drawImage(image, 0, 0, canvas.width, canvas.height); // Compress and convert to Base64 canvas.toBlob( (blob) => { const reader = new FileReader(); reader.onloadend = () => resolve(reader.result); reader.readAsDataURL(blob); }, 'image/jpeg', quality ); }); } function getDistance(p1, p2, landmarks) { const dx = landmarks[p1].x - landmarks[p2].x; const dy = landmarks[p1].y - landmarks[p2].y; const dz = landmarks[p1].z - landmarks[p2].z; return Math.sqrt(dx * dx + dy * dy + dz * dz); } function getEyeAspectRatio(landmarks, eyeIndices) { try { // Vertical distances const v1 = getDistance(eyeIndices[1], eyeIndices[3], landmarks); const v2 = getDistance(eyeIndices[4], eyeIndices[5], landmarks); // Horizontal distance const h = getDistance(eyeIndices[0], eyeIndices[2], landmarks); const ear = h !== 0 ? (v1 + v2) / (2.0 * h) : 0; return ear; } catch (error) { console.error('Error calculating EAR:', error); return 0; } } const LEFT_EYE = [ 33, // left corner 159, // top 133, // right corner 145, // bottom 158, // outer top 144, // outer bottom ]; const RIGHT_EYE = [ 362, // left corner 386, // top 263, // right corner 374, // bottom 385, // outer top 380, // outer bottom ]; // Function to detect blinks function detectBlink(landmarks) { try { const leftEAR = getEyeAspectRatio(landmarks, LEFT_EYE); const rightEAR = getEyeAspectRatio(landmarks, RIGHT_EYE); const ear = (leftEAR + rightEAR) / 2.0; // Check if eyes are closed const isClosed = ear < EYE_AR_THRESH; eyeStates.push(isClosed); if (eyeStates.length > EYE_AR_CONSEC_FRAMES) { eyeStates.shift(); } // Modified blink detection pattern if (eyeStates.length === EYE_AR_CONSEC_FRAMES && !blinkDetected) { // Look for any closed state in recent frames const hasClosed = eyeStates.some((state) => state); if (hasClosed && Date.now() - lastBlinkTime > BLINK_COOLDOWN) { blinkDetected = true; lastBlinkTime = Date.now(); return true; } } return false; } catch (error) { console.error('Error in detectBlink:', error); return false; } } function resetScan() { analysisActive = false; scanPaused = false; switchCameraButton.style.display = 'flex'; clearCanvas(); if (progressInterval) { clearInterval(progressInterval); } progressInterval = null; progressValue.textContent = '0%'; progressCircle.style.background = `conic-gradient(#ccc 0deg, #ccc 360deg)`; startScanButton.textContent = 'Start Scan'; scanProgressMessage.style.display = 'none'; progressBox1.style.display = 'flex'; progressBox2.style.display = 'none'; } function handleInvalidDistance() { showErrorDialog('Please keep the camera closer for better scanning.'); scanPaused = true; clearCanvas(); } function calculateDistanceFromCamera(landmarks) { const leftEyeCorner = landmarks[33]; const rightEyeCorner = landmarks[263]; const faceWidth = Math.sqrt( Math.pow(rightEyeCorner.x - leftEyeCorner.x, 2) + Math.pow(rightEyeCorner.y - leftEyeCorner.y, 2) ); return (0.1 / faceWidth) * 100; // Adjust scale factor if needed } function checkFaceOrientation(landmarks) { // Use nose tip and both cheeks for orientation check const noseTip = landmarks[1]; const leftCheek = landmarks[234]; const rightCheek = landmarks[454]; // Calculate distances from nose to each cheek const leftCheekDist = Math.abs(noseTip.x - leftCheek.x); const rightCheekDist = Math.abs(rightCheek.x - noseTip.x); // Calculate the difference ratio const distanceRatio = Math.abs(leftCheekDist - rightCheekDist) / ((leftCheekDist + rightCheekDist) / 2); // Lower threshold means more sensitive detection const orientationThreshold = 0.5; return distanceRatio <= orientationThreshold; } function handleFaceNotStraight() { showErrorDialog('Please keep your face straight for better scanning.'); scanPaused = true; clearCanvas(); } function isLaptop() { const userAgent = navigator.userAgent.toLowerCase(); const isMobile = /mobile|android|iphone|ipad|tablet/.test(userAgent); return !isMobile; // If not a mobile device, assume it's a laptop/desktop } function onResults(results) { if (!analysisActive) return; canvasCtx.save(); canvasCtx.clearRect(0, 0, canvasElement.width, canvasElement.height); // Always draw the camera feed canvasCtx.drawImage( results.image, 0, 0, canvasElement.width, canvasElement.height ); if (results.multiFaceLandmarks.length === 0) { showErrorDialog('No face detected.\nPlace your face in front of camera.'); scanPaused = true; canvasCtx.restore(); return; } // Get landmarks and check distance const landmarks = results.multiFaceLandmarks[0]; // Check face orientation first if (!checkFaceOrientation(landmarks)) { handleFaceNotStraight(); canvasCtx.restore(); return; } // Then check distance const distance = calculateDistanceFromCamera(landmarks); const distanceThreshold = isLaptop() ? 92 : 30; // Check distance before proceeding if (distance > distanceThreshold) { handleInvalidDistance(); canvasCtx.restore(); return; } // If we get here, both face orientation and distance are okay if (scanPaused) { scanPaused = false; closeErrorDialog(); } // Process frames and draw facemesh only if not paused if (!scanPaused) { // Blink detection const blinkOccurred = detectBlink(landmarks); // Draw facemesh drawConnectors(canvasCtx, landmarks, FACEMESH_TESSELATION, { color: '#C0C0C070', lineWidth: 1, }); drawConnectors(canvasCtx, landmarks, FACEMESH_RIGHT_EYE, { color: '#E0E0E0', lineWidth: 1, }); drawConnectors(canvasCtx, landmarks, FACEMESH_RIGHT_EYEBROW, { color: '#E0E0E0', lineWidth: 1, }); drawConnectors(canvasCtx, landmarks, FACEMESH_RIGHT_IRIS, { color: '#E0E0E0', lineWidth: 1, }); drawConnectors(canvasCtx, landmarks, FACEMESH_LEFT_EYE, { color: '#E0E0E0', lineWidth: 1, }); drawConnectors(canvasCtx, landmarks, FACEMESH_LEFT_EYEBROW, { color: '#E0E0E0', lineWidth: 1, }); drawConnectors(canvasCtx, landmarks, FACEMESH_LEFT_IRIS, { color: '#E0E0E0', lineWidth: 1, }); drawConnectors(canvasCtx, landmarks, FACEMESH_FACE_OVAL, { color: '#E0E0E0', }); drawConnectors(canvasCtx, landmarks, FACEMESH_LIPS, { color: '#E0E0E0', lineWidth: 1, }); // Process frames if appropriate if (frameCount >= 100 && frameCount <= 600) { const color = processFrame(results.image, landmarks); if (color) { rgbValues.r.push(color[0]); rgbValues.g.push(color[1]); rgbValues.b.push(color[2]); } } // Capture frame if needed if (frameCount === 101 && !capturedFrame) { captureCompressAndEncode(results.image).then((base64Image) => { capturedFrame = base64Image.replace('data:image/jpeg;base64,', ''); }); } frameCount++; } canvasCtx.restore(); } function processFrame(image, landmarks) { const regions = { forehead: [10, 108, 151, 9, 336, 337, 338, 339, 340], rightCheek: [36, 31, 39, 0, 267, 269, 270, 409], leftCheek: [266, 261, 269, 230, 37, 39, 40, 185], }; const allRegionsColors = []; for (const [regionName, indices] of Object.entries(regions)) { const regionPoints = indices.map((index) => ({ x: landmarks[index].x * image.width, y: landmarks[index].y * image.height, })); const minX = Math.min(...regionPoints.map((p) => p.x)); const maxX = Math.max(...regionPoints.map((p) => p.x)); const minY = Math.min(...regionPoints.map((p) => p.y)); const maxY = Math.max(...regionPoints.map((p) => p.y)); const tempCanvas = document.createElement('canvas'); tempCanvas.width = maxX - minX; tempCanvas.height = maxY - minY; const tempCtx = tempCanvas.getContext('2d'); tempCtx.drawImage( image, minX, minY, tempCanvas.width, tempCanvas.height, 0, 0, tempCanvas.width, tempCanvas.height ); const imageData = tempCtx.getImageData( 0, 0, tempCanvas.width, tempCanvas.height ); const data = imageData.data; let sumR = 0, sumG = 0, sumB = 0, count = 0; for (let i = 0; i < data.length; i += 4) { sumR += data[i]; sumG += data[i + 1]; sumB += data[i + 2]; count++; } if (count > 0) { allRegionsColors.push([sumR / count, sumG / count, sumB / count]); } } if (allRegionsColors.length > 0) { const avgColor = allRegionsColors.reduce( (acc, color) => [ acc[0] + color[0] / allRegionsColors.length, acc[1] + color[1] / allRegionsColors.length, acc[2] + color[2] / allRegionsColors.length, ], [0, 0, 0] ); return avgColor.map(Math.round); } return null; } function startScan() { if (analysisActive) { // Cancel scan analysisActive = false; scanPaused = false; blinkDetected = false; lastBlinkTime = 0; eyeStates = []; updateVideoTransform(); switchCameraButton.style.display = 'flex'; clearCanvas(); if (progressInterval) { clearInterval(progressInterval); } // Reset the progress indicator progressValue.textContent = '0%'; progressCircle.style.background = `conic-gradient(#ccc 0deg, #ccc 360deg)`; startScanButton.textContent = 'Start Scan'; scanProgressMessage.style.display = 'none'; progressBox1.style.display = 'flex'; progressBox2.style.display = 'none'; } else { // Start scan initValues(); scanPaused = false; analysisActive = true; blinkDetected = false; lastBlinkTime = 0; updateVideoTransform(); eyeStates = []; startTime = Date.now(); frameCount = 0; fpsStartTime = Date.now(); capturedFrame = null; rgbValues = { r: [], g: [], b: [] }; scanProgressMessage.style.display = 'block'; progressBox2.style.display = 'none'; startScanButton.textContent = 'Cancel'; switchCameraButton.style.display = 'none'; progressInterval = animateProgressBar(); } } function initValues() { frameCount = 0; rgbValues = { r: [], g: [], b: [] }; analysisActive = true; startTime = Date.now(); frameCount = 0; fpsStartTime = Date.now(); capturedFrame = null; rgbValues = { r: [], g: [], b: [] }; scanProgressMessage.style.display = 'block'; progressBox2.style.display = 'none'; } function resetValues() { frameCount = 0; rgbValues = { r: [], g: [], b: [] }; capturedFrame = null; progressValue.textContent = '0%'; progressCircle.style.background = 'conic-gradient(#ccc 0deg, #ccc 360deg)'; colorDisplay.textContent = ''; scanProgressMessage.textContent = 'Scan in progress...'; } function animateProgressBar() { const totalTime = 30000; const interval = 100; let progress = 0; let pauseStartTime = null; let totalPausedTime = 0; return setInterval(() => { // If scan is paused, store the pause start time if (scanPaused && !pauseStartTime) { pauseStartTime = Date.now(); return; } // If scan is resumed, add the paused duration to total paused time if (!scanPaused && pauseStartTime) { totalPausedTime += Date.now() - pauseStartTime; pauseStartTime = null; } if (!analysisActive || progress >= 100) { clearInterval(progressInterval); canvasCtx.clearRect(0, 0, canvasElement.width, canvasElement.height); return; } // Calculate progress accounting for paused time const currentTime = Date.now(); const effectiveTime = currentTime - startTime - totalPausedTime; progress = Math.min((effectiveTime / totalTime) * 100, 100); // Update UI only if not paused if (!scanPaused) { progressValue.textContent = `${Math.floor(progress)}%`; progressCircle.style.background = `conic-gradient( #4caf50 ${progress * 3.6}deg, #ccc ${progress * 3.6}deg )`; } if (effectiveTime >= totalTime) { analysisActive = false; checkCapturedFrameAndCallApi(); canvasCtx.clearRect(0, 0, canvasElement.width, canvasElement.height); } }, interval); } function checkCapturedFrameAndCallApi() { if (capturedFrame != null) { callAPI(); switchCameraButton.style.display = 'flex'; } else { //keep calling after every 2 seconds setTimeout(checkCapturedFrameAndCallApi, 2000); } } function checkImage() { if (capturedFrame != null) { callAPI(); } } function callAPI() { const elapsedTime = (Date.now() - fpsStartTime) / 1000; const fps = frameCount / elapsedTime; const apiUrl = 'https://ok67gldbh5.execute-api.ap-south-1.amazonaws.com/prod/process-rppg'; const data = { redChannel: rgbValues.r, greenChannel: rgbValues.g, blueChannel: rgbValues.b, image: capturedFrame, metadata: { fps: Math.round(fps), user_id: userUid, gender: userGender, email: userEmail, fullname: userName, height: userHeight, weight: userWeight, waist: userWaist, age: userAge, }, }; console.log('data--'); console.log({ data }); fetch(apiUrl, { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify(data), }) .then((response) => response.json()) .then((data) => { console.log('API Response:', data); if (camera) { camera .stop() .then(() => console.log('Camera stopped successfully after scan')) .catch((err) => console.error('Error stopping camera:', err)); } switchProgressBoxes(); sendMessageToFlutter('DONE'); }) .catch((error) => { console.error('Error calling API:', error); const errorDialog = document.getElementById('error-dialog'); if (errorDialog) { errorDialog.style.display = 'none'; } // Show network error overlay instead of the error dialog showNetworkErrorOverlay(); // Reset scan state analysisActive = false; scanPaused = false; if (progressInterval) { clearInterval(progressInterval); } switchCameraButton.style.display = 'flex'; startScanButton.textContent = 'Start Scan'; // Reset UI elements progressBox1.style.display = 'flex'; progressBox2.style.display = 'none'; }); } function initializeCamera() { return new Promise((resolve, reject) => { camera = new Camera(videoElement, { onFrame: async () => { await faceMesh.send({ image: videoElement }); }, width: 1280, height: 720, facingMode: currentFacingMode, }); camera .start() .then(() => { updateVideoTransform(); // Set initial transform resolve(); }) .catch((error) => { console.error('Error starting camera:', error); reject(error); showErrorDialog('Failed to start camera: Try updating your Browser'); }); }); } async function switchCamera() { if (camera) { await camera.stop(); } currentFacingMode = currentFacingMode === 'user' ? 'environment' : 'user'; updateVideoTransform(); // Update transform when switching cameras try { await initializeCamera(); } catch (error) { console.error('Error switching camera:', error); showErrorDialog('Failed to switch camera: ' + error.message); } } // Add click event listener for the switch camera button switchCameraButton.addEventListener('click', switchCamera); function switchProgressBoxes() { progressBox1.style.display = 'none'; progressBox2.style.display = 'flex'; } startScanButton.addEventListener('click', startScan); backToHomeButton.addEventListener('click', backToHome); function backToHome() { sendMessageToFlutter('backToHomePage'); } function clearCanvas() { canvasCtx.clearRect(0, 0, canvasElement.width, canvasElement.height); } // Clear the canvas and reset to normal view when canceling or finishing the scan function stopFaceMeshProcessing() { analysisActive = false; clearCanvas(); } document .getElementById('error-dialog-close') .addEventListener('click', closeErrorDialog); initializeApp() };