From ee7c87a56252ec295119823a2821e5c1f8ec5163 Mon Sep 17 00:00:00 2001 From: Yasutake Yohei <61961825+yasutakeyohei@users.noreply.github.com> Date: Mon, 5 Oct 2026 13:37:46 +0900 Subject: bluebey-studio: ぎざぎざの歯の深さ、形の大きさ、涙スイッチ、まぶたの高さを整理 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- public/bluebey-studio/src/faceArt.js | 77 +++++++++++++++++++++--------------- 1 file changed, 46 insertions(+), 31 deletions(-) (limited to 'public/bluebey-studio/src/faceArt.js') diff --git a/public/bluebey-studio/src/faceArt.js b/public/bluebey-studio/src/faceArt.js index 5e99be5..01890b5 100644 --- a/public/bluebey-studio/src/faceArt.js +++ b/public/bluebey-studio/src/faceArt.js @@ -573,15 +573,19 @@ function lidPath(ctx, eye, amount, lower, flat = false) { } /** The "eyes shut" artwork: a line, a chevron or a happy arch. */ -function drawShutEye(ctx, eye, spec, style) { +function drawShutEye(ctx, eye, spec, style, shapeScale = 1, maxHalf = Infinity) { const layout = EYE_LAYOUT.closedLine; const { line, width } = style; + // 形の大きさ, clamped so the shape stays inside the white of the eye when it is + // shown (`maxHalf` is Infinity otherwise). + const k = (unitHalf) => Math.min(shapeScale, maxHalf / unitHalf); if (spec.closed === 'chevron') { // The chevron points *at* the nose, matching the original artwork. - const vertex = { x: eye.cx + eye.towardNose * layout.vertex, y: eye.cy + layout.offsetY - 8 }; - const armX = eye.cx - eye.towardNose * layout.armX; - const spread = layout.spread * (spec.spread ?? 1); + const scale = k(layout.armX); + const vertex = { x: eye.cx + eye.towardNose * layout.vertex * scale, y: eye.cy + layout.offsetY * scale - 8 * scale }; + const armX = eye.cx - eye.towardNose * layout.armX * scale; + const spread = layout.spread * (spec.spread ?? 1) * scale; strokePolyline(ctx, [vertex, { x: armX, y: vertex.y - spread }], { color: line, width }); strokePolyline(ctx, [vertex, { x: armX, y: vertex.y + spread }], { color: line, width }); return; @@ -591,8 +595,7 @@ function drawShutEye(ctx, eye, spec, style) { // A real "3": an upper bowl and a lower bowl that meet at a single pinch on // the nose side. Stacking two C's instead made the two lobes sit on top of // each other; joining them at a point is what makes it read as the digit. - // `length` scales the whole shape. - const r = 34 * (spec.length ?? 1); + const r = 34 * k(1.7 * 34); // Which way the digit faces. It used to follow the nose side, which made the // pair a mirror image; each eye can now be set either way, because a pair of // mirrored 3s does not always read the way you want. @@ -609,10 +612,11 @@ function drawShutEye(ctx, eye, spec, style) { } if (spec.closed === 'arch') { - const edges = { x: eye.r, y: EYE_LAYOUT.arch.edgesUp }; + const scale = k(eye.r); + const edges = { x: eye.r * scale, y: EYE_LAYOUT.arch.edgesUp * scale }; const points = quadraticPoints( { x: eye.cx - edges.x, y: eye.cy + edges.y }, - { x: eye.cx, y: eye.cy + edges.y - EYE_LAYOUT.arch.apexUp * 2 }, + { x: eye.cx, y: eye.cy + edges.y - EYE_LAYOUT.arch.apexUp * 2 * scale }, { x: eye.cx + edges.x, y: eye.cy + edges.y }, 24, ); @@ -625,17 +629,19 @@ function drawShutEye(ctx, eye, spec, style) { // strokes share one endpoint instead - a ">" or bird's-foot shape, which is // how manga draws a happily squeezed-shut eye. const count = clamp(Math.round(spec.closedLines ?? 1), 1, 3); + const scale = k(layout.length / 2); if (count >= 2) { - const vertex = { x: eye.cx + eye.towardNose * layout.vertex, y: eye.cy + layout.offsetY - 8 }; + const vertex = { x: eye.cx + eye.towardNose * layout.vertex * scale, y: eye.cy + layout.offsetY * scale - 8 * scale }; // Reach as far as the single line does, not just a short stub: the arms are // the *long* form of the same eyelid the one-line version covers. - const outX = -eye.towardNose * layout.armSpan; + const outX = -eye.towardNose * layout.armSpan * scale; + const spread = layout.spread * scale; const arms = count === 2 - ? [{ x: outX, y: -layout.spread }, { x: outX, y: layout.spread }] + ? [{ x: outX, y: -spread }, { x: outX, y: spread }] : [ - { x: outX, y: -layout.spread }, - { x: -eye.towardNose * Math.hypot(layout.armSpan, layout.spread), y: 0 }, - { x: outX, y: layout.spread }, + { x: outX, y: -spread }, + { x: -eye.towardNose * Math.hypot(layout.armSpan, layout.spread) * scale, y: 0 }, + { x: outX, y: spread }, ]; for (const arm of arms) { strokePolyline(ctx, [vertex, { x: vertex.x + arm.x, y: vertex.y + arm.y }], { color: line, width }); @@ -643,11 +649,11 @@ function drawShutEye(ctx, eye, spec, style) { return; } - const half = (layout.length / 2) * (spec.length ?? 1); - const midX = eye.cx - eye.towardNose * layout.offsetX; - const midY = eye.cy + layout.offsetY; + const half = (layout.length / 2) * (spec.length ?? 1) * scale; + const midX = eye.cx - eye.towardNose * layout.offsetX * scale; + const midY = eye.cy + layout.offsetY * scale; const angle = ((layout.slantDeg * (spec.slant ?? 1) * eye.towardNose) * Math.PI) / 180; - const bow = (layout.bow ?? 0) * (spec.slant ?? 1); + const bow = (layout.bow ?? 0) * (spec.slant ?? 1) * scale; const from = { x: midX - Math.cos(angle) * half, y: midY - Math.sin(angle) * half }; const to = { x: midX + Math.cos(angle) * half, y: midY + Math.sin(angle) * half }; const points = quadraticPoints(from, { x: midX, y: midY + bow * 2 }, to, 20); @@ -868,11 +874,12 @@ function drawHeadCover(ctx, spec, limits) { const boundary = (x) => { const t = clamp((x - cx) / halfSpan, -1, 1); // ぎざぎざ: a jagged, saw-toothed hairline. A triangle wave across the front - // gives the teeth, and `size` scales how far they reach down. + // gives the teeth; `teeth` is how far they reach down (its own slider), so + // the 大きさ slider only moves the hairline up and down. if (shape === 'fringe') { const teeth = 8; const saw = Math.abs((((t * teeth) % 2) + 2) % 2 - 1); // 0..1 triangle - return base + 200 * size * saw; + return base + (spec.teeth ?? 0.12) * 600 * saw; } // はちわれ: the same V upside down - a widow's peak. if (shape === 'fringe-up') return base - 320 * Math.max(0, 1 - Math.abs(t) * 1.5); @@ -946,10 +953,11 @@ export function drawHeadCoverWrap(ctx, spec, layout) { // the three peaks and two valleys of an "M". `t` is -1..1 across the front. if (shape === 'fringe') { // ぎざぎざ: a jagged, saw-toothed hairline. A triangle wave across the front - // gives the teeth, and `size` scales how far they reach down. + // gives the teeth; `teeth` is how far they reach down (its own slider), so + // the 大きさ slider only moves the hairline up and down. const teeth = 8; const saw = Math.abs((((t * teeth) % 2) + 2) % 2 - 1); // 0..1 triangle - return base + 0.20 * size * saw; + return base + (spec.teeth ?? 0.12) * saw; } // はちわれ: 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); @@ -1337,6 +1345,14 @@ export function drawEyes(ctx, p) { const isHeart = (spec.irisShape ?? 'circle') === 'heart'; // The lower lid is per eye (falling back to the shared value). const lowerLid = clamp(spec.lowerLid ?? style.lowerLid ?? 0, 0, 1); + // 形の大きさ: scales the drawn shape (shut lines, the 3, the arch, the heart). + const shapeScale = clamp(spec.shapeScale ?? 1, 0.4, 2); + // `white` is tri-state: an open eye shows its white, a shut eye or a heart + // does not, unless the switch forces it one way. + const showWhite = spec.white != null ? spec.white === true : (open > 0.02 && !isHeart); + // While the white is shown, a shape that would poke out of the eyeball is + // scaled back inside it; `shapeHalfLimit` is the half-size it may reach. + const shapeHalfLimit = showWhite ? eye.r * 0.92 : Infinity; drawBrow(ctx, eye, spec, style, limits); @@ -1346,7 +1362,8 @@ export function drawEyes(ctx, p) { // amount, the height and the tilt are all *per eye*, because one eye crying // while the other does not is a real expression. const drawTear = () => { - const amount = clamp(spec.tear ?? 0, 0, 1.6); + if (spec.tearOn !== true) return; + const amount = clamp(spec.tear ?? 0.3, 0, 1.6); if (amount <= 0.01) return; const size = EYE_LAYOUT.tear.size * amount; const tearX = eye.cx + clamp(spec.tearX ?? 0, -400, 400) - layout.towardNose * EYE_LAYOUT.tear.offsetX; @@ -1376,8 +1393,8 @@ export function drawEyes(ctx, p) { if (open <= 0.02) { // A shut eye can still be given a white backing (the "白目" switch), which // reads as a line drawn across an eyeball rather than a bare line. - if (!line && spec.white === true) fillCircle(ctx, eye.cx, eye.cy, eye.r, white); - drawShutEye(ctx, eye, spec, lineStyle); + if (!line && showWhite) fillCircle(ctx, eye.cx, eye.cy, eye.r, white); + drawShutEye(ctx, eye, spec, lineStyle, shapeScale, shapeHalfLimit); // The glasses sit over the shut-eye artwork too, but under the tear below. drawGlasses(ctx, eye, line, style, limits); drawTear(); @@ -1400,11 +1417,7 @@ export function drawEyes(ctx, p) { dy = (dy / dist) * eyeTravel; } - // The white of the eye. `spec.white` is per eye and tri-state: `null` keeps - // the usual behaviour (an open eye shows its white, a heart does not), while - // true/false force it on or off - so an open eye can lose its white, and a - // shut eye or a heart can gain it. - const showWhite = spec.white == null ? !isHeart : spec.white === true; + // The white of the eye (see `showWhite` above). if (!line && showWhite) fillCircle(ctx, eye.cx, eye.cy, eye.r, white); const irisX = eye.cx + dx; const irisY = eye.cy + dy; @@ -1414,7 +1427,9 @@ export function drawEyes(ctx, p) { // A heart is not an eyeball, so it may grow well past the eye: the plate // keeps the whole canvas, and the eyes plate is drawn over the mouth, so a // big heart has room. The cap only stops it from running off the canvas. - const heartRadius = Math.min(irisRadius * heartScale, EYE_LAYOUT.radius * 2.1); + // When the white is shown, it is capped to the eyeball instead. + let heartRadius = Math.min(irisRadius * heartScale * shapeScale, EYE_LAYOUT.radius * 2.1); + if (showWhite) heartRadius = Math.min(heartRadius, shapeHalfLimit); fillHeart(ctx, irisX, irisY, heartRadius, line ? lineColor : heartColor); } else { fillCircle(ctx, irisX, irisY, irisRadius, line ? lineColor : irisColor); -- cgit v1.3.1