pipwave-ekyc-uikit 3.0.4 → 3.0.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,8 @@
1
+ {
2
+ "permissions": {
3
+ "allow": [
4
+ "Bash(git branch *)",
5
+ "Bash(git push *)"
6
+ ]
7
+ }
8
+ }
package/dist/pw-bundle.js CHANGED
@@ -4033,7 +4033,7 @@ eval("\nvar __awaiter = (this && this.__awaiter) || function (thisArg, _argument
4033
4033
  /***/ (function(__unused_webpack_module, exports, __webpack_require__) {
4034
4034
 
4035
4035
  "use strict";
4036
- eval("\nvar __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {\n function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }\n return new (P || (P = Promise))(function (resolve, reject) {\n function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }\n function rejected(value) { try { step(generator[\"throw\"](value)); } catch (e) { reject(e); } }\n function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }\n step((generator = generator.apply(thisArg, _arguments || [])).next());\n });\n};\nvar __importDefault = (this && this.__importDefault) || function (mod) {\n return (mod && mod.__esModule) ? mod : { \"default\": mod };\n};\nObject.defineProperty(exports, \"__esModule\", ({ value: true }));\nconst face_detection_1 = __importDefault(__webpack_require__(/*! ./face-detection */ \"./src/plugins/face-detection.ts\"));\nconst translations_1 = __webpack_require__(/*! ../translations */ \"./src/translations/index.ts\");\nconst deviceDetection_1 = __webpack_require__(/*! ../helper/deviceDetection */ \"./src/helper/deviceDetection.ts\");\nconst MainPage_1 = __webpack_require__(/*! ../classes/MainPage */ \"./src/classes/MainPage.ts\");\nconst camera_1 = __webpack_require__(/*! ../models/camera */ \"./src/models/camera.ts\");\nconst components_1 = __webpack_require__(/*! ../components/components */ \"./src/components/components.ts\");\nconst baseDialog_1 = __importDefault(__webpack_require__(/*! ../components/baseDialog */ \"./src/components/baseDialog.ts\"));\nconst navigationBar_1 = __importDefault(__webpack_require__(/*! ../components/navigationBar */ \"./src/components/navigationBar.ts\"));\nconst footer_1 = __importDefault(__webpack_require__(/*! ../components/footer */ \"./src/components/footer.ts\"));\nconst imagesHelper_1 = __webpack_require__(/*! ../helper/imagesHelper */ \"./src/helper/imagesHelper.ts\");\nclass FaceCamera {\n constructor(cameraId, mainScreenId = \"\") {\n this.cameraTimeLimit = 90000;\n this.videoElement = null;\n this.canvasElement = null;\n this.overlayElement = null;\n this.titleElement = null;\n this.cameraId = cameraId;\n this.mainScreenId = mainScreenId;\n }\n isMobile() {\n // Use extracted helper function\n return (0, deviceDetection_1.isMobileDevice)();\n }\n captureImage() {\n var _a, _b, _c, _d, _e, _f;\n return __awaiter(this, void 0, void 0, function* () {\n const bitmap = this.videoElement;\n // 1. full image\n let canvas = document.createElement(\"canvas\");\n let context = canvas.getContext(\"2d\");\n canvas.width = bitmap.videoWidth;\n canvas.height = bitmap.videoHeight;\n context.drawImage(bitmap, 0, 0, bitmap.videoWidth, bitmap.videoHeight);\n // 2. cropped image — region corresponds exactly to what the overlay shows\n let cropCanvas = document.createElement(\"canvas\");\n let cropContext = cropCanvas.getContext(\"2d\");\n cropCanvas.width = bitmap.videoWidth;\n cropCanvas.height = bitmap.videoHeight;\n cropContext.drawImage(bitmap, 0, 0, bitmap.videoWidth, bitmap.videoHeight);\n // Scale factor: stream pixels per display pixel\n const scaleX = bitmap.videoWidth / ((_b = (_a = this.videoElement) === null || _a === void 0 ? void 0 : _a.clientWidth) !== null && _b !== void 0 ? _b : bitmap.videoWidth);\n const scaleY = bitmap.videoHeight / ((_d = (_c = this.videoElement) === null || _c === void 0 ? void 0 : _c.clientHeight) !== null && _d !== void 0 ? _d : bitmap.videoHeight);\n // Use overlay's actual on-screen position as the crop region\n const overlayRect = (_e = this.overlayElement) === null || _e === void 0 ? void 0 : _e.getBoundingClientRect();\n const videoRect = (_f = this.videoElement) === null || _f === void 0 ? void 0 : _f.getBoundingClientRect();\n let cropX, cropY, cropW, cropH;\n if (overlayRect && videoRect) {\n cropX = Math.round((overlayRect.left - videoRect.left) * scaleX);\n cropY = Math.round((overlayRect.top - videoRect.top) * scaleY);\n cropW = Math.round(overlayRect.width * scaleX);\n cropH = Math.round(overlayRect.height * scaleY);\n }\n else {\n // Fallback: derive from calculateHeightNWidth\n const { width, height } = this.faceDetectionSdk.calculateHeightNWidth(bitmap.videoWidth, bitmap.videoHeight);\n cropW = width;\n cropH = height;\n cropX = Math.round((bitmap.videoWidth - cropW) / 2);\n cropY = Math.round((bitmap.videoHeight - cropH) / 2);\n }\n // Clamp to stream bounds\n cropX = Math.max(0, cropX);\n cropY = Math.max(0, cropY);\n cropW = Math.min(cropW, bitmap.videoWidth - cropX);\n cropH = Math.min(cropH, bitmap.videoHeight - cropY);\n const imgData = cropContext.getImageData(cropX, cropY, cropW, cropH);\n cropCanvas.width = cropW;\n cropCanvas.height = cropH;\n cropContext.putImageData(imgData, 0, 0);\n // 3. resize the canvas\n try {\n canvas = (0, imagesHelper_1.resizeCanvas)(canvas, imagesHelper_1.maxFullImage.width, imagesHelper_1.maxFullImage.height);\n cropCanvas = (0, imagesHelper_1.resizeCanvas)(cropCanvas, imagesHelper_1.maxCroppedImage.width, imagesHelper_1.maxCroppedImage.height);\n }\n catch (err) {\n console.error(\"face resize image error\", err);\n }\n // 4. construct payload - convert to desired file format\n const payload = {\n fullImage: canvas.toDataURL(\"image/jpeg\"),\n croppedImage: cropCanvas.toDataURL(\"image/jpeg\"),\n };\n if (MainPage_1.enableImageDebug) {\n if (payload.fullImage)\n this.downloadImage(payload.fullImage, `selfie-full-${canvas.width}x${canvas.height}.jpg`);\n if (payload.croppedImage)\n this.downloadImage(payload.croppedImage, `selfie-cropped-${cropCanvas.width}x${cropCanvas.height}.jpg`);\n }\n return payload;\n });\n }\n init() {\n try {\n // show loading spinner in full pg\n (0, components_1.createFullLoading)(this.cameraId, \"Initializing Camera...\");\n // 1. Draw the camera display\n this.drawCameraDisplay();\n // 2. Setup related SDK\n const { videoElement, canvasElement, overlayElement, titleElement } = this;\n this.faceDetectionSdk = new face_detection_1.default({\n cameraId: this.cameraId,\n videoElement,\n canvasElement,\n overlayElement,\n titleElement,\n });\n // 3. Get the camera permission and trigger the selfie detection\n this.cameraInit();\n // 4. Trigger event after 3 seconds of face detection\n this.faceDetectionSdk.setCaptureImage((livenessScore) => __awaiter(this, void 0, void 0, function* () {\n var _a;\n const image = yield this.captureImage();\n const event = new CustomEvent(\"selfieCapture\", {\n detail: { image, livenessScore },\n });\n (_a = this.overlayElement) === null || _a === void 0 ? void 0 : _a.dispatchEvent(event);\n }));\n // 5. Face camera custom events listeners\n const description = document.getElementById(\"pw-camera-description\");\n this.faceDetectionSdk.listenToUserNoSmile(() => {\n if (description) {\n description.innerHTML = (0, translations_1.$t)(\"face_smile_at_camera\");\n }\n });\n this.faceDetectionSdk.listenToUserSmiled(() => {\n if (description) {\n description.innerHTML = (0, translations_1.$t)(\"face_smile_seconds\");\n }\n });\n this.faceDetectionSdk.listenToUserSmiledButNotLiveness(() => {\n if (description) {\n description.innerHTML = (0, translations_1.$t)(\"face_neutral_expression\");\n }\n });\n this.faceDetectionSdk.listenToLookStraight(() => {\n if (description) {\n description.innerHTML = (0, translations_1.$t)(\"face_looks_straight\");\n }\n });\n this.faceDetectionSdk.listenToPlaceFace(() => {\n if (description) {\n description.innerHTML = (0, translations_1.$t)(\"face_within_frame\");\n }\n });\n this.faceDetectionSdk.listenToFaceTooFar(() => {\n if (description) {\n description.innerHTML = (0, translations_1.$t)(\"face_too_far\");\n }\n });\n // 6. Start timer to close camera\n this.timer = setTimeout(() => __awaiter(this, void 0, void 0, function* () {\n this.stopVideoStream();\n // open idle dialog\n const idleDialog = new baseDialog_1.default();\n idleDialog.render(this.mainScreenId, (0, translations_1.$t)(\"idle_message\"), (0, translations_1.$t)(\"btn_okay\"));\n }), this.cameraTimeLimit);\n }\n finally {\n if (!this.faceDetectionSdk ||\n this.faceDetectionSdk.getLoadedModelState() !== \"LOADING\")\n (0, components_1.hideFullLoading)(this.cameraId);\n }\n }\n drawCameraDisplay() {\n const mainBody = document.getElementById(this.cameraId);\n if (!mainBody)\n return;\n // 1. header section\n const header = document.createElement(\"div\");\n header.id = camera_1.ELEMENT_IDS.HEADER;\n // header.classList.add(\"camera-header-area\");\n header.classList.add(\"pw-absolute\", \"pw-z-[2]\", \"pw-w-full\", \"pw-text-white\");\n const navigationDiv = new navigationBar_1.default().render(() => {\n this.stopVideoStream();\n });\n header.appendChild(navigationDiv);\n const titleDiv = document.createElement(\"div\");\n titleDiv.id = \"pw-camera-title\";\n titleDiv.classList.add(\"pw-text-2xl\", \"pw-text-center\", \"pw-mb-4\", \"pw-px-5\");\n const body = document.createElement(\"div\");\n body.id = camera_1.ELEMENT_IDS.BODY;\n body.classList.add(\"camera-body-area\");\n const videoContainer = document.createElement(\"div\");\n videoContainer.id = camera_1.ELEMENT_IDS.VIDEO_CONTAINER;\n videoContainer.classList.add(\"video-container\");\n const overlayContainer = document.createElement(\"div\");\n overlayContainer.id = camera_1.ELEMENT_IDS.OVERLAY_CONTAINER;\n overlayContainer.classList.add(\"overlay-container\");\n const overlayElement = document.createElement(\"div\");\n overlayElement.id = camera_1.ELEMENT_IDS.OVERLAY;\n overlayElement.classList.add(\"overlay-element\");\n overlayElement.classList.add(\"pw-hidden\");\n overlayElement.classList.add(\"pw-relative\");\n // Set Description — full viewport width so text is not constrained by oval\n const descriptionDiv = document.createElement(\"div\");\n descriptionDiv.classList.add(\"pw-text-center\", \"pw-px-5\", \"pw-text-xl\", \"pw-mb-4\", \"pw-absolute\", \"pw-text-white\");\n descriptionDiv.style.zIndex = \"1\";\n descriptionDiv.style.left = \"50%\";\n descriptionDiv.style.marginLeft = \"-50vw\";\n descriptionDiv.style.bottom = \"100%\";\n descriptionDiv.style.transform = \"translateY(-20px)\";\n descriptionDiv.style.width = \"100vw\";\n descriptionDiv.style.boxSizing = \"border-box\";\n descriptionDiv.id = \"pw-camera-description\";\n descriptionDiv.innerHTML = (0, translations_1.$t)(\"face_within_frame\");\n this.descriptionElement = descriptionDiv;\n overlayElement.appendChild(descriptionDiv);\n const canvasElement = document.createElement(\"canvas\");\n canvasElement.id = camera_1.ELEMENT_IDS.CANVAS;\n canvasElement.classList.add(\"video-element\");\n canvasElement.classList.add(\"canvas-element\");\n canvasElement.style.transform = \"scaleX(-1)\";\n const videoElement = document.createElement(\"video\");\n videoElement.id = camera_1.ELEMENT_IDS.VIDEO;\n videoElement.autoplay = true;\n videoElement.playsInline = true;\n videoElement.preload = \"auto\";\n videoElement.loop = true;\n videoElement.classList.add(\"video-element\");\n videoElement.style.transform = \"scaleX(-1)\";\n // green tick icon - inside overlayElement so it aligns with overlay (same fix as card-camera)\n const iconElement = document.createElement(\"span\");\n iconElement.id = \"pw-camera-check-icon\";\n iconElement.classList.add(\"pw-hidden\", \"pw-absolute\", \"pw-inset-0\", \"pw-flex\", \"pw-items-center\", \"pw-justify-center\");\n const checkIcon = document.createElement(\"span\");\n checkIcon.classList.add(\"material-icons\", \"pw-text-green-500\", \"pw-text-4xl\", \"pw-rounded-full\");\n checkIcon.style.fontSize = \"48px\";\n checkIcon.innerHTML = \"check_circle\";\n iconElement.appendChild(checkIcon);\n // 2.1\n this.videoElement = videoElement;\n this.overlayElement = overlayElement;\n this.canvasElement = canvasElement;\n overlayElement.appendChild(iconElement);\n overlayContainer.appendChild(overlayElement);\n videoContainer.appendChild(overlayContainer);\n videoContainer.appendChild(canvasElement);\n videoContainer.appendChild(videoElement);\n body.appendChild(videoContainer);\n // 3. Footer - Powered by pipwave\n const footerWrapper = document.createElement(\"div\");\n footerWrapper.classList.add(\"pw-absolute\", \"pw-bottom-0\", \"pw-left-0\", \"pw-right-0\", \"pw-pb-4\", \"pw-z-[2]\", \"pw-flex\", \"pw-justify-center\", \"pw-items-center\", \"pw-text-white\");\n const footer = new footer_1.default().render(true);\n footerWrapper.appendChild(footer);\n mainBody.appendChild(footerWrapper);\n // 4. append child\n mainBody.classList.add(\"camera-bg\");\n mainBody.appendChild(header);\n mainBody.appendChild(body);\n }\n loadVideoStream(videoElement) {\n const initialConstraints = {\n facingMode: \"user\",\n width: {\n ideal: 1280, // 1280, 1024, 640\n },\n height: {\n ideal: 960, // 960, 768, 480\n },\n video: true,\n };\n // Load the video stream\n const nav = window.navigator.mediaDevices\n ? window.navigator.mediaDevices\n : window.navigator;\n if (!nav)\n return;\n nav\n .getUserMedia({ video: initialConstraints })\n .then((stream) => {\n videoElement.srcObject = stream;\n })\n .catch((err) => {\n console.error(\"Error accessing the camera:\", err);\n });\n }\n cameraInit() {\n var _a;\n if (!this.videoElement)\n return;\n this.loadVideoStream(this.videoElement);\n // video event listeners\n this.videoElement.addEventListener(\"loadedmetadata\", () => __awaiter(this, void 0, void 0, function* () {\n var _b;\n this.faceDetectionSdk.init();\n (_b = this.overlayElement) === null || _b === void 0 ? void 0 : _b.classList.remove(\"pw-hidden\");\n // Set initial video dimensions based on orientation\n this.resizeElement();\n }));\n this.videoElement.addEventListener(\"resize\", () => {\n this.resizeElement();\n });\n window.addEventListener(\"resize\", () => {\n this.resizeElement();\n });\n (_a = screen.orientation) === null || _a === void 0 ? void 0 : _a.addEventListener(\"change\", () => {\n const orientationType = screen.orientation.type;\n if (!orientationType || !this.isMobile)\n return;\n if ([\"landscape-primary\", \"landscape-secondary\"].includes(orientationType)) {\n // TO ADD DIALOG ASK USER TO USE POTRAIT VIEW\n }\n else if ([\"portrait-secondary\", \"portrait-primary\"].includes(orientationType)) {\n // TO BE CONFIRM IF NEED CLOSE DIALOG\n }\n });\n }\n stopVideoStream() {\n if (this.videoElement && this.videoElement.srcObject) {\n const stream = this.videoElement.srcObject;\n const tracks = stream.getTracks();\n tracks.forEach(function (track) {\n track.stop();\n });\n this.videoElement.srcObject = null;\n }\n if (this.timer)\n clearTimeout(this.timer);\n const cameraDiv = document.getElementById(this.cameraId);\n cameraDiv === null || cameraDiv === void 0 ? void 0 : cameraDiv.remove();\n (0, components_1.hideFullLoading)(this.cameraId);\n }\n resizeElement() {\n const videoContainer = document.getElementById(camera_1.ELEMENT_IDS.VIDEO_CONTAINER);\n const videoElement = this.videoElement;\n if (videoElement && videoElement.videoWidth && videoElement.videoHeight) {\n const vpW = window.innerWidth;\n const vpH = window.innerHeight;\n const videoAspect = videoElement.videoWidth / videoElement.videoHeight;\n // Fit the container to the camera's native aspect ratio within the viewport\n let containerW;\n let containerH;\n if (vpW / vpH > videoAspect) {\n // Viewport is wider than video — constrain by height\n containerH = vpH;\n containerW = vpH * videoAspect;\n }\n else {\n // Viewport is taller than video — constrain by width\n containerW = vpW;\n containerH = vpW / videoAspect;\n }\n if (videoContainer) {\n videoContainer.style.width = `${containerW}px`;\n videoContainer.style.height = `${containerH}px`;\n }\n videoElement.style.width = \"100%\";\n videoElement.style.height = \"100%\";\n videoElement.style.minWidth = \"\";\n videoElement.style.minHeight = \"\";\n }\n this.faceDetectionSdk.resizeElement();\n }\n listenToSelfieCapture(cb) {\n var _a;\n (_a = this.overlayElement) === null || _a === void 0 ? void 0 : _a.addEventListener(\"selfieCapture\", (e) => {\n cb(e);\n });\n }\n pauseSmileFace() {\n var _a, _b;\n return __awaiter(this, void 0, void 0, function* () {\n // stop the video\n (_a = this.videoElement) === null || _a === void 0 ? void 0 : _a.pause();\n yield ((_b = this.faceDetectionSdk) === null || _b === void 0 ? void 0 : _b.disableDetection());\n // stop idle timeout\n if (this.timer) {\n clearTimeout(this.timer);\n }\n // clear description\n const description = document.getElementById(\"pw-camera-description\");\n if (description)\n description.innerHTML = \"\";\n // show green tick icon\n const checkIcon = document.getElementById(\"pw-camera-check-icon\");\n checkIcon === null || checkIcon === void 0 ? void 0 : checkIcon.classList.remove(\"pw-hidden\");\n });\n }\n resumeSmileFace() {\n var _a, _b;\n return __awaiter(this, void 0, void 0, function* () {\n // resume the video\n (_a = this.videoElement) === null || _a === void 0 ? void 0 : _a.play();\n yield ((_b = this.faceDetectionSdk) === null || _b === void 0 ? void 0 : _b.startDetection());\n // start idle timeout\n this.timer = setTimeout(() => __awaiter(this, void 0, void 0, function* () {\n this.stopVideoStream();\n // open idle dialog\n const idleDialog = new baseDialog_1.default();\n idleDialog.render(this.mainScreenId, (0, translations_1.$t)(\"idle_message\"), (0, translations_1.$t)(\"btn_okay\"));\n }), this.cameraTimeLimit);\n // hide green tick icon\n const checkIcon = document.getElementById(\"pw-camera-check-icon\");\n checkIcon === null || checkIcon === void 0 ? void 0 : checkIcon.classList.add(\"pw-hidden\");\n // remove green circle\n const overlayElement = document.getElementById(\"overlayElement\");\n if (overlayElement)\n overlayElement.classList.remove(\"overlay-element--active\");\n // close all the background loading\n (0, components_1.hideFullLoading)(this.cameraId);\n });\n }\n downloadImage(dataUrl, filename) {\n const link = document.createElement(\"a\");\n link.href = dataUrl;\n link.download = filename;\n document.body.appendChild(link);\n link.click();\n document.body.removeChild(link);\n }\n}\nexports[\"default\"] = FaceCamera;\n\n\n//# sourceURL=webpack://PWUISDK/./src/plugins/face-camera.ts?");
4036
+ eval("\nvar __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {\n function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }\n return new (P || (P = Promise))(function (resolve, reject) {\n function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }\n function rejected(value) { try { step(generator[\"throw\"](value)); } catch (e) { reject(e); } }\n function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }\n step((generator = generator.apply(thisArg, _arguments || [])).next());\n });\n};\nvar __importDefault = (this && this.__importDefault) || function (mod) {\n return (mod && mod.__esModule) ? mod : { \"default\": mod };\n};\nObject.defineProperty(exports, \"__esModule\", ({ value: true }));\nconst face_detection_1 = __importDefault(__webpack_require__(/*! ./face-detection */ \"./src/plugins/face-detection.ts\"));\nconst translations_1 = __webpack_require__(/*! ../translations */ \"./src/translations/index.ts\");\nconst deviceDetection_1 = __webpack_require__(/*! ../helper/deviceDetection */ \"./src/helper/deviceDetection.ts\");\nconst MainPage_1 = __webpack_require__(/*! ../classes/MainPage */ \"./src/classes/MainPage.ts\");\nconst camera_1 = __webpack_require__(/*! ../models/camera */ \"./src/models/camera.ts\");\nconst components_1 = __webpack_require__(/*! ../components/components */ \"./src/components/components.ts\");\nconst baseDialog_1 = __importDefault(__webpack_require__(/*! ../components/baseDialog */ \"./src/components/baseDialog.ts\"));\nconst navigationBar_1 = __importDefault(__webpack_require__(/*! ../components/navigationBar */ \"./src/components/navigationBar.ts\"));\nconst footer_1 = __importDefault(__webpack_require__(/*! ../components/footer */ \"./src/components/footer.ts\"));\nconst imagesHelper_1 = __webpack_require__(/*! ../helper/imagesHelper */ \"./src/helper/imagesHelper.ts\");\nclass FaceCamera {\n constructor(cameraId, mainScreenId = \"\") {\n this.cameraTimeLimit = 90000;\n this.videoElement = null;\n this.canvasElement = null;\n this.overlayElement = null;\n this.titleElement = null;\n this.cameraId = cameraId;\n this.mainScreenId = mainScreenId;\n }\n isMobile() {\n // Use extracted helper function\n return (0, deviceDetection_1.isMobileDevice)();\n }\n captureImage() {\n var _a, _b, _c, _d, _e, _f;\n return __awaiter(this, void 0, void 0, function* () {\n const bitmap = this.videoElement;\n // 1. full image\n let canvas = document.createElement(\"canvas\");\n let context = canvas.getContext(\"2d\");\n canvas.width = bitmap.videoWidth;\n canvas.height = bitmap.videoHeight;\n context.drawImage(bitmap, 0, 0, bitmap.videoWidth, bitmap.videoHeight);\n // 2. cropped image — region corresponds exactly to what the overlay shows\n let cropCanvas = document.createElement(\"canvas\");\n let cropContext = cropCanvas.getContext(\"2d\");\n cropCanvas.width = bitmap.videoWidth;\n cropCanvas.height = bitmap.videoHeight;\n cropContext.drawImage(bitmap, 0, 0, bitmap.videoWidth, bitmap.videoHeight);\n // Scale factor: stream pixels per display pixel\n const scaleX = bitmap.videoWidth / ((_b = (_a = this.videoElement) === null || _a === void 0 ? void 0 : _a.clientWidth) !== null && _b !== void 0 ? _b : bitmap.videoWidth);\n const scaleY = bitmap.videoHeight / ((_d = (_c = this.videoElement) === null || _c === void 0 ? void 0 : _c.clientHeight) !== null && _d !== void 0 ? _d : bitmap.videoHeight);\n // Use overlay's actual on-screen position as the crop region\n const overlayRect = (_e = this.overlayElement) === null || _e === void 0 ? void 0 : _e.getBoundingClientRect();\n const videoRect = (_f = this.videoElement) === null || _f === void 0 ? void 0 : _f.getBoundingClientRect();\n let cropX, cropY, cropW, cropH;\n if (overlayRect && videoRect) {\n cropX = Math.round((overlayRect.left - videoRect.left) * scaleX);\n cropY = Math.round((overlayRect.top - videoRect.top) * scaleY);\n cropW = Math.round(overlayRect.width * scaleX);\n cropH = Math.round(overlayRect.height * scaleY);\n }\n else {\n // Fallback: derive from calculateHeightNWidth\n const { width, height } = this.faceDetectionSdk.calculateHeightNWidth(bitmap.videoWidth, bitmap.videoHeight);\n cropW = width;\n cropH = height;\n cropX = Math.round((bitmap.videoWidth - cropW) / 2);\n cropY = Math.round((bitmap.videoHeight - cropH) / 2);\n }\n // Clamp to stream bounds\n cropX = Math.max(0, cropX);\n cropY = Math.max(0, cropY);\n cropW = Math.min(cropW, bitmap.videoWidth - cropX);\n cropH = Math.min(cropH, bitmap.videoHeight - cropY);\n const imgData = cropContext.getImageData(cropX, cropY, cropW, cropH);\n cropCanvas.width = cropW;\n cropCanvas.height = cropH;\n cropContext.putImageData(imgData, 0, 0);\n // 3. resize the canvas\n try {\n canvas = (0, imagesHelper_1.resizeCanvas)(canvas, imagesHelper_1.maxFullImage.width, imagesHelper_1.maxFullImage.height);\n cropCanvas = (0, imagesHelper_1.resizeCanvas)(cropCanvas, imagesHelper_1.maxCroppedImage.width, imagesHelper_1.maxCroppedImage.height);\n }\n catch (err) {\n console.error(\"face resize image error\", err);\n }\n // 4. construct payload - convert to desired file format\n const payload = {\n fullImage: canvas.toDataURL(\"image/jpeg\"),\n croppedImage: cropCanvas.toDataURL(\"image/jpeg\"),\n };\n if (MainPage_1.enableImageDebug) {\n if (payload.fullImage)\n this.downloadImage(payload.fullImage, `selfie-full-${canvas.width}x${canvas.height}.jpg`);\n if (payload.croppedImage)\n this.downloadImage(payload.croppedImage, `selfie-cropped-${cropCanvas.width}x${cropCanvas.height}.jpg`);\n }\n return payload;\n });\n }\n init() {\n try {\n // show loading spinner in full pg\n (0, components_1.createFullLoading)(this.cameraId, \"Initializing Camera...\");\n // 1. Draw the camera display\n this.drawCameraDisplay();\n // 2. Setup related SDK\n const { videoElement, canvasElement, overlayElement, titleElement } = this;\n this.faceDetectionSdk = new face_detection_1.default({\n cameraId: this.cameraId,\n videoElement,\n canvasElement,\n overlayElement,\n titleElement,\n });\n // 3. Get the camera permission and trigger the selfie detection\n this.cameraInit();\n // 4. Trigger event after 3 seconds of face detection\n this.faceDetectionSdk.setCaptureImage((livenessScore) => __awaiter(this, void 0, void 0, function* () {\n var _a;\n const image = yield this.captureImage();\n const event = new CustomEvent(\"selfieCapture\", {\n detail: { image, livenessScore },\n });\n (_a = this.overlayElement) === null || _a === void 0 ? void 0 : _a.dispatchEvent(event);\n }));\n // 5. Face camera custom events listeners\n const description = document.getElementById(\"pw-camera-description\");\n this.faceDetectionSdk.listenToUserNoSmile(() => {\n if (description) {\n description.innerHTML = (0, translations_1.$t)(\"face_smile_at_camera\");\n }\n });\n this.faceDetectionSdk.listenToUserSmiled(() => {\n if (description) {\n description.innerHTML = (0, translations_1.$t)(\"face_smile_seconds\");\n }\n });\n this.faceDetectionSdk.listenToUserSmiledButNotLiveness(() => {\n if (description) {\n description.innerHTML = (0, translations_1.$t)(\"face_neutral_expression\");\n }\n });\n this.faceDetectionSdk.listenToLookStraight(() => {\n if (description) {\n description.innerHTML = (0, translations_1.$t)(\"face_looks_straight\");\n }\n });\n this.faceDetectionSdk.listenToPlaceFace(() => {\n if (description) {\n description.innerHTML = (0, translations_1.$t)(\"face_within_frame\");\n }\n });\n this.faceDetectionSdk.listenToFaceTooFar(() => {\n if (description) {\n description.innerHTML = (0, translations_1.$t)(\"face_too_far\");\n }\n });\n // 6. Start timer to close camera\n this.timer = setTimeout(() => __awaiter(this, void 0, void 0, function* () {\n this.stopVideoStream();\n // open idle dialog\n const idleDialog = new baseDialog_1.default();\n idleDialog.render(this.mainScreenId, (0, translations_1.$t)(\"idle_message\"), (0, translations_1.$t)(\"btn_okay\"));\n }), this.cameraTimeLimit);\n }\n finally {\n if (!this.faceDetectionSdk ||\n this.faceDetectionSdk.getLoadedModelState() !== \"LOADING\")\n (0, components_1.hideFullLoading)(this.cameraId);\n }\n }\n drawCameraDisplay() {\n const mainBody = document.getElementById(this.cameraId);\n if (!mainBody)\n return;\n // 1. header section\n const header = document.createElement(\"div\");\n header.id = camera_1.ELEMENT_IDS.HEADER;\n // header.classList.add(\"camera-header-area\");\n header.classList.add(\"pw-absolute\", \"pw-z-[2]\", \"pw-w-full\", \"pw-text-white\");\n const navigationDiv = new navigationBar_1.default().render(() => {\n this.stopVideoStream();\n });\n header.appendChild(navigationDiv);\n const titleDiv = document.createElement(\"div\");\n titleDiv.id = \"pw-camera-title\";\n titleDiv.classList.add(\"pw-text-2xl\", \"pw-text-center\", \"pw-mb-4\", \"pw-px-5\");\n const body = document.createElement(\"div\");\n body.id = camera_1.ELEMENT_IDS.BODY;\n body.classList.add(\"camera-body-area\");\n const videoContainer = document.createElement(\"div\");\n videoContainer.id = camera_1.ELEMENT_IDS.VIDEO_CONTAINER;\n videoContainer.classList.add(\"video-container\");\n const overlayContainer = document.createElement(\"div\");\n overlayContainer.id = camera_1.ELEMENT_IDS.OVERLAY_CONTAINER;\n overlayContainer.classList.add(\"overlay-container\");\n const overlayElement = document.createElement(\"div\");\n overlayElement.id = camera_1.ELEMENT_IDS.OVERLAY;\n overlayElement.classList.add(\"overlay-element\");\n overlayElement.classList.add(\"pw-hidden\");\n overlayElement.classList.add(\"pw-relative\");\n // Set Description — full viewport width so text is not constrained by oval\n const descriptionDiv = document.createElement(\"div\");\n descriptionDiv.classList.add(\"pw-text-center\", \"pw-px-5\", \"pw-text-xl\", \"pw-mb-4\", \"pw-absolute\", \"pw-text-white\");\n descriptionDiv.style.zIndex = \"1\";\n descriptionDiv.style.left = \"50%\";\n descriptionDiv.style.marginLeft = \"-50vw\";\n descriptionDiv.style.bottom = \"100%\";\n descriptionDiv.style.transform = \"translateY(-20px)\";\n descriptionDiv.style.width = \"100vw\";\n descriptionDiv.style.boxSizing = \"border-box\";\n descriptionDiv.id = \"pw-camera-description\";\n descriptionDiv.innerHTML = (0, translations_1.$t)(\"face_within_frame\");\n this.descriptionElement = descriptionDiv;\n overlayElement.appendChild(descriptionDiv);\n const canvasElement = document.createElement(\"canvas\");\n canvasElement.id = camera_1.ELEMENT_IDS.CANVAS;\n canvasElement.classList.add(\"video-element\");\n canvasElement.classList.add(\"canvas-element\");\n canvasElement.style.transform = \"scaleX(-1)\";\n const videoElement = document.createElement(\"video\");\n videoElement.id = camera_1.ELEMENT_IDS.VIDEO;\n videoElement.autoplay = true;\n videoElement.playsInline = true;\n videoElement.preload = \"auto\";\n videoElement.loop = true;\n videoElement.classList.add(\"video-element\");\n videoElement.style.transform = \"scaleX(-1)\";\n // green tick icon - inside overlayElement so it aligns with overlay (same fix as card-camera)\n const iconElement = document.createElement(\"span\");\n iconElement.id = \"pw-camera-check-icon\";\n iconElement.classList.add(\"pw-hidden\", \"pw-absolute\", \"pw-inset-0\", \"pw-flex\", \"pw-items-center\", \"pw-justify-center\");\n const checkIcon = document.createElement(\"span\");\n checkIcon.classList.add(\"material-icons\", \"pw-text-green-500\", \"pw-text-4xl\", \"pw-rounded-full\");\n checkIcon.style.fontSize = \"48px\";\n checkIcon.innerHTML = \"check_circle\";\n iconElement.appendChild(checkIcon);\n // 2.1\n this.videoElement = videoElement;\n this.overlayElement = overlayElement;\n this.canvasElement = canvasElement;\n overlayElement.appendChild(iconElement);\n overlayContainer.appendChild(overlayElement);\n videoContainer.appendChild(overlayContainer);\n videoContainer.appendChild(canvasElement);\n videoContainer.appendChild(videoElement);\n body.appendChild(videoContainer);\n // 3. Footer - Powered by pipwave\n const footerWrapper = document.createElement(\"div\");\n footerWrapper.classList.add(\"pw-absolute\", \"pw-bottom-0\", \"pw-left-0\", \"pw-right-0\", \"pw-pb-4\", \"pw-z-[2]\", \"pw-flex\", \"pw-justify-center\", \"pw-items-center\", \"pw-text-white\");\n const footer = new footer_1.default().render(true);\n footerWrapper.appendChild(footer);\n mainBody.appendChild(footerWrapper);\n // 4. append child\n mainBody.classList.add(\"camera-bg\");\n mainBody.appendChild(header);\n mainBody.appendChild(body);\n }\n loadVideoStream(videoElement) {\n const initialConstraints = {\n facingMode: \"user\",\n width: {\n ideal: 1280, // 1280, 1024, 640\n },\n height: {\n ideal: 960, // 960, 768, 480\n },\n video: true,\n };\n // Load the video stream\n const nav = window.navigator.mediaDevices\n ? window.navigator.mediaDevices\n : window.navigator;\n if (!nav)\n return;\n nav\n .getUserMedia({ video: initialConstraints })\n .then((stream) => {\n videoElement.srcObject = stream;\n // iOS Safari sometimes leaves a getUserMedia <video> paused even with\n // autoplay+playsInline; an explicit play() avoids a frozen first frame\n // (the \"wait a long time before the camera starts\" symptom).\n const playResult = videoElement.play();\n if (playResult && typeof playResult.catch === \"function\") {\n playResult.catch((err) => console.error(\"Error starting video playback:\", err));\n }\n })\n .catch((err) => {\n console.error(\"Error accessing the camera:\", err);\n });\n }\n cameraInit() {\n var _a;\n if (!this.videoElement)\n return;\n this.loadVideoStream(this.videoElement);\n // video event listeners\n this.videoElement.addEventListener(\"loadedmetadata\", () => __awaiter(this, void 0, void 0, function* () {\n var _b;\n this.faceDetectionSdk.init();\n (_b = this.overlayElement) === null || _b === void 0 ? void 0 : _b.classList.remove(\"pw-hidden\");\n // Set initial video dimensions based on orientation\n this.resizeElement();\n }));\n this.videoElement.addEventListener(\"resize\", () => {\n this.resizeElement();\n });\n window.addEventListener(\"resize\", () => {\n this.resizeElement();\n });\n (_a = screen.orientation) === null || _a === void 0 ? void 0 : _a.addEventListener(\"change\", () => {\n const orientationType = screen.orientation.type;\n if (!orientationType || !this.isMobile)\n return;\n if ([\"landscape-primary\", \"landscape-secondary\"].includes(orientationType)) {\n // TO ADD DIALOG ASK USER TO USE POTRAIT VIEW\n }\n else if ([\"portrait-secondary\", \"portrait-primary\"].includes(orientationType)) {\n // TO BE CONFIRM IF NEED CLOSE DIALOG\n }\n });\n }\n stopVideoStream() {\n var _a;\n // Stop the face-detection animation-frame loop FIRST. Paths that tear the\n // camera down without a preceding pauseSmileFace() (back button, idle\n // timeout, error redirects) would otherwise leave the requestAnimationFrame\n // loop running against a detached video element. Each leaked loop keeps\n // running face-api inference every frame; re-entering the selfie screen\n // stacks more loops and starves the iOS Safari main thread — which freezes\n // the countdown and makes detection unresponsive.\n (_a = this.faceDetectionSdk) === null || _a === void 0 ? void 0 : _a.disableDetection();\n if (this.videoElement && this.videoElement.srcObject) {\n const stream = this.videoElement.srcObject;\n const tracks = stream.getTracks();\n tracks.forEach(function (track) {\n track.stop();\n });\n this.videoElement.srcObject = null;\n }\n if (this.timer)\n clearTimeout(this.timer);\n const cameraDiv = document.getElementById(this.cameraId);\n cameraDiv === null || cameraDiv === void 0 ? void 0 : cameraDiv.remove();\n (0, components_1.hideFullLoading)(this.cameraId);\n }\n resizeElement() {\n const videoContainer = document.getElementById(camera_1.ELEMENT_IDS.VIDEO_CONTAINER);\n const videoElement = this.videoElement;\n if (videoElement && videoElement.videoWidth && videoElement.videoHeight) {\n const vpW = window.innerWidth;\n const vpH = window.innerHeight;\n const videoAspect = videoElement.videoWidth / videoElement.videoHeight;\n // Fit the container to the camera's native aspect ratio within the viewport\n let containerW;\n let containerH;\n if (vpW / vpH > videoAspect) {\n // Viewport is wider than video — constrain by height\n containerH = vpH;\n containerW = vpH * videoAspect;\n }\n else {\n // Viewport is taller than video — constrain by width\n containerW = vpW;\n containerH = vpW / videoAspect;\n }\n if (videoContainer) {\n videoContainer.style.width = `${containerW}px`;\n videoContainer.style.height = `${containerH}px`;\n }\n videoElement.style.width = \"100%\";\n videoElement.style.height = \"100%\";\n videoElement.style.minWidth = \"\";\n videoElement.style.minHeight = \"\";\n }\n this.faceDetectionSdk.resizeElement();\n }\n listenToSelfieCapture(cb) {\n var _a;\n (_a = this.overlayElement) === null || _a === void 0 ? void 0 : _a.addEventListener(\"selfieCapture\", (e) => {\n cb(e);\n });\n }\n pauseSmileFace() {\n var _a, _b;\n return __awaiter(this, void 0, void 0, function* () {\n // stop the video\n (_a = this.videoElement) === null || _a === void 0 ? void 0 : _a.pause();\n yield ((_b = this.faceDetectionSdk) === null || _b === void 0 ? void 0 : _b.disableDetection());\n // stop idle timeout\n if (this.timer) {\n clearTimeout(this.timer);\n }\n // clear description\n const description = document.getElementById(\"pw-camera-description\");\n if (description)\n description.innerHTML = \"\";\n // show green tick icon\n const checkIcon = document.getElementById(\"pw-camera-check-icon\");\n checkIcon === null || checkIcon === void 0 ? void 0 : checkIcon.classList.remove(\"pw-hidden\");\n });\n }\n resumeSmileFace() {\n var _a, _b;\n return __awaiter(this, void 0, void 0, function* () {\n // resume the video\n (_a = this.videoElement) === null || _a === void 0 ? void 0 : _a.play();\n yield ((_b = this.faceDetectionSdk) === null || _b === void 0 ? void 0 : _b.startDetection());\n // start idle timeout\n this.timer = setTimeout(() => __awaiter(this, void 0, void 0, function* () {\n this.stopVideoStream();\n // open idle dialog\n const idleDialog = new baseDialog_1.default();\n idleDialog.render(this.mainScreenId, (0, translations_1.$t)(\"idle_message\"), (0, translations_1.$t)(\"btn_okay\"));\n }), this.cameraTimeLimit);\n // hide green tick icon\n const checkIcon = document.getElementById(\"pw-camera-check-icon\");\n checkIcon === null || checkIcon === void 0 ? void 0 : checkIcon.classList.add(\"pw-hidden\");\n // remove green circle\n const overlayElement = document.getElementById(\"overlayElement\");\n if (overlayElement)\n overlayElement.classList.remove(\"overlay-element--active\");\n // close all the background loading\n (0, components_1.hideFullLoading)(this.cameraId);\n });\n }\n downloadImage(dataUrl, filename) {\n const link = document.createElement(\"a\");\n link.href = dataUrl;\n link.download = filename;\n document.body.appendChild(link);\n link.click();\n document.body.removeChild(link);\n }\n}\nexports[\"default\"] = FaceCamera;\n\n\n//# sourceURL=webpack://PWUISDK/./src/plugins/face-camera.ts?");
4037
4037
 
4038
4038
  /***/ }),
4039
4039
 
@@ -4044,7 +4044,7 @@ eval("\nvar __awaiter = (this && this.__awaiter) || function (thisArg, _argument
4044
4044
  /***/ (function(__unused_webpack_module, exports, __webpack_require__) {
4045
4045
 
4046
4046
  "use strict";
4047
- eval("\nvar __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {\n function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }\n return new (P || (P = Promise))(function (resolve, reject) {\n function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }\n function rejected(value) { try { step(generator[\"throw\"](value)); } catch (e) { reject(e); } }\n function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }\n step((generator = generator.apply(thisArg, _arguments || [])).next());\n });\n};\nObject.defineProperty(exports, \"__esModule\", ({ value: true }));\nconst translations_1 = __webpack_require__(/*! ../translations */ \"./src/translations/index.ts\");\nconst components_1 = __webpack_require__(/*! ../components/components */ \"./src/components/components.ts\");\nlet loadedModelState = \"NOT_LOADED\";\nlet faceApiInstance;\n// direct use faceapi if it is already import in client's project (especially angular)\nfaceApiInstance =\n typeof faceapi !== \"undefined\" ? faceapi : __webpack_require__(/*! face-api.js */ \"./node_modules/face-api.js/build/es6/index.js\");\nclass FACEDETECTIONSDK {\n constructor(val) {\n this.cameraId = \"\";\n this.isDone = false; // use to check if the face detection is done\n this.prevEmotionalState = \"\";\n this.currenEmotionaltState = \"\";\n this.isLiveness = false;\n // set to true to enforce face-fill-ratio check (user must be close enough to camera)\n this.enforceFaceFillRatio = true;\n // set to true to enforce straight-look check during smile capture\n this.enforceLookStraight = true;\n // constant variable\n this.face_api_detection_options = new faceApiInstance.TinyFaceDetectorOptions({\n inputSize: 224,\n });\n this.faceTrack_settings = {\n contrast: 3,\n brightness: 0.5,\n threshold: 100,\n minCorrelation: 0.17,\n minScore: 0.6,\n };\n // html element\n this.videoElement = null;\n this.canvasElement = null;\n this.overlayElement = null;\n this.titleElement = null;\n this.instructionElement = null;\n Object.assign(this, val);\n }\n getLoadedModelState() {\n return loadedModelState;\n }\n setCaptureImage(captureImage) {\n this.captureImage = captureImage;\n }\n // get the face position\n getFaceXY(pos) {\n const posF = {\n top: Math.min.apply(Math, pos.map(function (o) {\n return o.y;\n })),\n bottom: Math.max.apply(Math, pos.map(function (o) {\n return o.y;\n })),\n left: Math.min.apply(Math, pos.map(function (o) {\n return o.x;\n })),\n right: Math.max.apply(Math, pos.map(function (o) {\n return o.x;\n })),\n };\n return {\n x: posF[\"left\"],\n y: posF[\"top\"],\n w: posF[\"right\"] - posF[\"left\"],\n h: posF[\"bottom\"] - posF[\"top\"],\n };\n }\n // get the circle area position\n getCircleXY() {\n if (!this.canvasElement || !this.overlayElement)\n return { x: 0, y: 0, w: 0, h: 0 };\n // canvas rectangle position\n const posR = this.canvasElement.getBoundingClientRect();\n // circle position\n const posC = this.overlayElement.getBoundingClientRect();\n return {\n // get the coord x and y of the circle inside the canvas rectangle\n x: posC.x - posR.x,\n y: posC.y - posR.y,\n w: posC.width,\n h: posC.height,\n };\n }\n // to detect if your face is looking straight at the camera\n isAngleDetected(pos) {\n const angleOne = (Math.atan2(Math.abs(pos[7][\"y\"] - pos[33][\"y\"]), Math.abs(pos[7][\"x\"] - pos[33][\"x\"])) *\n 180) /\n Math.PI;\n const angleTwo = (Math.atan2(Math.abs(pos[60][\"y\"] - pos[33][\"y\"]), Math.abs(pos[60][\"x\"] - pos[33][\"x\"])) *\n 180) /\n Math.PI;\n const distance1 = Math.abs(pos[0][\"x\"] - pos[33][\"x\"]);\n const distance2 = Math.abs(pos[14][\"x\"] - pos[33][\"x\"]);\n const isAngle = !((angleOne > 40 && angleOne < 93) ||\n (angleTwo > 40 && angleTwo < 93));\n const isDistance = Math.abs(distance1 - distance2) / distance1 > 0.5;\n return isAngle || isDistance;\n }\n // check if the face is inside the circle area\n isFaceDetected(pos) {\n const posCircle = this.getCircleXY();\n const posFace = this.getFaceXY(pos);\n // face-api landmarks stop at the eyebrows — the forehead above is not covered.\n // Add ~60% of landmark height as top padding to account for the unseen forehead,\n // so the oval check requires the full face (including forehead) to be inside.\n const foreheadPadding = posFace.h * 0.6;\n return (posFace.x + posFace.w < posCircle.x + posCircle.w &&\n posFace.x > posCircle.x &&\n posFace.y - foreheadPadding > posCircle.y &&\n posFace.y + posFace.h < posCircle.y + posCircle.h);\n }\n // check if the face fills enough of the oval (user is close enough to camera).\n // Face width and estimated full-face height (landmarks + forehead) must each\n // cover at least MIN_FACE_FILL_RATIO of the oval's dimensions.\n isFaceLargeEnough(pos) {\n const MIN_FACE_FILL_RATIO = 0.60;\n const posCircle = this.getCircleXY();\n const posFace = this.getFaceXY(pos);\n const estimatedFaceHeight = posFace.h * 1.6; // landmarks height + 60% forehead\n return (posFace.w / posCircle.w >= MIN_FACE_FILL_RATIO &&\n estimatedFaceHeight / posCircle.h >= MIN_FACE_FILL_RATIO);\n }\n // check if the face emotional is changing\n detectLiveness(current) {\n this.prevEmotionalState = this.currenEmotionaltState;\n this.currenEmotionaltState = current;\n return (this.prevEmotionalState &&\n this.currenEmotionaltState &&\n this.prevEmotionalState != this.currenEmotionaltState);\n }\n // check if the face in the video is similar to the previous face\n detectSimilarFace(current) {\n this.prevFaceDescriptor = this.currentFaceDescriptor;\n this.currentFaceDescriptor = current;\n if (!this.prevFaceDescriptor || !this.currentFaceDescriptor)\n return 1;\n // the smaller the distance, the more similar the faces are\n return faceApiInstance.euclideanDistance(this.prevFaceDescriptor, this.currentFaceDescriptor);\n }\n // reset instruction html text and all those checking\n resetInstructions() {\n this.isLiveness = false;\n this.currenEmotionaltState = \"\";\n this.prevEmotionalState = \"\";\n this.prevFaceDescriptor = undefined;\n this.currentFaceDescriptor = undefined;\n // this.overlayElement?.classList.remove(\"overlay-element--selfie-smile\");\n // this.overlayElement?.classList.remove(\"overlay-element--selfie-neutral\");\n // this.overlayElement?.classList.remove(\"overlay-element--active\");\n }\n checkFace(livenessScore, pos, currentExpression, currentFaceDescriptor) {\n var _a, _b, _c, _d, _e, _f;\n const isFace = this.isFaceDetected(pos);\n // 1. check if the face is inside the circle area\n if (!isFace) {\n this.resetInstructions();\n const event = new CustomEvent(\"placeFace\");\n (_a = this.overlayElement) === null || _a === void 0 ? void 0 : _a.dispatchEvent(event);\n return;\n }\n // 1b. check if the face is large enough (user not too far from camera)\n if (this.enforceFaceFillRatio && !this.isFaceLargeEnough(pos)) {\n this.resetInstructions();\n const event = new CustomEvent(\"faceTooFar\");\n (_b = this.overlayElement) === null || _b === void 0 ? void 0 : _b.dispatchEvent(event);\n return;\n }\n // 2. check if the face is looking straight at the camera\n const isLookingStraight = !this.isAngleDetected(pos);\n if (this.enforceLookStraight && !isLookingStraight) {\n this.resetInstructions();\n const event = new CustomEvent(\"lookStraight\");\n (_c = this.overlayElement) === null || _c === void 0 ? void 0 : _c.dispatchEvent(event);\n if (this.instructionElement)\n this.instructionElement.innerHTML = (0, translations_1.$t)(\"face_looks_straight\");\n return;\n }\n // 3. check the liveness of the face\n if (this.detectLiveness(currentExpression))\n this.isLiveness = true;\n // 4. check the similarity of the face\n const isSimilarFace = this.detectSimilarFace(currentFaceDescriptor) < 0.6;\n if (this.isLiveness && !isSimilarFace) {\n this.resetInstructions();\n return;\n }\n if (currentExpression !== \"happy\") {\n const event = new CustomEvent(\"userNoSmile\");\n (_d = this.overlayElement) === null || _d === void 0 ? void 0 : _d.dispatchEvent(event);\n // this.overlayElement?.classList.add(\"overlay-element--selfie-smile\");\n // this.overlayElement?.classList.remove(\"overlay-element--selfie-neutral\");\n // this.overlayElement?.classList.remove(\"overlay-element--active\");\n }\n else if (this.isLiveness) {\n // emit event here STAY 3 seconds\n const event = new CustomEvent(\"userSmiled\");\n (_e = this.overlayElement) === null || _e === void 0 ? void 0 : _e.dispatchEvent(event);\n // this.overlayElement?.classList.remove(\"overlay-element--selfie-smile\");\n // this.overlayElement?.classList.remove(\"overlay-element--selfie-neutral\");\n // this.overlayElement?.classList.add(\"overlay-element--active\");\n if (isSimilarFace &&\n this.isLiveness &&\n this.currenEmotionaltState === \"happy\" &&\n (!this.enforceLookStraight || isLookingStraight) &&\n this.captureImage) {\n this.captureImage(livenessScore);\n }\n }\n else if (!this.isDone) {\n const event = new CustomEvent(\"userSmiledButNotLiveness\");\n (_f = this.overlayElement) === null || _f === void 0 ? void 0 : _f.dispatchEvent(event);\n // this.overlayElement?.classList.remove(\"overlay-element--selfie-smile\");\n // this.overlayElement?.classList.add(\"overlay-element--selfie-neutral\");\n // this.overlayElement?.classList.remove(\"overlay-element--active\");\n }\n }\n detectFace() {\n var _a, _b;\n return __awaiter(this, void 0, void 0, function* () {\n if (!this.videoElement || !this.canvasElement || !this.overlayElement)\n return;\n const displaySize = {\n width: this.canvasElement.width,\n height: this.canvasElement.height,\n };\n const detections = yield faceApiInstance\n .detectSingleFace(this.videoElement, this.face_api_detection_options)\n .withFaceLandmarks()\n .withFaceExpressions()\n .withFaceDescriptor();\n const event = new CustomEvent(\"placeFace\");\n if (typeof detections !== \"undefined\") {\n const expressions = detections.expressions;\n const currentExpression = Object.keys(expressions).reduce((a, b) => {\n return expressions[a] >\n expressions[b]\n ? a\n : b;\n });\n const resizedDetections = faceApiInstance.resizeResults(detections, displaySize);\n const pos = resizedDetections.landmarks.positions;\n const score = detections.detection.score;\n if (score > this.faceTrack_settings.minScore && pos) {\n this.checkFace(score, pos, currentExpression, detections.descriptor);\n }\n else {\n this.resetInstructions();\n (_a = this.overlayElement) === null || _a === void 0 ? void 0 : _a.dispatchEvent(event);\n }\n // draw canvas points for developer view only\n // if (typeof resizedDetections !== \"undefined\" && this.canvasElement) {\n // this.canvasElement\n // .getContext(\"2d\")\n // ?.clearRect(\n // 0,\n // 0,\n // this.canvasElement.width,\n // this.canvasElement.height\n // );\n // if (resizedDetections) {\n // faceApiInstance.draw.drawDetections(this.canvasElement, resizedDetections);\n // faceApiInstance.draw.drawFaceLandmarks(this.canvasElement, resizedDetections);\n // faceApiInstance.draw.drawFaceExpressions(\n // this.canvasElement,\n // resizedDetections\n // );\n // }\n // }\n }\n else {\n this.resetInstructions();\n (_b = this.overlayElement) === null || _b === void 0 ? void 0 : _b.dispatchEvent(event);\n }\n if (!this.isDone)\n this.iFrameRef = requestAnimationFrame(() => this.detectFace());\n });\n }\n disableDetection() {\n return __awaiter(this, void 0, void 0, function* () {\n this.isDone = true;\n // stop face detection\n if (this.iFrameRef)\n cancelAnimationFrame(this.iFrameRef);\n this.iFrameRef = undefined;\n // reset all the instructions text\n this.resetInstructions();\n });\n }\n startDetection() {\n return __awaiter(this, void 0, void 0, function* () {\n this.isDone = false;\n // start face detection\n (0, components_1.createCountdown)(this.cameraId, 5, () => __awaiter(this, void 0, void 0, function* () {\n yield this.detectFace();\n }));\n });\n }\n // use to calculate the cropped image size / ratio\n // from ui or exact image src\n calculateHeightNWidth(width, height) {\n var _a, _b;\n const videoWidth = ((_a = this.videoElement) === null || _a === void 0 ? void 0 : _a.clientWidth) || 0;\n const videoHeight = ((_b = this.videoElement) === null || _b === void 0 ? void 0 : _b.clientHeight) || 0;\n // calculate the circle size for the image\n // based on which has the largest size: height or width\n // then the circle will be 5/6 from that smaller size\n const circleSize = videoHeight > videoWidth ? width - width / 6 : height - height / 6;\n const newWidth = (circleSize / 5) * 4.5;\n const newHeight = circleSize;\n return { width: newWidth, height: newHeight };\n }\n resizeElement() {\n var _a, _b, _c, _d;\n if (!this.videoElement || !this.canvasElement)\n return;\n const videoW = this.videoElement.clientWidth;\n const videoH = this.videoElement.clientHeight;\n // 1. resize canvas to match video size\n this.canvasElement.width = videoW;\n this.canvasElement.height = videoH;\n // 2. desired oval size (9:10 width/height)\n const OVERLAY_ASPECT_RATIO = 9 / 10; // width / height\n const OVERLAY_MAX_HEIGHT = 500;\n const MOBILE_PADDING_VW = 0.08;\n let width;\n let height;\n if (window.screen.width >= 475) {\n height = OVERLAY_MAX_HEIGHT;\n width = Math.round(height * OVERLAY_ASPECT_RATIO);\n }\n else {\n const paddingPx = window.innerWidth * MOBILE_PADDING_VW;\n width = Math.round(window.innerWidth - 2 * paddingPx);\n height = Math.round(width / OVERLAY_ASPECT_RATIO);\n }\n // 3. clamp oval to video bounds (oval must not exceed video width or height)\n if (videoW > 0 && videoH > 0) {\n const maxWidthByHeight = videoH * OVERLAY_ASPECT_RATIO;\n width = Math.min(width, videoW, maxWidthByHeight);\n height = Math.round(width / OVERLAY_ASPECT_RATIO);\n if (height > videoH) {\n height = videoH;\n width = Math.round(height * OVERLAY_ASPECT_RATIO);\n }\n }\n (_a = this.overlayElement) === null || _a === void 0 ? void 0 : _a.style.setProperty(\"border-radius\", \"50%\");\n (_b = this.overlayElement) === null || _b === void 0 ? void 0 : _b.style.setProperty(\"border\", \"solid white 3px\");\n (_c = this.overlayElement) === null || _c === void 0 ? void 0 : _c.style.setProperty(\"width\", `${width}px`);\n (_d = this.overlayElement) === null || _d === void 0 ? void 0 : _d.style.setProperty(\"height\", `${height}px`);\n }\n // train face-api models\n loadFaceModel() {\n return __awaiter(this, void 0, void 0, function* () {\n try {\n if (loadedModelState !== \"NOT_LOADED\")\n return;\n const url = \"https://assets.pipwave.com/ekyc/sdk/face-models\" || 0;\n loadedModelState = \"LOADING\";\n yield Promise.all([\n faceApiInstance.loadTinyFaceDetectorModel(url),\n faceApiInstance.loadFaceLandmarkModel(url),\n faceApiInstance.loadFaceRecognitionModel(url),\n faceApiInstance.loadFaceExpressionModel(url),\n ]);\n loadedModelState = \"LOADED\";\n }\n catch (err) {\n loadedModelState = \"NOT_LOADED\";\n return Promise.reject(err);\n }\n });\n }\n init() {\n return __awaiter(this, void 0, void 0, function* () {\n // show loading spinner in full pg\n (0, components_1.createFullLoading)(this.cameraId, \"Initializing Camera...\");\n this.isDone = false;\n this.resizeElement();\n if (loadedModelState === \"NOT_LOADED\")\n yield this.loadFaceModel();\n // Wait until loadFaceModel is LOADED\n while (loadedModelState === \"LOADING\") {\n yield new Promise((resolve) => setTimeout(resolve, 500));\n }\n if (!this.iFrameRef) {\n (0, components_1.hideFullLoading)(this.cameraId);\n yield this.startDetection();\n }\n });\n }\n listenToUserNoSmile(cb) {\n var _a;\n (_a = this.overlayElement) === null || _a === void 0 ? void 0 : _a.addEventListener(\"userNoSmile\", () => {\n cb();\n });\n }\n listenToUserSmiled(cb) {\n var _a;\n (_a = this.overlayElement) === null || _a === void 0 ? void 0 : _a.addEventListener(\"userSmiled\", () => {\n cb();\n });\n }\n listenToUserSmiledButNotLiveness(cb) {\n var _a;\n (_a = this.overlayElement) === null || _a === void 0 ? void 0 : _a.addEventListener(\"userSmiledButNotLiveness\", () => {\n cb();\n });\n }\n listenToLookStraight(cb) {\n var _a;\n (_a = this.overlayElement) === null || _a === void 0 ? void 0 : _a.addEventListener(\"lookStraight\", () => {\n cb();\n });\n }\n listenToPlaceFace(cb) {\n var _a;\n (_a = this.overlayElement) === null || _a === void 0 ? void 0 : _a.addEventListener(\"placeFace\", () => {\n cb();\n });\n }\n listenToFaceTooFar(cb) {\n var _a;\n (_a = this.overlayElement) === null || _a === void 0 ? void 0 : _a.addEventListener(\"faceTooFar\", () => {\n cb();\n });\n }\n}\nexports[\"default\"] = FACEDETECTIONSDK;\n\n\n//# sourceURL=webpack://PWUISDK/./src/plugins/face-detection.ts?");
4047
+ eval("\nvar __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {\n function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }\n return new (P || (P = Promise))(function (resolve, reject) {\n function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }\n function rejected(value) { try { step(generator[\"throw\"](value)); } catch (e) { reject(e); } }\n function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }\n step((generator = generator.apply(thisArg, _arguments || [])).next());\n });\n};\nObject.defineProperty(exports, \"__esModule\", ({ value: true }));\nconst translations_1 = __webpack_require__(/*! ../translations */ \"./src/translations/index.ts\");\nconst components_1 = __webpack_require__(/*! ../components/components */ \"./src/components/components.ts\");\nconst MainPage_1 = __webpack_require__(/*! ../classes/MainPage */ \"./src/classes/MainPage.ts\");\n// Gated diagnostic logger. Enable by starting the SDK with enableImageDebug:true\n// (or ticking the debug checkbox on the demo page). Prefix makes the selfie /\n// face-detection logs easy to filter in the console: filter on \"[pw-face]\".\nfunction faceLog(...args) {\n if (MainPage_1.enableImageDebug)\n console.log(\"[pw-face]\", ...args);\n}\nlet loadedModelState = \"NOT_LOADED\";\n// face-api's FIRST inference compiles its WebGL shaders and is dramatically\n// slower than steady-state. We pay that cost once, up front, behind the loading\n// spinner (see warmUpDetection). Module-level so it persists across re-entries.\nlet warmedUp = false;\nlet faceApiInstance;\n// direct use faceapi if it is already import in client's project (especially angular)\nfaceApiInstance =\n typeof faceapi !== \"undefined\" ? faceapi : __webpack_require__(/*! face-api.js */ \"./node_modules/face-api.js/build/es6/index.js\");\nclass FACEDETECTIONSDK {\n constructor(val) {\n this.cameraId = \"\";\n this.isDone = false; // use to check if the face detection is done\n // true from the moment startDetection() begins until disableDetection() runs.\n // `iFrameRef` is only set AFTER the countdown ends, so it can't guard the\n // model-load + countdown window; this flag does, preventing stacked loops.\n this.isStartingDetection = false;\n this.prevEmotionalState = \"\";\n this.currenEmotionaltState = \"\";\n this.isLiveness = false;\n // --- hint debounce ---\n // detectFace() runs on every animation frame, so the raw hint can flip many\n // times a second near a threshold, making the on-screen message flicker.\n // emitHint() suppresses same-as-last hints and throttles genuine changes to\n // at most one per HINT_MIN_INTERVAL_MS.\n this.lastHintEvent = \"\";\n this.lastHintAt = 0;\n this.HINT_MIN_INTERVAL_MS = 500;\n // --- detection throttle ---\n // detectFace() re-fires the moment each inference resolves, so on fast\n // devices it runs inferences back-to-back (15-30/sec), burning GPU/battery\n // for samples no one perceives. Cap it at ~5fps — plenty for smile/liveness/\n // angle, which change over hundreds of ms. This is a floor, not a cap: on\n // slow devices where one inference already takes >200ms it's a no-op.\n this.lastDetectAt = 0;\n this.DETECT_MIN_INTERVAL_MS = 200;\n // set to true to enforce face-fill-ratio check (user must be close enough to camera)\n this.enforceFaceFillRatio = true;\n // set to true to enforce straight-look check during smile capture\n this.enforceLookStraight = true;\n // constant variable\n this.face_api_detection_options = new faceApiInstance.TinyFaceDetectorOptions({\n inputSize: 224,\n });\n this.faceTrack_settings = {\n contrast: 3,\n brightness: 0.5,\n threshold: 100,\n minCorrelation: 0.17,\n minScore: 0.6,\n };\n // html element\n this.videoElement = null;\n this.canvasElement = null;\n this.overlayElement = null;\n this.titleElement = null;\n this.instructionElement = null;\n Object.assign(this, val);\n }\n getLoadedModelState() {\n return loadedModelState;\n }\n setCaptureImage(captureImage) {\n this.captureImage = captureImage;\n }\n // get the face position\n getFaceXY(pos) {\n const posF = {\n top: Math.min.apply(Math, pos.map(function (o) {\n return o.y;\n })),\n bottom: Math.max.apply(Math, pos.map(function (o) {\n return o.y;\n })),\n left: Math.min.apply(Math, pos.map(function (o) {\n return o.x;\n })),\n right: Math.max.apply(Math, pos.map(function (o) {\n return o.x;\n })),\n };\n return {\n x: posF[\"left\"],\n y: posF[\"top\"],\n w: posF[\"right\"] - posF[\"left\"],\n h: posF[\"bottom\"] - posF[\"top\"],\n };\n }\n // get the circle area position\n getCircleXY() {\n if (!this.canvasElement || !this.overlayElement)\n return { x: 0, y: 0, w: 0, h: 0 };\n // canvas rectangle position\n const posR = this.canvasElement.getBoundingClientRect();\n // circle position\n const posC = this.overlayElement.getBoundingClientRect();\n return {\n // get the coord x and y of the circle inside the canvas rectangle\n x: posC.x - posR.x,\n y: posC.y - posR.y,\n w: posC.width,\n h: posC.height,\n };\n }\n // to detect if your face is looking straight at the camera\n isAngleDetected(pos) {\n const angleOne = (Math.atan2(Math.abs(pos[7][\"y\"] - pos[33][\"y\"]), Math.abs(pos[7][\"x\"] - pos[33][\"x\"])) *\n 180) /\n Math.PI;\n const angleTwo = (Math.atan2(Math.abs(pos[60][\"y\"] - pos[33][\"y\"]), Math.abs(pos[60][\"x\"] - pos[33][\"x\"])) *\n 180) /\n Math.PI;\n const distance1 = Math.abs(pos[0][\"x\"] - pos[33][\"x\"]);\n const distance2 = Math.abs(pos[14][\"x\"] - pos[33][\"x\"]);\n const isAngle = !((angleOne > 40 && angleOne < 93) ||\n (angleTwo > 40 && angleTwo < 93));\n const isDistance = Math.abs(distance1 - distance2) / distance1 > 0.5;\n return isAngle || isDistance;\n }\n // check if the face is inside the circle area\n isFaceDetected(pos) {\n const posCircle = this.getCircleXY();\n const posFace = this.getFaceXY(pos);\n // face-api landmarks stop at the eyebrows — the forehead above is not covered.\n // Add ~60% of landmark height as top padding to account for the unseen forehead,\n // so the oval check requires the full face (including forehead) to be inside.\n const foreheadPadding = posFace.h * 0.6;\n return (posFace.x + posFace.w < posCircle.x + posCircle.w &&\n posFace.x > posCircle.x &&\n posFace.y - foreheadPadding > posCircle.y &&\n posFace.y + posFace.h < posCircle.y + posCircle.h);\n }\n // check if the face fills enough of the oval (user is close enough to camera).\n // Face width and estimated full-face height (landmarks + forehead) must each\n // cover at least MIN_FACE_FILL_RATIO of the oval's dimensions.\n isFaceLargeEnough(pos) {\n const MIN_FACE_FILL_RATIO = 0.60;\n const posCircle = this.getCircleXY();\n const posFace = this.getFaceXY(pos);\n // landmarks height + 60% forehead. Higher multiplier = less strict (face\n // counts as \"large enough\" from farther away); lower = stricter (user must\n // move closer to pass the fill-ratio check).\n const estimatedFaceHeight = posFace.h * 1.6;\n return (posFace.w / posCircle.w >= MIN_FACE_FILL_RATIO &&\n estimatedFaceHeight / posCircle.h >= MIN_FACE_FILL_RATIO);\n }\n // check if the face emotional is changing\n detectLiveness(current) {\n this.prevEmotionalState = this.currenEmotionaltState;\n this.currenEmotionaltState = current;\n return (this.prevEmotionalState &&\n this.currenEmotionaltState &&\n this.prevEmotionalState != this.currenEmotionaltState);\n }\n // check if the face in the video is similar to the previous face\n detectSimilarFace(current) {\n this.prevFaceDescriptor = this.currentFaceDescriptor;\n this.currentFaceDescriptor = current;\n if (!this.prevFaceDescriptor || !this.currentFaceDescriptor)\n return 1;\n // the smaller the distance, the more similar the faces are\n return faceApiInstance.euclideanDistance(this.prevFaceDescriptor, this.currentFaceDescriptor);\n }\n // reset instruction html text and all those checking\n resetInstructions() {\n this.isLiveness = false;\n this.currenEmotionaltState = \"\";\n this.prevEmotionalState = \"\";\n this.prevFaceDescriptor = undefined;\n this.currentFaceDescriptor = undefined;\n // this.overlayElement?.classList.remove(\"overlay-element--selfie-smile\");\n // this.overlayElement?.classList.remove(\"overlay-element--selfie-neutral\");\n // this.overlayElement?.classList.remove(\"overlay-element--active\");\n }\n // Dispatch a hint event, debounced. Skips repeats of the current hint and\n // ignores changes that arrive sooner than HINT_MIN_INTERVAL_MS after the last\n // one, so the guidance message can't flicker frame-to-frame.\n emitHint(eventName) {\n var _a;\n if (eventName === this.lastHintEvent)\n return;\n const now = performance.now();\n if (now - this.lastHintAt < this.HINT_MIN_INTERVAL_MS) {\n faceLog(`hint suppressed (throttled): \"${this.lastHintEvent}\" -> \"${eventName}\"`, `${Math.round(now - this.lastHintAt)}ms < ${this.HINT_MIN_INTERVAL_MS}ms`);\n return;\n }\n faceLog(`hint change: \"${this.lastHintEvent || \"—\"}\" -> \"${eventName}\"`);\n this.lastHintEvent = eventName;\n this.lastHintAt = now;\n (_a = this.overlayElement) === null || _a === void 0 ? void 0 : _a.dispatchEvent(new CustomEvent(eventName));\n }\n checkFace(livenessScore, pos, currentExpression, currentFaceDescriptor) {\n const isFace = this.isFaceDetected(pos);\n // 1. check if the face is inside the circle area\n if (!isFace) {\n this.resetInstructions();\n this.emitHint(\"placeFace\");\n return;\n }\n // 1b. check if the face is large enough (user not too far from camera)\n if (this.enforceFaceFillRatio && !this.isFaceLargeEnough(pos)) {\n this.resetInstructions();\n this.emitHint(\"faceTooFar\");\n return;\n }\n // 2. check if the face is looking straight at the camera\n const isLookingStraight = !this.isAngleDetected(pos);\n if (this.enforceLookStraight && !isLookingStraight) {\n this.resetInstructions();\n this.emitHint(\"lookStraight\");\n if (this.instructionElement)\n this.instructionElement.innerHTML = (0, translations_1.$t)(\"face_looks_straight\");\n return;\n }\n // 3. check the liveness of the face\n if (this.detectLiveness(currentExpression))\n this.isLiveness = true;\n // 4. check the similarity of the face\n const isSimilarFace = this.detectSimilarFace(currentFaceDescriptor) < 0.6;\n if (this.isLiveness && !isSimilarFace) {\n this.resetInstructions();\n return;\n }\n if (currentExpression !== \"happy\") {\n this.emitHint(\"userNoSmile\");\n // this.overlayElement?.classList.add(\"overlay-element--selfie-smile\");\n // this.overlayElement?.classList.remove(\"overlay-element--selfie-neutral\");\n // this.overlayElement?.classList.remove(\"overlay-element--active\");\n }\n else if (this.isLiveness) {\n // emit event here STAY 3 seconds\n this.emitHint(\"userSmiled\");\n // this.overlayElement?.classList.remove(\"overlay-element--selfie-smile\");\n // this.overlayElement?.classList.remove(\"overlay-element--selfie-neutral\");\n // this.overlayElement?.classList.add(\"overlay-element--active\");\n if (isSimilarFace &&\n this.isLiveness &&\n this.currenEmotionaltState === \"happy\" &&\n (!this.enforceLookStraight || isLookingStraight) &&\n this.captureImage) {\n this.captureImage(livenessScore);\n }\n }\n else if (!this.isDone) {\n this.emitHint(\"userSmiledButNotLiveness\");\n // this.overlayElement?.classList.remove(\"overlay-element--selfie-smile\");\n // this.overlayElement?.classList.add(\"overlay-element--selfie-neutral\");\n // this.overlayElement?.classList.remove(\"overlay-element--active\");\n }\n }\n detectFace() {\n return __awaiter(this, void 0, void 0, function* () {\n if (!this.videoElement || !this.canvasElement || !this.overlayElement)\n return;\n // throttle inference to ~DETECT_MIN_INTERVAL_MS. Re-schedule the loop\n // without running detection until enough time has elapsed.\n const now = performance.now();\n if (now - this.lastDetectAt < this.DETECT_MIN_INTERVAL_MS) {\n if (!this.isDone)\n this.iFrameRef = requestAnimationFrame(() => this.detectFace());\n return;\n }\n this.lastDetectAt = now;\n const displaySize = {\n width: this.canvasElement.width,\n height: this.canvasElement.height,\n };\n const detections = yield faceApiInstance\n .detectSingleFace(this.videoElement, this.face_api_detection_options)\n .withFaceLandmarks()\n .withFaceExpressions()\n .withFaceDescriptor();\n if (typeof detections !== \"undefined\") {\n const expressions = detections.expressions;\n const currentExpression = Object.keys(expressions).reduce((a, b) => {\n return expressions[a] >\n expressions[b]\n ? a\n : b;\n });\n const resizedDetections = faceApiInstance.resizeResults(detections, displaySize);\n const pos = resizedDetections.landmarks.positions;\n const score = detections.detection.score;\n if (score > this.faceTrack_settings.minScore && pos) {\n this.checkFace(score, pos, currentExpression, detections.descriptor);\n }\n else {\n this.resetInstructions();\n this.emitHint(\"placeFace\");\n }\n // draw canvas points for developer view only\n // if (typeof resizedDetections !== \"undefined\" && this.canvasElement) {\n // this.canvasElement\n // .getContext(\"2d\")\n // ?.clearRect(\n // 0,\n // 0,\n // this.canvasElement.width,\n // this.canvasElement.height\n // );\n // if (resizedDetections) {\n // faceApiInstance.draw.drawDetections(this.canvasElement, resizedDetections);\n // faceApiInstance.draw.drawFaceLandmarks(this.canvasElement, resizedDetections);\n // faceApiInstance.draw.drawFaceExpressions(\n // this.canvasElement,\n // resizedDetections\n // );\n // }\n // }\n }\n else {\n this.resetInstructions();\n this.emitHint(\"placeFace\");\n }\n if (!this.isDone)\n this.iFrameRef = requestAnimationFrame(() => this.detectFace());\n });\n }\n disableDetection() {\n return __awaiter(this, void 0, void 0, function* () {\n faceLog(\"disableDetection: stopping loop + resetting state\", {\n hadLoop: this.iFrameRef !== undefined,\n });\n this.isDone = true;\n this.isStartingDetection = false;\n // reset hint debounce so the next session emits its first hint immediately\n this.lastHintEvent = \"\";\n this.lastHintAt = 0;\n // reset detection throttle so the next session's first inference runs at once\n this.lastDetectAt = 0;\n // stop face detection\n if (this.iFrameRef)\n cancelAnimationFrame(this.iFrameRef);\n this.iFrameRef = undefined;\n // reset all the instructions text\n this.resetInstructions();\n });\n }\n startDetection() {\n return __awaiter(this, void 0, void 0, function* () {\n // Guard against stacking detection loops. A second entry can happen via\n // repeated `loadedmetadata`, a retry, or a resume without a matching pause.\n // `iFrameRef` is only set AFTER the 5s countdown finishes, so it alone\n // leaves the model-load + countdown window unguarded — two \"camera ready\"\n // signals in that window would each start their own countdown + loop and\n // run face-api inference 2x/3x per frame, starving the main thread (the\n // \"countdown stuck / detection unresponsive\" bug). `isStartingDetection`\n // closes that gap by marking detection active the moment we start.\n if (this.iFrameRef !== undefined || this.isStartingDetection) {\n faceLog(\"startDetection blocked: loop already starting/running\", {\n iFrameRef: this.iFrameRef,\n isStartingDetection: this.isStartingDetection,\n });\n return;\n }\n this.isStartingDetection = true;\n this.isDone = false;\n faceLog(\"startDetection: countdown started\");\n // start face detection\n (0, components_1.createCountdown)(this.cameraId, 5, () => __awaiter(this, void 0, void 0, function* () {\n faceLog(\"countdown finished: starting detection loop\");\n yield this.detectFace();\n }));\n });\n }\n // use to calculate the cropped image size / ratio\n // from ui or exact image src\n calculateHeightNWidth(width, height) {\n var _a, _b;\n const videoWidth = ((_a = this.videoElement) === null || _a === void 0 ? void 0 : _a.clientWidth) || 0;\n const videoHeight = ((_b = this.videoElement) === null || _b === void 0 ? void 0 : _b.clientHeight) || 0;\n // calculate the circle size for the image\n // based on which has the largest size: height or width\n // then the circle will be 5/6 from that smaller size\n const circleSize = videoHeight > videoWidth ? width - width / 6 : height - height / 6;\n const newWidth = (circleSize / 5) * 4.5;\n const newHeight = circleSize;\n return { width: newWidth, height: newHeight };\n }\n resizeElement() {\n var _a, _b, _c, _d;\n if (!this.videoElement || !this.canvasElement)\n return;\n const videoW = this.videoElement.clientWidth;\n const videoH = this.videoElement.clientHeight;\n // 1. resize canvas to match video size\n this.canvasElement.width = videoW;\n this.canvasElement.height = videoH;\n // 2. desired oval size (9:10 width/height)\n const OVERLAY_ASPECT_RATIO = 9 / 10; // width / height\n const OVERLAY_MAX_HEIGHT = 500;\n const MOBILE_PADDING_VW = 0.08;\n let width;\n let height;\n if (window.screen.width >= 475) {\n height = OVERLAY_MAX_HEIGHT;\n width = Math.round(height * OVERLAY_ASPECT_RATIO);\n }\n else {\n const paddingPx = window.innerWidth * MOBILE_PADDING_VW;\n width = Math.round(window.innerWidth - 2 * paddingPx);\n height = Math.round(width / OVERLAY_ASPECT_RATIO);\n }\n // 3. clamp oval to video bounds (oval must not exceed video width or height)\n if (videoW > 0 && videoH > 0) {\n const maxWidthByHeight = videoH * OVERLAY_ASPECT_RATIO;\n width = Math.min(width, videoW, maxWidthByHeight);\n height = Math.round(width / OVERLAY_ASPECT_RATIO);\n if (height > videoH) {\n height = videoH;\n width = Math.round(height * OVERLAY_ASPECT_RATIO);\n }\n }\n (_a = this.overlayElement) === null || _a === void 0 ? void 0 : _a.style.setProperty(\"border-radius\", \"50%\");\n (_b = this.overlayElement) === null || _b === void 0 ? void 0 : _b.style.setProperty(\"border\", \"solid white 3px\");\n (_c = this.overlayElement) === null || _c === void 0 ? void 0 : _c.style.setProperty(\"width\", `${width}px`);\n (_d = this.overlayElement) === null || _d === void 0 ? void 0 : _d.style.setProperty(\"height\", `${height}px`);\n }\n // train face-api models\n loadFaceModel() {\n return __awaiter(this, void 0, void 0, function* () {\n try {\n if (loadedModelState !== \"NOT_LOADED\")\n return;\n const url = \"https://assets.pipwave.com/ekyc/sdk/face-models\" || 0;\n loadedModelState = \"LOADING\";\n yield Promise.all([\n faceApiInstance.loadTinyFaceDetectorModel(url),\n faceApiInstance.loadFaceLandmarkModel(url),\n faceApiInstance.loadFaceRecognitionModel(url),\n faceApiInstance.loadFaceExpressionModel(url),\n ]);\n loadedModelState = \"LOADED\";\n }\n catch (err) {\n loadedModelState = \"NOT_LOADED\";\n return Promise.reject(err);\n }\n });\n }\n // Run one throwaway inference so face-api compiles its WebGL shaders / warms\n // the GPU pipeline now — while the loading spinner is still showing — instead\n // of on the first real frame right after the countdown. Without this, that\n // first inference blocks the main thread for seconds: the countdown appears\n // \"stuck at 1\" and the hint stays frozen until warmup finishes.\n warmUpDetection() {\n return __awaiter(this, void 0, void 0, function* () {\n if (warmedUp) {\n faceLog(\"warmup skipped: already warmed up\");\n return;\n }\n const startedAt = performance.now();\n try {\n // Prefer the live video: if a face is visible it warms the FULL pipeline\n // (detector + landmarks + expressions + descriptor). The blank-canvas\n // fallback only warms the detector — with no face, detectSingleFace\n // returns undefined and the chained landmark/expression/descriptor steps\n // short-circuit, so their shaders aren't compiled until the first real\n // frame containing a face.\n let input;\n if (this.videoElement && this.videoElement.readyState >= 2) {\n input = this.videoElement;\n }\n else {\n const canvas = document.createElement(\"canvas\");\n canvas.width = 224;\n canvas.height = 224;\n input = canvas;\n }\n faceLog(`warmup started (input: ${input instanceof HTMLVideoElement ? \"video\" : \"canvas\"})`);\n yield faceApiInstance\n .detectSingleFace(input, this.face_api_detection_options)\n .withFaceLandmarks()\n .withFaceExpressions()\n .withFaceDescriptor();\n warmedUp = true;\n faceLog(`warmup done in ${Math.round(performance.now() - startedAt)}ms`);\n }\n catch (err) {\n // best-effort: never block camera start if warmup fails\n faceLog(\"warmup failed (ignored):\", err);\n }\n });\n }\n init() {\n return __awaiter(this, void 0, void 0, function* () {\n // show loading spinner in full pg\n (0, components_1.createFullLoading)(this.cameraId, \"Initializing Camera...\");\n this.isDone = false;\n this.resizeElement();\n if (loadedModelState === \"NOT_LOADED\")\n yield this.loadFaceModel();\n // Wait until loadFaceModel is LOADED\n while (loadedModelState === \"LOADING\") {\n yield new Promise((resolve) => setTimeout(resolve, 500));\n }\n if (!this.iFrameRef && !this.isStartingDetection) {\n // warm up the inference pipeline while the spinner is still showing, so\n // detection is responsive the instant the countdown ends\n yield this.warmUpDetection();\n (0, components_1.hideFullLoading)(this.cameraId);\n yield this.startDetection();\n }\n });\n }\n listenToUserNoSmile(cb) {\n var _a;\n (_a = this.overlayElement) === null || _a === void 0 ? void 0 : _a.addEventListener(\"userNoSmile\", () => {\n cb();\n });\n }\n listenToUserSmiled(cb) {\n var _a;\n (_a = this.overlayElement) === null || _a === void 0 ? void 0 : _a.addEventListener(\"userSmiled\", () => {\n cb();\n });\n }\n listenToUserSmiledButNotLiveness(cb) {\n var _a;\n (_a = this.overlayElement) === null || _a === void 0 ? void 0 : _a.addEventListener(\"userSmiledButNotLiveness\", () => {\n cb();\n });\n }\n listenToLookStraight(cb) {\n var _a;\n (_a = this.overlayElement) === null || _a === void 0 ? void 0 : _a.addEventListener(\"lookStraight\", () => {\n cb();\n });\n }\n listenToPlaceFace(cb) {\n var _a;\n (_a = this.overlayElement) === null || _a === void 0 ? void 0 : _a.addEventListener(\"placeFace\", () => {\n cb();\n });\n }\n listenToFaceTooFar(cb) {\n var _a;\n (_a = this.overlayElement) === null || _a === void 0 ? void 0 : _a.addEventListener(\"faceTooFar\", () => {\n cb();\n });\n }\n}\nexports[\"default\"] = FACEDETECTIONSDK;\n\n\n//# sourceURL=webpack://PWUISDK/./src/plugins/face-detection.ts?");
4048
4048
 
4049
4049
  /***/ }),
4050
4050
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pipwave-ekyc-uikit",
3
- "version": "3.0.4",
3
+ "version": "3.0.5",
4
4
  "description": "",
5
5
  "main": "dist/pw-bundle.js",
6
6
  "scripts": {