aboutsummaryrefslogtreecommitdiffhomepage
path: root/public/bluebey-studio/src/faceArt.js
diff options
context:
space:
mode:
authorYasutake Yohei <61961825+yasutakeyohei@users.noreply.github.com>2026-10-03 19:41:34 +0900
committerYasutake Yohei <61961825+yasutakeyohei@users.noreply.github.com>2026-10-03 19:41:34 +0900
commit75796bb956d8aedc35e004f620f10ab137eb0216 (patch)
tree17ea2fb125c08cf0c379dad1b3e54eb0afc0682e /public/bluebey-studio/src/faceArt.js
parente530565430622f7d1e824d27b6dc26d95024eb6f (diff)
public/bluebey-studio: ひげをテクスチャに、舌を外に出すを削除
ひげの種類に画像(assets/beards/<kind>.png)があれば、それを使って描くようにした。まず「ちょびひげ」を追加。画像が無い種類は今までどおり描画で出す。 「舌を外に出す」「外に出す舌の位置」を削除した。 「すべてをランダムに」は、スタイル(実写・フラット・線画)と左右反転を変えないようにし、後ろを向く・大きく傾くポーズは選ばないようにした。アイコンを「?!」に変更。
Diffstat (limited to 'public/bluebey-studio/src/faceArt.js')
-rw-r--r--public/bluebey-studio/src/faceArt.js107
1 files changed, 22 insertions, 85 deletions
diff --git a/public/bluebey-studio/src/faceArt.js b/public/bluebey-studio/src/faceArt.js
index 4442bc6..2e1572a 100644
--- a/public/bluebey-studio/src/faceArt.js
+++ b/public/bluebey-studio/src/faceArt.js
@@ -286,11 +286,6 @@ export const MOUTH_LAYOUT = {
// `rise = height * (1 - |dx/half|^2.5)` - so it is steep-sided with a smooth,
// almost flat top. It is convex upwards, but it is **not** a point.
tongue: { pos: 0.84, width: 126, height: 115, crown: 2.5 },
- // The tongue *lolling out* of the mouth (舌を出す). The same super-ellipse as
- // the tongue above, but the crown hangs below its foot instead of rising over
- // it, so the tip droops past the lip. `pos` is its own, because the in-mouth
- // tongue sits off to one side and a lolling tongue reads better centred.
- tongueOut: { pos: 0.5, width: 104, height: 118, crown: 2.2 },
corner: { fromX: 26, fromY: 3, toX: 32, toY: 42, width: 11, curve: 26 },
openRise: 64,
// Was 0.25: opening the mouth used to flatten the smile to stay inside the
@@ -873,7 +868,7 @@ 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);
- drawBeard(ctx, style.beard, lineOnly);
+ drawBeard(ctx, style.beard, lineOnly, style.beards);
}
/**
@@ -882,13 +877,27 @@ function drawFaceExtras(ctx, style, lineOnly) {
* thick waving band, and `apron` is a filled bib. In a line drawing nothing is
* filled and only the outlines are inked.
*/
-function drawBeard(ctx, spec, lineOnly = false) {
+function drawBeard(ctx, spec, lineOnly = false, textures = null) {
const shape = spec?.shape ?? 'off';
if (shape === 'off' || shape === 'none') return;
const size = clamp(spec.size ?? 1, 0.2, 3);
const cx = BEARD_LAYOUT.cx;
const cy = BEARD_LAYOUT.cy + clamp(spec.offsetY ?? 0, -600, 600);
const w = BEARD_LAYOUT.width * size;
+
+ // A supplied drawing for this kind (assets/beards/<shape>.png) replaces the
+ // built-in strokes. The drawings are trimmed to their ink, so `w` is the
+ // beard's real width; it is centred on the nose and scaled by `size`.
+ const texture = textures?.[shape];
+ if (texture && texture.width) {
+ // Slightly wider than the built-in base: a moustache drawn to the same width
+ // as a beard's stroke sat mostly behind the nose.
+ const drawW = w * 1.4;
+ const dh = (drawW * (texture.height || 1)) / (texture.width || 1);
+ ctx.drawImage(texture, cx - drawW / 2, cy - dh / 2, drawW, dh);
+ return;
+ }
+
const color = spec.color ?? '#3a2a4a';
const line = spec.line ?? '#55386e';
const lw = Math.max(2, 9 * size);
@@ -1372,9 +1381,6 @@ export function drawMouth(ctx, p) {
const arc = cubicPoints(lip.p0, lip.p1, lip.p2, lip.p3, 96);
const tongueAmount = clamp(p.tongue ?? 1, 0, 1.6);
- // How far the tongue hangs *out* of the mouth (below the lip). Separate from
- // `tongue`, which is the one that pokes up over the lip of a closed mouth.
- const tongueOutAmount = clamp(p.tongueOut ?? 0, 0, 1);
// --- a round "O" mouth (surprised, singing) ---------------------------
const roundAmount = clamp(p.round ?? 0, 0, 1);
@@ -1402,24 +1408,6 @@ export function drawMouth(ctx, p) {
ctx.fillStyle = tongueColor;
ctx.fill();
}
- if (tongueOutAmount > 0.01) {
- // A tongue hanging out below the round mouth. Its foot is a flat line
- // inside the lower lip; the tip is clamped to the plate's lower edge.
- const drop = Math.min(L.tongueOut.height * tongueOutAmount, Math.max(0, edgeBottom - (centreY + ry * 0.5)));
- if (drop > 4) {
- const half = (L.tongueOut.width * tongueOutAmount) / 2;
- const footY = centreY + ry * 0.5;
- ctx.beginPath();
- ctx.moveTo(L.centreX - half, footY);
- for (let i = 0; i <= 26; i++) {
- const dx = 1 - 2 * (i / 26);
- ctx.lineTo(L.centreX + dx * half, footY + drop * (1 - Math.pow(Math.abs(dx), L.tongueOut.crown)));
- }
- ctx.closePath();
- ctx.fillStyle = tongueColor;
- ctx.fill();
- }
- }
}
ctx.strokeStyle = strokeColor;
ctx.lineWidth = line ? thickness * 0.8 : thickness;
@@ -1489,61 +1477,13 @@ export function drawMouth(ctx, p) {
ctx.fill();
}
- /**
- * The tongue lolling *out* of the mouth, hanging below `curve`.
- *
- * The mirror of `drawTongue`: the foot sits along the lip (or the lower jaw
- * when the mouth is open) and the crown drops *below* it. The tip is clamped
- * against the plate's real lower edge, so a tongue on a low mouth gets shorter
- * instead of being smeared off the chin by the texture's clamp.
- */
- function drawTongueOut(pointAt, curve, height) {
- const half = (L.tongueOut.width * tongueOutAmount) / 2;
- const pos = clamp(p.tongueOutPos ?? L.tongueOut.pos, 0.05, 0.95);
- const arcLength = Math.max(1, chordLength(curve));
- const dt = clamp(half / arcLength, 0.01, 0.45);
- const t0 = clamp(pos - dt, 0, 1);
- const t1 = clamp(pos + dt, 0, 1);
- const base = [];
- for (let i = 0; i <= 16; i++) base.push(pointAt(t0 + ((t1 - t0) * i) / 16));
- const mid = pointAt(clamp(pos, 0, 1));
- const drop = Math.min(height, Math.max(0, edgeBottom - mid.y));
- if (drop < 2) return;
- const footRight = base[base.length - 1];
- const footLeft = base[0];
- const crown = [];
- const steps = 26;
- for (let i = 0; i <= steps; i++) {
- const f = i / steps;
- const dx = 1 - 2 * f;
- const footY = footRight.y + (footLeft.y - footRight.y) * f;
- crown.push({
- x: mid.x + dx * half,
- y: footY + drop * (1 - Math.pow(Math.abs(dx), L.tongueOut.crown)),
- });
- }
- const shaped = rotate([...base, ...crown], pivot, tilt);
- if (line) {
- strokePolyline(ctx, shaped, { color: strokeColor, width: thickness * 0.8, closed: true });
- return;
- }
- tracePolyline(ctx, shaped, true);
- ctx.fillStyle = tongueColor;
- ctx.fill();
- }
-
if (openAmount > 0.004) {
if (!line) fillPolygon(ctx, openShape, innerColor);
// The tongue rises from the lower lip *into* the mouth, so it has to arrive
// with the opening: at a hair's width the jaw curve is a sliver, and the
// tongue used to burst straight out of it and sit on the chin.
const tongueOpen = clamp((openAmount - 0.06) / 0.24, 0, 1);
- if (tongueOutAmount > 0.01 && tongueOpen > 0.01) {
- // A tongue lolling *out* over the lower jaw is not clipped to the mouth:
- // it hangs in front of the chin. Drawn before the outline, which then
- // crosses its root.
- drawTongueOut(jawAt, jaw, L.tongueOut.height * tongueOutAmount * tongueOpen);
- } else if (tongueAmount > 0.01 && tongueOpen > 0.01) {
+ if (tongueAmount > 0.01 && tongueOpen > 0.01) {
ctx.save();
tracePolyline(ctx, openShape, true);
ctx.clip();
@@ -1555,14 +1495,11 @@ export function drawMouth(ctx, p) {
// would leave the lower edge as a bare colour change.
strokePolyline(ctx, openShape, { color: strokeColor, width: thickness, closed: true });
} else {
- // Closed lips - but the tongue still pokes out. That is what the original
- // artwork does (the mouth reads as a smile with the tongue showing, like
- // ペコちゃん), and it is why a plain line looked wrong there. `tongueOut`
- // hangs the tongue the other way, *below* the lip line. The lip line goes on
- // afterwards, so the tongue comes out from under it.
- if (tongueOutAmount > 0.01) {
- drawTongueOut(lipAt, arc, L.tongueOut.height * tongueOutAmount);
- } else if (tongueAmount > 0.01) {
+ // Closed lips - but the tongue still pokes over the lip. That is what the
+ // original artwork does (the mouth reads as a smile with the tongue showing,
+ // like ペコちゃん), and it is why a plain line looked wrong there. The lip
+ // line goes on afterwards, so the tongue comes out from under it.
+ if (tongueAmount > 0.01) {
drawTongue(lipAt, arc, tongueHeight);
}
strokePolyline(ctx, rotate(arc, pivot, tilt), { color: strokeColor, width: thickness });