aboutsummaryrefslogtreecommitdiffhomepage
path: root/public/bluebey-studio/src/faceArt.js
diff options
context:
space:
mode:
authorYasutake Yohei <61961825+yasutakeyohei@users.noreply.github.com>2026-10-05 13:37:46 +0900
committerYasutake Yohei <61961825+yasutakeyohei@users.noreply.github.com>2026-10-05 13:37:46 +0900
commitee7c87a56252ec295119823a2821e5c1f8ec5163 (patch)
tree773fa9322a0e3625acbee20788ddff6db2f2584f /public/bluebey-studio/src/faceArt.js
parent645b204e26962c9adfc11047052020a777b1ca1d (diff)
bluebey-studio: ぎざぎざの歯の深さ、形の大きさ、涙スイッチ、まぶたの高さを整理
Diffstat (limited to 'public/bluebey-studio/src/faceArt.js')
-rw-r--r--public/bluebey-studio/src/faceArt.js77
1 files changed, 46 insertions, 31 deletions
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);