@needle-tools/facefilter
Version:
Needle Engine FaceFilter
167 lines (146 loc) • 7.57 kB
text/typescript
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");
}