aboutsummaryrefslogtreecommitdiffhomepage
path: root/public/bluebey-studio
diff options
context:
space:
mode:
authorYasutake Yohei <61961825+yasutakeyohei@users.noreply.github.com>2026-10-04 00:54:43 +0900
committerYasutake Yohei <61961825+yasutakeyohei@users.noreply.github.com>2026-10-04 00:54:43 +0900
commite9b8f3194c8604f5d0826df1b1e6a17cf27f68f7 (patch)
tree28d551e5b5f855e71eeb3e025af5b65d29037693 /public/bluebey-studio
parent8c430aefc24545c100b34acea2824d837f5e0337 (diff)
bluebey-studio: 髪の生え際をなめらかにし、カーブとわけめを作り直す
髪のテクスチャを2倍解像度・曲線の分割を細かくして生え際のギザつきを解消。カーブを深い丸カバーに。わけめは正面側だけで非対称にし、継ぎ目で戻すことで背中の段差を解消。分け目はギャップで表現。
Diffstat (limited to 'public/bluebey-studio')
-rw-r--r--public/bluebey-studio/src/face.js19
-rw-r--r--public/bluebey-studio/src/faceArt.js35
2 files changed, 43 insertions, 11 deletions
diff --git a/public/bluebey-studio/src/face.js b/public/bluebey-studio/src/face.js
index e3063f2..f2440d3 100644
--- a/public/bluebey-studio/src/face.js
+++ b/public/bluebey-studio/src/face.js
@@ -1,6 +1,12 @@
import * as THREE from 'three';
import { drawEyes, drawMouth, drawHeadCoverWrap, canvasLayout } from './faceArt.js';
+// The hair plate's texture is drawn at this multiple of the plate's natural UV
+// size. The hairline crosses the head over only a fraction of that height, so at
+// 1x its edge comes out visibly jagged; doubling it (with the finer curve
+// subdivision in `drawHeadCoverWrap`) keeps the edge smooth.
+const HAIR_SCALE = 2;
+
/**
* Owns the two procedural textures (eyes and mouth) and keeps them in sync with
* the face parameters.
@@ -48,7 +54,7 @@ export class Face {
this.beardImages = {};
this.eyeCanvas = makeCanvas(this.eyeLayout);
this.mouthCanvas = makeCanvas(this.mouthLayout);
- this.hairCanvas = this.hairLayout ? makeCanvas(this.hairLayout) : null;
+ this.hairCanvas = this.hairLayout ? makeCanvas(this.hairLayout, HAIR_SCALE) : null;
this.eyeCtx = this.eyeCanvas.getContext('2d');
this.mouthCtx = this.mouthCanvas.getContext('2d');
this.hairCtx = this.hairCanvas ? this.hairCanvas.getContext('2d') : null;
@@ -264,10 +270,13 @@ export class Face {
this.hairMesh.renderOrder = p.eyes.headMark?.overEyes === false ? 0 : 3;
const layout = this.hairLayout;
const ctx = this.hairCtx;
- ctx.clearRect(0, 0, layout.width, layout.height);
+ ctx.setTransform(1, 0, 0, 1, 0, 0);
+ ctx.clearRect(0, 0, layout.width * HAIR_SCALE, layout.height * HAIR_SCALE);
+ ctx.scale(HAIR_SCALE, HAIR_SCALE);
// `drawHeadCoverWrap` works in canvas coordinates (its x axis is the UV
// angle already normalised), so it is not shifted by the window offset.
drawHeadCoverWrap(ctx, p.eyes.headMark ?? {}, layout);
+ ctx.setTransform(1, 0, 0, 1, 0, 0);
this.hairTexture.needsUpdate = true;
}
@@ -275,10 +284,10 @@ export class Face {
}
}
-function makeCanvas({ width, height }) {
+function makeCanvas({ width, height }, scale = 1) {
const canvas = document.createElement('canvas');
- canvas.width = width;
- canvas.height = height;
+ canvas.width = Math.round(width * scale);
+ canvas.height = Math.round(height * scale);
return canvas;
}
diff --git a/public/bluebey-studio/src/faceArt.js b/public/bluebey-studio/src/faceArt.js
index 7b5f7de..9d5e20d 100644
--- a/public/bluebey-studio/src/faceArt.js
+++ b/public/bluebey-studio/src/faceArt.js
@@ -919,15 +919,21 @@ export function drawHeadCoverWrap(ctx, spec, layout) {
if (shape === 'fringe') return base + 0.15 * Math.max(0, 1 - Math.abs(t) * 1.6);
// はちわれ: the same V upside down - a widow's peak.
if (shape === 'fringe-up') return base - 0.18 * Math.max(0, 1 - Math.abs(t) * 1.5);
- // カーブ: a plain shallow curve, lower (more hair) round the sides.
- if (shape === 'curve') return base + 0.10 * t * t;
- // わけめ: a 7:3 parting - a step at 30% from the left, so the larger side
- // sits lower (more hair).
- if (shape === 'side') return base + 0.08 - 0.16 / (1 + Math.exp(-(t + 0.4) * 10));
+ // カーブ: a deep round cover - low at the sides, high in the middle, like the
+ // blue of a certain robot cat's head.
+ if (shape === 'curve') return base + 0.42 * t * t;
+ // わけめ: a 7:3 part. The hairline is asymmetric across the *front* only and
+ // comes back to the base level at the sides, so it is continuous at the back
+ // seam (an angle-varying hairline there would ride the seam and step). The
+ // parting line is the gap cut out below.
+ if (shape === 'side') {
+ const w = Math.max(0, 1 - t * t); // 0 at the sides, 1 at the front
+ return base + 0.18 * w * (0.5 + 0.5 * t);
+ }
return base - 0.07 * (1 - t * t); // `cap`
};
- const steps = 96;
+ const steps = Math.max(96, Math.ceil(W / 2));
ctx.beginPath();
ctx.moveTo(0, 0);
for (let i = 0; i <= steps; i += 1) {
@@ -938,6 +944,23 @@ export function drawHeadCoverWrap(ctx, spec, layout) {
ctx.closePath();
ctx.fillStyle = color;
ctx.fill();
+
+ if (shape === 'side') {
+ // The parting: a thin wedge cut out at 30% (a 7:3 split), running from the
+ // top of the head down to the hairline.
+ const partX = span * 0.4;
+ const gw = Math.max(8, span * 0.016) * (0.6 + size);
+ ctx.save();
+ ctx.globalCompositeOperation = 'destination-out';
+ ctx.beginPath();
+ ctx.moveTo(partX - gw * 0.5, 0);
+ ctx.lineTo(partX + gw * 0.5, 0);
+ ctx.lineTo(partX + gw * 1.2, clamp(vBound(partX), 0, 1) * H + 2);
+ ctx.lineTo(partX - gw * 1.2, clamp(vBound(partX), 0, 1) * H + 2);
+ ctx.closePath();
+ ctx.fill();
+ ctx.restore();
+ }
ctx.restore();
}