UNPKG

@needle-tools/facefilter

Version:

Needle Engine FaceFilter

167 lines (146 loc) 7.57 kB
import { isDevEnvironment, onStart, showBalloonError, Vec3 } from "@needle-tools/engine"; import { FaceFilterRoot, FaceMeshTexture, NeedleFaceFilterTrackingManager } from ".."; import { FaceLayout } from "./facemesh/utils.facemesh"; /** * Use <needle-engine> html attributes to create a facemask */ onStart(async ctx => { let facefilterValue = ctx.domElement.getAttribute("face-filter"); if (facefilterValue) { console.debug("Face filter detected", facefilterValue, { isImage: isImage(facefilterValue), isModel: isModel(facefilterValue) }); // Setup filter let filter: FaceFilterRoot | FaceMeshTexture | null = null; let manager: NeedleFaceFilterTrackingManager | null = null; let maxFaces = 1; const maxFacesAttribute = ctx.domElement.getAttribute("face-filter-max-faces"); if (maxFacesAttribute) { const number = parseInt(maxFacesAttribute); console.debug(`Setting max faces to ${number}`); if (!isNaN(number)) { maxFaces = number; } else console.warn(`Invalid value for "face-filter-max-faces" attribute: "${maxFacesAttribute}". Expected a number`); } if (isImage(facefilterValue)) { let layout: FaceLayout | null | undefined = ctx.domElement.getAttribute("face-filter-layout") as any;; switch (layout) { case "procreate": case "mediapipe": case "canonical": // Do nothing break; default: if (layout) { const msg = `Invalid value for "face-filter-layout" attribute: "${layout}". Supported values are: "procreate", "mediapipe", "canonical"`; console.warn(msg); if (isDevEnvironment()) showBalloonError(msg); } layout = "mediapipe"; break; } console.debug(`Selected facefilter layout: ${layout}. Use "face-filter-layout" attribute to change layout`); let faceFilterMask: string | null | undefined = ctx.domElement.getAttribute("face-filter-mask"); if (!faceFilterMask || !isImage(faceFilterMask)) { faceFilterMask = undefined; } manager = ctx.scene.addComponent(NeedleFaceFilterTrackingManager, { maxFaces: maxFaces }); filter = new FaceMeshTexture({ layout: layout, texture: { url: facefilterValue }, mask: { url: faceFilterMask } }); manager.activateFilter(filter); } else if (isModel(facefilterValue)) { let facefilterScale: string | null | number = ctx.domElement.getAttribute("face-filter-scale"); let facefilterOffsetAttribute: string | null = ctx.domElement.getAttribute("face-filter-offset"); const faceFilterOffset = { x: 0, y: 0, z: 0 } if (typeof facefilterScale === "string") { facefilterScale = parseFloat(facefilterScale); } if (typeof facefilterOffsetAttribute === "string") { const values = facefilterOffsetAttribute.split(",").map(v => parseFloat(v)); if (values.length === 3) { faceFilterOffset.x = values[0] || 0; faceFilterOffset.y = values[1] || 0; faceFilterOffset.z = values[2] || 0; } } manager = ctx.scene.addComponent(NeedleFaceFilterTrackingManager, { maxFaces: maxFaces }); filter = await FaceFilterRoot.create(facefilterValue, { scale: Number.isNaN(facefilterScale) ? 1 : facefilterScale || 1, offset: faceFilterOffset, }); if (filter) { filter.gameObject.visible = false; manager.activateFilter(filter); } } else { const msg = `Value for "face-filter" attribute is not a valid image or model: "${facefilterValue}". Please provide a valid image or model url (Supported formats: .jpeg, .jpg, .png, .webp, .glb, .gltf, .fbx, .obj)`; console.error(msg); if (isDevEnvironment()) showBalloonError(msg); return; } updateShowVideo(manager, ctx.domElement); const videoSelector = ctx.domElement.getAttribute("face-filter-video-selector"); if(videoSelector) { const videoElement = document.querySelector(videoSelector) as HTMLVideoElement; if(!videoElement) console.error(`Video element not found for selector "${videoSelector}"`); else { console.debug(`Using video element "${videoSelector}"`); manager.video = videoElement; } } // Handle runtime updating of the filter attribute const observer = new MutationObserver(function (mutations) { mutations.forEach(mutation => { switch (mutation.type) { case "attributes": switch (mutation.attributeName) { case "face-filter": { const newValue = ctx.domElement.getAttribute("face-filter"); if (newValue !== facefilterValue) { console.debug(`Face filter changed from "${facefilterValue}" to "${newValue}"`); if (filter instanceof FaceMeshTexture) { if (newValue && isImage(newValue)) { filter.updateTexture(newValue); for (const obj of manager.getActiveFaceObjects()) { const faceMesh = obj.instance?.getComponentInChildren(FaceMeshTexture); faceMesh?.updateTexture(newValue); } } } } } break; case "face-filter-video": { updateShowVideo(manager, ctx.domElement); } break; } } }) }); observer.observe(ctx.domElement, { attributes: true }); } }); function updateShowVideo(filter: NeedleFaceFilterTrackingManager, domElement:HTMLElement) { const attributeValue = domElement.getAttribute("face-filter-show-video"); if(attributeValue === null || attributeValue === undefined) { filter.showVideo = true; } else { filter.showVideo = attributeValue === "true" || attributeValue === "1" || attributeValue === "yes" || attributeValue === "on" || attributeValue?.length === 0; } } function isImage(str: string) { return str.toLowerCase().match(/\.(jpeg|jpg|png|webp)$/); } function isModel(str: string) { return str.toLowerCase().match(/\.(glb|gltf|fbx|obj)$/) || str.includes("cloud.needle.tools"); }