function generateFace({
debug = false,
width = 150,
height = 200,
features = ["mouth", "eyes", "nose", "hair"],
fill = "hsl(0,0%,95%)",
stroke = "hsl(0,0%,5%)",
strokeWidth = 1.5,
strokeLinejoin = "round",
strokeLinecap = "round",
noseWidth = 1,
noseWobble = 0.5,
formity = 0.5,
happiness = 0,
hairStyle = "flat",
hairVariation = 0.5
} = {}) {
const aspectRatio = width / height;
const happinessUV = happiness * 0.5 + 0.5;
const verticalDivisions = 3;
const verticalDivHeight = height / verticalDivisions;
const eyelineDivisions = 5;
const eyelineY = verticalDivHeight + (verticalDivHeight * 1) / 4;
const eyeWidth = width / eyelineDivisions;
const earWidth = (eyeWidth * 2) / 5;
const earHeight = (verticalDivHeight * 2) / 4;
const mouthLine = verticalDivHeight * 2 + verticalDivHeight / 3;
const tickSize = 10;
const drawSkin = () => {
const faceWidth = width - 2 * earWidth;
const dCx = (formity * faceWidth) / 5;
return htl.svg`<g transform="translate(${earWidth},0)">
<path
d="M${faceWidth / 2},0
Q${faceWidth},0 ${faceWidth},${height / 2}
Q${faceWidth - dCx},${height} ${faceWidth / 2},${height}
Q${dCx},${height} 0,${height / 2}
Q0,0 ${faceWidth / 2},0
Z"
fill=${fill}
></path>
</g>`;
};
const drawEars = () => {
const earDebugStroke = debug ? "red" : "none";
const earDx = m.lerp(0, earWidth / 2, formity);
const d = `M${earWidth * 1.5},${eyelineY}
Q${earWidth * 1.5},${verticalDivHeight}
${earWidth / 2},${verticalDivHeight}
Q${0},${verticalDivHeight} ${0},${eyelineY}
Q${earDx},${verticalDivHeight * 2}
${earWidth * 1.75},${eyelineY + verticalDivHeight * 0.25}
Z`;
return htl.svg`<g class="ears">
<path
d=${d}
fill=${fill}
></path>
<g transform="scale(-1,1) translate(${-width},0) ">
<path
d=${d}
fill=${fill}
></path>
</g>
</g>`;
};
const drawEyes = () => {
if (!features.includes("eyes")) return;
const eyeY = eyelineY;
const eyeXFromMid = (eyeWidth * 3) / 4;
const eyeDx = m.lerp(0, eyeWidth / 5, happinessUV);
const eyeStrokeWidth = m.lerp(strokeWidth * 2, strokeWidth, happinessUV);
return htl.svg`<g class="eyes">
<line
x1=${width / 2 - eyeXFromMid - eyeDx}
y1=${eyeY}
x2=${width / 2 - eyeXFromMid + eyeDx}
y2=${eyeY}
fill="none"
stroke=${stroke}
stroke-width=${eyeStrokeWidth}
stroke-linejoin=${strokeLinejoin}
stroke-linecap=${strokeLinecap}
></line>
<line
x1=${width / 2 + eyeXFromMid - eyeDx}
y1=${eyeY}
x2=${width / 2 + eyeXFromMid + eyeDx}
y2=${eyeY}
fill="none"
stroke=${stroke}
stroke-width=${eyeStrokeWidth}
stroke-linejoin=${strokeLinejoin}
stroke-linecap=${strokeLinecap}
></line>
</g>`;
};
const drawNose = () => {
if (!features.includes("nose")) return;
const noseWidthDX = m.lerp(-eyeWidth, eyeWidth, noseWidth);
const noseCx = width / 2;
const noseY1 = eyelineY;
const noseY2 = 2 * verticalDivHeight;
const dy = m.lerp(0, verticalDivHeight / 5, noseWobble);
// if (Math.abs(noseWidth * 2 - 1) < 0.25) return;
return htl.svg`<g class="nose">
<path
d="M${noseCx},${noseY1}
Q ${noseCx - noseWidthDX},${noseY2 + dy} ${noseCx},${noseY2}"
r=20
fill="none"
stroke=${stroke}
stroke-width=${strokeWidth}
stroke-linejoin=${strokeLinejoin}
stroke-linecap=${strokeLinecap}
></path>
</g>`;
};
const drawMouth = () => {
if (!features.includes("mouth")) return;
const cx = width / 2;
const mouthY = mouthLine;
const mouthDy =
happiness < 0
? m.lerp(0, -verticalDivHeight / 3, Math.abs(happiness))
: m.lerp(0, verticalDivHeight / 2, Math.abs(happiness));
const mouthDx = m.lerp(eyeWidth * 0.25, eyeWidth * 0.85, happinessUV);
return htl.svg` <path
class="mouth"
d="M${cx - mouthDx},${mouthY}
Q ${cx},${mouthY + mouthDy} ${cx + mouthDx},${mouthY}"
r=20
fill="none"
stroke=${stroke}
stroke-width=${strokeWidth}
stroke-linejoin=${strokeLinejoin}
stroke-linecap=${strokeLinecap}
></path>`;
};
const drawHair = () => {
if (!features.includes("hair")) return;
if (hairStyle === "flat") {
const hairY = m.lerp(
verticalDivHeight * 0.25,
verticalDivHeight * 0.75,
hairVariation
);
return htl.svg`<line class="hair"
x1=${earWidth}
y1=${hairY}
x2=${width - earWidth}
y2=${hairY}
fill="none"
stroke=${stroke}
stroke-width=${strokeWidth}
></line>`;
} else if (hairStyle === "parted") {
const hairYMin = verticalDivHeight * 0.25;
const hairPartitionX = m.lerp(
earWidth * 3,
width - earWidth * 3,
hairVariation
);
return htl.svg`<path class="hair"
d="M${earWidth},${verticalDivHeight - strokeWidth / 2}
A ${hairPartitionX - earWidth}
${verticalDivHeight - hairYMin} 0 0 0 ${hairPartitionX},${hairYMin}
A ${width - earWidth - hairPartitionX}
${verticalDivHeight - hairYMin}
0 0 0 ${width - earWidth},${verticalDivHeight - strokeWidth / 2}"
fill="none"
stroke=${stroke}
stroke-width=${strokeWidth}
stroke-linejoin=${strokeLinejoin}
stroke-linecap=${strokeLinecap}
></path>`;
} else if (hairStyle === "wavy") {
const hairYMin = m.lerp(
verticalDivHeight * 0.8,
verticalDivHeight * 0.9,
hairVariation
);
const hairYMax = verticalDivHeight * 1.25 - strokeWidth / 2;
const hairPartitionX = m.lerp(
earWidth * 1.333,
width - earWidth * 1.333,
hairVariation
);
const partions = 2 * Math.floor(m.lerp(3, 6, hairVariation));
const points = m
.linspace(partions + 1, true)
.map((u, i) => [
earWidth + u * (width - 2 * earWidth),
i % 2 === 0 ? hairYMin : hairYMax
]);
const path = d3.path();
points.forEach((p, i) => {
if (i === 0) {
path.moveTo(...p);
} else if (i % 2 !== 0) {
} else {
const cx = points[i - 1][0];
const cy = points[i - 1][1];
path.quadraticCurveTo(cx, cy, ...p);
}
});
const d = path.toString();
return htl.svg`<path class="hair"
d=${d}
fill="none"
stroke=${stroke}
stroke-width=${strokeWidth}
stroke-linejoin=${strokeLinejoin}
stroke-linecap=${strokeLinecap}
></path>`;
}
};
let debugEls;
if (debug) {
debugEls = htl.svg`<g class="debug">
<rect width=${width} height=${height} stroke="#ff0" fill="none"></rect>
${d3.range(verticalDivisions - 1).map(
(i) =>
htl.svg`<line
x1=0
y1=${(i + 1) * verticalDivHeight}
x2=${width}
y2=${(i + 1) * verticalDivHeight}
stroke="#f0f"
stroke-width=${strokeWidth}
fill="none"></rect>`
)}
<line
x1="0" y1=${eyelineY}
x2=${width} y2=${eyelineY}
stroke="#0ff"
stroke-width=${strokeWidth}
></line>
${d3.range(eyelineDivisions + 1).map(
(i) => htl.svg`<line
x1=${i * eyeWidth}
y1=${eyelineY - tickSize / 2}
x2=${i * eyeWidth}
y2=${eyelineY + tickSize / 2}
stroke="#0ff"
stroke-width=${strokeWidth}></line>`
)}
<line
x1=${eyeWidth * 1.5}
y1=${mouthLine}
x2=${width - eyeWidth * 1.5}
y2=${mouthLine}
stroke="#0ff"
stroke-width=${strokeWidth}
></line>
<path
d="M${width / 2},${eyelineY}
L${eyeWidth * 2},${2 * verticalDivHeight}
L${eyeWidth * 3},${2 * verticalDivHeight}z"
fill="none"
stroke="#0ff"
stroke-width=${strokeWidth}
></path>
</g>`;
}
return htl.svg`<g class="face">
${drawSkin()}
${drawEars()}
${drawHair()}
${drawNose()}
${drawEyes()}
${drawMouth()}
${debugEls}
</g>`;
}