aboutsummaryrefslogtreecommitdiffhomepage
path: root/public/bluebey-studio/src/faceArt.js
diff options
context:
space:
mode:
Diffstat (limited to 'public/bluebey-studio/src/faceArt.js')
-rw-r--r--public/bluebey-studio/src/faceArt.js89
1 files changed, 86 insertions, 3 deletions
diff --git a/public/bluebey-studio/src/faceArt.js b/public/bluebey-studio/src/faceArt.js
index 61bfa0d..1beedad 100644
--- a/public/bluebey-studio/src/faceArt.js
+++ b/public/bluebey-studio/src/faceArt.js
@@ -178,6 +178,22 @@ export const HEAD_MARK_LAYOUT = {
};
/**
+ * 鼻ちょうちん: the little snot bubble that grows and shrinks while someone sleeps.
+ *
+ * It hangs off the nose, which is a *separate mesh* in front of the plate, so the
+ * bubble sits out to the side of it (a bubble drawn straight below would run off
+ * the plate's bottom edge). The neck starts at the nose and reaches out to the
+ * bubble, one thin wisp, the way manga draws it.
+ */
+export const SNOT_LAYOUT = {
+ cx: 511.5, // the face midline, the same centre the mouth uses
+ cy: 336, // just below the eye row, level with the nose
+ offset: 122, // how far out to the side the bubble hangs, clear of the nose
+ radius: 48, // bubble radius at size 1
+ lineWidth: 6,
+};
+
+/**
* 眼鏡 / サングラス, drawn into the same texture as the eyes and brows.
*
* Not measured off any artwork - the character has no glasses - but the numbers
@@ -839,10 +855,77 @@ function drawHeadCover(ctx, spec, limits) {
* The shared extras (ほっぺ and 頭の模様) are drawn once per face, before the eyes,
* so the eyes, brows and glasses win any overlap and the cheeks are not doubled.
*/
-function drawFaceExtras(ctx, style) {
+function drawFaceExtras(ctx, style, lineOnly) {
drawCheeks(ctx, style.cheeks);
// The head covering fills out to the plate's real edges, so it needs `limits`.
drawHeadCover(ctx, style.headMark, style.limits ?? WINDOW_LIMITS);
+ drawSnotBubble(ctx, style.snot, lineOnly);
+}
+
+/**
+ * 鼻ちょうちん: a bubble hanging off the nose, joined to it by a thin neck. The
+ * neck starts at the nose (which the nose mesh hides, so it reads as growing out
+ * from under it) and swells into the round bubble out to the side. In a line
+ * drawing the bubble is left empty and only its outline is inked.
+ */
+function drawSnotBubble(ctx, spec, lineOnly) {
+ if (!spec?.enabled) return;
+ const size = clamp(spec.size ?? 1, 0.2, 3);
+ const dx = spec.offsetX ?? 0;
+ const dy = spec.offsetY ?? 0;
+ const r = SNOT_LAYOUT.radius * size;
+ const cx = SNOT_LAYOUT.cx + SNOT_LAYOUT.offset + dx;
+ const cy = SNOT_LAYOUT.cy + dy;
+ const color = spec.color ?? '#e3ecff';
+ const line = spec.line ?? '#55386e';
+ const lw = Math.max(1, SNOT_LAYOUT.lineWidth * size);
+
+ // The nose end sits on the midline; the bubble hangs out to one side.
+ const nx = SNOT_LAYOUT.cx + dx;
+ const ny = SNOT_LAYOUT.cy + dy - r * 0.1;
+ const dist = Math.hypot(cx - nx, cy - ny) || 1;
+ const ux = (cx - nx) / dist;
+ const uy = (cy - ny) / dist;
+ // Where the neck meets the bubble: a little inside its edge, so the two merge.
+ const reach = Math.max(0, dist - r * 0.72);
+ const ex = nx + ux * reach;
+ const ey = ny + uy * reach;
+
+ ctx.save();
+ ctx.lineJoin = 'round';
+ ctx.lineCap = 'round';
+ // The neck: a wedge from a narrow end at the nose to a wide end at the bubble.
+ const px = -uy;
+ const py = ux;
+ const nW = lw * 0.55;
+ const bW = r * 0.5;
+ ctx.beginPath();
+ ctx.moveTo(nx + px * nW, ny + py * nW);
+ ctx.lineTo(ex + px * bW, ey + py * bW);
+ ctx.lineTo(ex - px * bW, ey - py * bW);
+ ctx.lineTo(nx - px * nW, ny - py * nW);
+ ctx.closePath();
+ if (lineOnly) {
+ ctx.strokeStyle = line;
+ ctx.lineWidth = lw;
+ ctx.stroke();
+ } else {
+ ctx.fillStyle = color;
+ ctx.fill();
+ ctx.strokeStyle = line;
+ ctx.lineWidth = lw * 0.7;
+ ctx.stroke();
+ }
+ // The bubble itself, over the neck's wide end.
+ circlePath(ctx, cx, cy, r);
+ if (!lineOnly) {
+ ctx.fillStyle = color;
+ ctx.fill();
+ }
+ ctx.strokeStyle = line;
+ ctx.lineWidth = lw;
+ ctx.stroke();
+ ctx.restore();
}
/**
@@ -1030,8 +1113,8 @@ export function drawEyes(ctx, p) {
const lowerLid = clamp(style.lowerLid ?? 0, 0, 1);
// ほっぺ and 頭の模様 belong to the whole face, so they are drawn once here
- // rather than inside the per-eye loop below.
- drawFaceExtras(ctx, style);
+ // rather than inside the per-eye loop below. 鼻ちょうちん comes along too.
+ drawFaceExtras(ctx, style, line);
for (const layout of EYE_LAYOUT.eyes) {
const spec = (p.eyes ?? {})[layout.key] ?? {};