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.js80
1 files changed, 80 insertions, 0 deletions
diff --git a/public/bluebey-studio/src/faceArt.js b/public/bluebey-studio/src/faceArt.js
index e999045..1083e52 100644
--- a/public/bluebey-studio/src/faceArt.js
+++ b/public/bluebey-studio/src/faceArt.js
@@ -178,6 +178,26 @@ export const HEAD_MARK_LAYOUT = {
};
/**
+ * 頭の模様 on the *hair plate*: the head's cover when the model carries a plate
+ * that wraps all the way round (material `hair-plate`, cylindrical UV).
+ *
+ * The front plate only reaches the head's silhouette, so hair drawn on it stops
+ * at the front half. The hair plate's UV is `u` = angle round the head (front at
+ * 0.5, back at the seam 0/1) and `v` = height (0 at the model's feet, 1 at the
+ * top of the head), so the covering is everything *above* a boundary line: a cap
+ * that is visible from behind as well.
+ */
+export const HAIR_WRAP_LAYOUT = {
+ // The wrap's v runs top-of-head (0) to the model's feet (1) after the GLB's
+ // V-flip, so `base` is how far down from the top the hairline sits at size 1.
+ // `reach` is how much a unit of `size` moves it; `offsetScale` converts the
+ // `offsetY` slider (artwork px) to the same units, so one slider drives both.
+ base: 0.30,
+ reach: 0.30,
+ offsetScale: 0.30 / 190,
+};
+
+/**
* ひげ: the mustaches and beards that hang under the nose. Drawn on the eye plate
* at the nose, the same spot the snot bubble uses. `shape` picks a well-known
* kind; `size` scales it about the nose, and the colours are adjustable.
@@ -861,6 +881,66 @@ function drawHeadCover(ctx, spec, limits) {
}
/**
+ * 頭の模様 drawn onto the hair plate, which wraps right round the head.
+ *
+ * The plate's canvas is the cylindrical UV unrolled: `x` is the angle (front at
+ * the middle, the seam at the back at both ends) and `y` is the height. The GLB
+ * flips V on export, so the top of the head lands at the top of the canvas and
+ * the covering is everything above the boundary line - the same direction as
+ * `drawHeadCover`, which fills down from the top edge of the front plate.
+ *
+ * The same `shape` names are reused, with their front-to-back modulation tied to
+ * how far the column leans towards the front (`t`).
+ */
+export function drawHeadCoverWrap(ctx, spec, layout) {
+ const shape = spec?.shape ?? 'off';
+ // `split` is a retired front-plate shape; it has no wrap equivalent.
+ if (shape === 'off' || shape === 'none' || shape === 'split') return;
+
+ const size = clamp(spec.size ?? 1, 0.02, 3);
+ const W = layout.width;
+ const H = layout.height;
+ // After the GLB's V-flip the wrap's v is 0 at the top of the head and 1 at the
+ // model's feet, so a *larger* boundary means more hair (it reaches further
+ // down) - the same direction as `size` and `offsetY` on the front plate.
+ const base = HAIR_WRAP_LAYOUT.base
+ + (size - 1) * HAIR_WRAP_LAYOUT.reach
+ + clamp(spec.offsetY ?? 0, -600, 600) * HAIR_WRAP_LAYOUT.offsetScale;
+ const color = spec.color ?? '#8a4fe0';
+
+ ctx.save();
+ ctx.beginPath();
+ ctx.rect(0, 0, W, H);
+ ctx.clip();
+
+ // `t` is -1..1 across the front (+-90 deg around u = 0.5); everything behind is
+ // pinned to +-1, so the hairline runs flat round the back of the head.
+ const half = W * 0.25;
+ const cx = W / 2 + (clamp(spec.offsetX ?? 0, -600, 600) / 512) * half;
+ const vBound = (x) => {
+ const t = clamp((x - cx) / half, -1, 1);
+ if (shape === 'fringe') return base + 0.14 * (1 - Math.abs(t)) - 0.05 * Math.abs(Math.sin(t * Math.PI));
+ if (shape === 'fringe-up') return base - 0.14 * (1 - Math.abs(t)) + 0.05 * Math.abs(Math.sin(t * Math.PI));
+ if (shape === 'wave') return base + 0.10 * Math.sin(t * Math.PI * 0.9 + 0.5);
+ if (shape === 'side') return base + 0.20 * t;
+ return base - 0.07 * (1 - t * t); // `cap`
+ };
+
+ const steps = 96;
+ ctx.beginPath();
+ ctx.moveTo(0, 0);
+ for (let i = 0; i <= steps; i += 1) {
+ const x = (W * i) / steps;
+ ctx.lineTo(x, clamp(vBound(x), 0, 1) * H);
+ }
+ ctx.lineTo(W, 0);
+ ctx.closePath();
+ ctx.fillStyle = color;
+ ctx.fill();
+ ctx.restore();
+}
+
+/**
* 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.
*/