diff options
Diffstat (limited to 'public/bluebey-studio/src/faceArt.js')
| -rw-r--r-- | public/bluebey-studio/src/faceArt.js | 107 |
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 }); |
