function faceInput({
size = 250,
strokeWidth = 2,
include = new Set(["leftEye", "rightEye", "nose", "mouth"])
} = {}) {
const viewBoxSize = 250;
let svgParts = `<svg width="${size}" height="${size}" viewBox="0 0 ${viewBoxSize} ${viewBoxSize}">
<style>
svg {
background: #f5f5f5;
border: 1px solid #ddd;
}
svg:hover {
border: 1px solid #bbb;
}
svg * {
vector-effect: non-scaling-stroke;
}
.drag {
stroke: black;
cursor: grab;
stroke-linecap: round;
stroke-linejoin: round;
stroke-width: ${strokeWidth};
filter: drop-shadow(3px 3px 2px rgba(0, 0, 0, .2));
transform-box: fill-box;
transform-origin: left;
}
.drag.active {
cursor: grabbing;
filter: drop-shadow(3px 3px 2px rgba(0, 0, 0, .4));
}
</style>`;
if (include.has("leftEye")) {
svgParts += `
<g id="leftEye" class="drag">
<ellipse cx="36" cy="36" rx="35" ry="20" fill="#fff" />
<circle cx="42" cy="36" r="14" fill="#ddd" />
<circle cx="42" cy="36" r="7" fill="#000" />
</g>`;
}
if (include.has("rightEye")) {
svgParts += `
<g id="rightEye" class="drag">
<ellipse cx="36" cy="36" rx="35" ry="20" fill="#fff" />
<circle cx="42" cy="36" r="14" fill="#ddd" />
<circle cx="42" cy="36" r="7" fill="#000" />
</g>`;
}
if (include.has("nose")) {
svgParts += `
<g id="nose" class="drag">
<g fill="#fff" stroke="none">
<path d="M46.0495,41.9032c-1.0781-3.8065-4.3718-14.867-3.1714-30.0282h-13.391 c1.2004,15.1611-2.0933,26.2217-3.1714,30.0282"/>
<path d="M50.25,36.7078c7.5833,4.6667,2.8334,12.7522-1.6875,14.0214C44.6425,51.8296,46,60.0682,36,60.0417 c-10,0.0265-8.6425-8.212-12.5625-9.3125c-4.5209-1.2691-9.2818-9.3547-1.6984-14.0214"/>
</g>
<g fill="none">
<path d="M50.25,37.7083c7.5833,4.6667,2.8334,11.7517-1.6875,13.0208C44.6425,51.8296,46,60.0682,36,60.0417 c-10,0.0265-8.6425-8.212-12.5625-9.3125C18.9166,49.46,14.1667,42.375,21.75,37.7083"/>
<path d="M29.4871,12.875c1.2004,14.6562-2.0933,25.3484-3.1714,29.0282M42.8781,12.875c-1.2004,14.6562,2.0933,25.3484,3.1714,29.0282"/>
</g>
</g>`;
}
if (include.has("mouth")) {
svgParts += `
<g id="mouth" class="drag">
<g fill="#fff">
<path d="m64.05 34.59c-4.56-2.963-9.963-9.286-15.69-14.31-1.328-1.169-2.925-2.567-5.85-1.169l-3.851 2.05c-0.8882 0.5138-1.191 0.6898-2.663 0.6898s-1.775-0.176-2.663-0.6898l-3.851-2.05c-2.925-1.399-4.522 0-5.85 1.169-5.722 5.02-11.13 11.34-15.69 14.31l15.02 14.36c2.529 2.452 5.827 4.522 13.03 4.522s10.5-2.069 13.03-4.522z"/>
<path d="m36 39.06c-7.667 0-9.791-4.905-13.87-4.292 6.763-5.096 10.48-0.6131 13.87-0.6131s7.108-4.483 13.87 0.6131c-4.081-0.6131-6.204 4.292-13.87 4.292z"/>
</g>
<g fill="none">
<path d="m8.167 34.6c4.981 1.66 6.255 1.889 14.07 0.1774"/>
<path d="m63.83 34.6c-4.981 1.66-6.255 1.889-14.07 0.1774"/>
<path d="m8.167 34.6c4.981 1.66 6.255 1.889 14.07 0.1774"/>
<path d="m8.167 34.6 14.91 14.25c2.51 2.434 5.782 4.487 12.93 4.487s10.42-2.053 12.93-4.487l14.91-14.25"/>
<path d="m8.167 34.6c4.525-2.941 9.886-9.214 15.56-14.2 1.318-1.16 2.902-2.548 5.805-1.16l3.821 2.034c0.8813 0.5098 1.182 0.6844 2.643 0.6844s1.761-0.1746 2.643-0.6844l3.821-2.034c2.902-1.388 4.487 0 5.805 1.16 5.678 4.981 11.04 11.25 15.56 14.2"/>
<path d="m63.83 34.6c-4.981 1.66-6.255 1.889-14.07 0.1774"/>
<path d="m36 39.03c-7.608 0-9.715-4.867-13.76-4.259 6.711-5.057 10.4-0.6084 13.76-0.6084s7.053-4.449 13.76 0.6084c-4.049-0.6084-6.156 4.259-13.76 4.259z"/>
</g>
</g>`;
}
if (include.has("rightEar")) {
svgParts += `
<g id="rightEar" class="drag">
<path fill="#fff" d="M26.125,19.625C30.75,2.0833,61,7.875,49.6875,35.9583c-14.5919,35.7917-31.875,25-28.8125,13.3229"/>
<path fill="#ddd" d="M35.1031,28C39.5417,35,42.5,41.5417,36.5,44.25c-1.3969,0.5735-2.8765,0.4132-4.3824,1.1735 c-1.5343,0.7223-1.8192,1.3265-3.6385,3.2848c-2.5625,2.0625-5.7708-1.5465-3.9583-3.6875 c3.0729-3.3958,6.4479-3.9271,4.6667-7.2083c-1.2187-2.2047-0.4062-5.2812,3.5-4.1562"/>
<path fill="#ddd" stroke="none" d="M45.6875,35.6144C49.3512,13.7369,31.125,18.375,29.6819,23.9375 C33.6562,8.0625,53.7734,14.3451,45.6875,35.6144z"/>
<path fill="none" d="M29.6819,23.9375c3.9744-15.875,24.8326-10.2582,15.2399,11.1183"/>
</g>`;
}
if (include.has("leftEar")) {
svgParts += `
<g id="leftEar" class="drag" >
<path fill="#fff" d="m 46.596194,19.625 c -4.625,-17.5417 -34.875,-11.75 -23.5625,16.3333 14.5919,35.7917 31.875,25 28.8125,13.3229" />
<path fill="#ddd" d="m 37.618094,28 c -4.4386,7 -7.3969,13.5417 -1.3969,16.25 1.3969,0.5735 2.8765,0.4132 4.3824,1.1735 1.5343,0.7223 1.8192,1.3265 3.6385,3.2848 2.5625,2.0625 5.7708,-1.5465 3.9583,-3.6875 -3.0729,-3.3958 -6.4479,-3.9271 -4.6667,-7.2083 1.2187,-2.2047 0.4062,-5.2812 -3.5,-4.1562" />
<path fill="#ddd" stroke="none" d="m 27.033694,35.6144 c -3.6637,-21.8775 14.5625,-17.2394 16.0056,-11.6769 -3.9743,-15.875 -24.0915,-9.5924 -16.0056,11.6769 z" />
<path fill="none" d="m 43.039294,23.9375 c -3.9744,-15.875 -24.8326,-10.2582 -15.2399,11.1183" />
</g>`;
}
if (include.has("leftBrow")) {
svgParts += `
<g id="leftBrow" class="drag">
<path fill="#fff" d="m 84.999934,32.359124 c 0.669003,2.69758 -0.332542,6.371495 -3.336528,7.108255 -11.404252,-0.775372 -22.205105,-6.072853 -33.77519,-5.182478 -7.968286,0.459461 -15.677388,3.65832 -21.868864,8.651709 -2.732177,0.18506 0.676445,-2.832106 1.457766,-3.627133 7.922645,-8.38892 19.29282,-13.392604 30.840384,-13.484562 7.92543,-0.275937 15.92786,0.710364 23.497902,3.086263 1.488061,0.596029 2.989589,1.742416 3.18453,3.447946 z"
id="path6902" />
</g>`;
}
if (include.has("rightBrow")) {
svgParts += `
<g id="rightBrow" class="drag">
<path fill="#fff" d="m 25.168207,32.359124 c -0.669003,2.69758 0.332542,6.371495 3.336528,7.108255 11.404252,-0.775372 22.205105,-6.072853 33.77519,-5.182478 7.968286,0.459461 15.677388,3.65832 21.868864,8.651709 2.732177,0.18506 -0.676445,-2.832106 -1.457766,-3.627133 -7.922645,-8.38892 -19.29282,-13.392604 -30.840384,-13.484562 -7.92543,-0.275937 -15.92786,0.710364 -23.497902,3.086263 -1.488061,0.596029 -2.989589,1.742416 -3.18453,3.447946 z" />
</g>
</svg>`;
}
const root = svg`${svgParts}`;
let initialValue = {
leftEye: { x: 45, y: 70 },
rightEye: { x: 135, y: 70 },
nose: { x: 90, y: 115 },
mouth: { x: 90, y: 170 },
leftEar: { x: 0, y: 115 },
rightEar: { x: 180, y: 115 },
leftBrow: { x: 20, y: 35 },
rightBrow: { x: 120, y: 35 }
};
[...Object.entries(initialValue)].forEach(function ([key, value]) {
if (!include.has(key)) {
delete initialValue[key];
}
});
root.value = Object.assign({}, initialValue);
const draggableElements = root.querySelectorAll(".drag");
draggableElements.forEach((element) => {
const id = element.getAttribute("id");
setValue(element, id, initialValue[id].x, initialValue[id].y);
});
function setValue(element, id, x, y) {
setTransform(element, "translate", x, y);
root.value[id] = {
x: (x - initialValue[id].x) / viewBoxSize,
y: (y - initialValue[id].y) / viewBoxSize
};
root.dispatchEvent(new CustomEvent("input"));
}
function getMousePosition(evt) {
const CTM = root.getScreenCTM();
let x, y;
if (evt.touches) {
x = evt.touches[0].clientX;
y = evt.touches[0].clientY;
} else {
x = evt.clientX;
y = evt.clientY;
}
return {
x: (x - CTM.e) / CTM.a,
y: (y - CTM.f) / CTM.d
};
}
function getTransformCoordinates(element) {
const transform = element.getAttribute("transform");
if (!transform) return { x: 0, y: 0 };
const match = transform.match(/translate\(([^,]+),([^)]+)\)/);
if (match) {
return {
x: parseFloat(match[1]),
y: parseFloat(match[2])
};
}
return { x: 0, y: 0 };
}
function setTransform(element, type, ...values) {
const currentTransform = element.getAttribute("transform") || "";
const newTransformValue = `${type}(${values.join(",")})`;
if (!currentTransform) {
element.setAttribute("transform", newTransformValue);
return;
}
const regex = new RegExp(`${type}\\([^)]*\\)`);
const newTransform = regex.test(currentTransform)
? currentTransform.replace(regex, newTransformValue)
: `${currentTransform} ${newTransformValue}`;
element.setAttribute("transform", newTransform);
}
function handleDrag(element) {
let isDragging = false;
let offset = { x: 0, y: 0 };
let id = element.getAttribute("id");
function onMove(evt) {
if (!isDragging) return;
evt.preventDefault();
const mousePos = getMousePosition(evt);
const newX = mousePos.x - offset.x;
const newY = mousePos.y - offset.y;
setValue(element, id, newX, newY);
}
function onEnd() {
if (!isDragging) return;
isDragging = false;
element.classList.remove("active");
root.removeEventListener("mousemove", onMove);
root.removeEventListener("mouseup", onEnd);
root.removeEventListener("touchmove", onMove);
root.removeEventListener("touchend", onEnd);
root.removeEventListener("touchcancel", onEnd);
}
function onStart(evt) {
if (evt.type === "mousedown" && evt.button !== 0) return;
if (evt.type === "touchstart" && evt.touches.length !== 1) return;
isDragging = true;
evt.preventDefault();
element.classList.add("active");
const mousePos = getMousePosition(evt);
const currentPos = getTransformCoordinates(element);
offset.x = mousePos.x - currentPos.x;
offset.y = mousePos.y - currentPos.y;
root.addEventListener("mousemove", onMove);
root.addEventListener("mouseup", onEnd);
root.addEventListener("touchmove", onMove, { passive: false });
root.addEventListener("touchend", onEnd);
root.addEventListener("touchcancel", onEnd);
}
element.addEventListener("mousedown", onStart);
element.addEventListener("touchstart", onStart, { passive: false });
}
draggableElements.forEach(handleDrag);
return root;
}