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 14:42:08 +0900
committerYasutake Yohei <61961825+yasutakeyohei@users.noreply.github.com>2026-10-05 14:42:08 +0900
commit1bf7ee115176bc366315a90ab31740e6cd968744 (patch)
tree752b2db6126700c920f7e8b98bc167c95035341a /public/bluebey-studio/src/faceArt.js
parent88010ce24f3a863518071c4bd5dd5d351d1bad26 (diff)
bluebey-studio: ハートの白目内クリップ・まつげ追従・まばたき修正、帽子の線画と位置、手描き目UI削除
指定場所を見るときもハートが白目内に収まるようクリップ。まつげを上まぶたに連動。閉じ目はまばたきせず、ハート選択後のまばたきで3の目/わらうが一瞬出る不具合を修正。帽子をランナンスタイル登録して線画対応、横・奥行き追加。手描きの目UIを削除。
Diffstat (limited to 'public/bluebey-studio/src/faceArt.js')
-rw-r--r--public/bluebey-studio/src/faceArt.js71
1 files changed, 48 insertions, 23 deletions
diff --git a/public/bluebey-studio/src/faceArt.js b/public/bluebey-studio/src/faceArt.js
index aa1204b..482ddd7 100644
--- a/public/bluebey-studio/src/faceArt.js
+++ b/public/bluebey-studio/src/faceArt.js
@@ -1329,14 +1329,24 @@ function drawGlasses(ctx, eye, line, style, limits) {
* rolls only set `open`/`closed`/`irisShape`, so this falls back to those.
*/
function eyeShapeKey(spec) {
- // A fully lowered upper lid always means a shut artwork, even if a stale
- // `shape` says otherwise - this is what keeps older saved links working.
+ const shape = spec.shape;
+ const explicitShut = shape === 'three' || shape === 'arch'
+ || (typeof shape === 'string' && shape.startsWith('line'));
+ // An explicit shut artwork is used as-is; the lid height only trims it.
+ if (explicitShut) return shape;
+ // A lowered lid on an open or heart eye reads as a shut line - a blink. It must
+ // NOT borrow a 3 or an arch from a stale `closed`, which used to flash on screen
+ // for a frame when blinking after picking 3/わらう and then switching to heart.
if ((spec.open ?? 1) <= 0.02) {
- if (spec.closed === 'three') return 'three';
- if (spec.closed === 'arch') return 'arch';
- return `line${clamp(Math.round(spec.closedLines ?? 1), 1, 3)}`;
+ if (!shape) {
+ // Older saved states have no `shape`; derive the shut artwork from `closed`.
+ if (spec.closed === 'three') return 'three';
+ if (spec.closed === 'arch') return 'arch';
+ return `line${clamp(Math.round(spec.closedLines ?? 1), 1, 3)}`;
+ }
+ return 'line1';
}
- if (spec.shape) return spec.shape;
+ if (shape) return shape;
return (spec.irisShape ?? 'circle') === 'heart' ? 'heart' : 'open';
}
@@ -1351,7 +1361,6 @@ export function drawEyes(ctx, p) {
const lookMax = style.lookMax ?? 1;
const heartScale = 2;
const heartColor = style.heartColor ?? '#e0344f';
- const heartTravel = EYE_LAYOUT.radius * 0.9;
// ほっぺ and 頭の模様 belong to the whole face, so they are drawn once here
// rather than inside the per-eye loop below. 鼻ちょうちん comes along too.
@@ -1441,11 +1450,21 @@ export function drawEyes(ctx, p) {
ctx.restore();
};
- // Where the pupil is looking. Every eye shape shares it: the shut artwork
- // shifts with the gaze too, not just the round eyeball.
- const eyeTravel = isHeart
- ? heartTravel * lookMax
- : Math.max(0, EYE_LAYOUT.radius - irisRadius) * lookMax;
+ // How far the gaze can push the content. A heart (or a shut line shown on the
+ // white) is much bigger than the iris, so it gets a smaller travel - and with
+ // the white shown the content is also clipped to the eyeball - so it never
+ // pokes outside the white.
+ const heartFit = isHeart && showWhite
+ ? Math.min(1, shapeHalfLimit / (EYE_LAYOUT.irisRadius * heartScale))
+ : 1;
+ const heartRadius = Math.min(
+ EYE_LAYOUT.irisRadius * heartScale * shapeScale * heartFit,
+ EYE_LAYOUT.radius * 2.1,
+ );
+ let travelBase = irisRadius;
+ if (isHeart) travelBase = heartRadius;
+ else if (isShutShape && showWhite) travelBase = shapeHalfLimit;
+ const eyeTravel = Math.max(0, EYE_LAYOUT.radius - travelBase) * lookMax;
let dx = clamp(spec.lookX ?? 0, -1, 1) * eyeTravel;
let dy = -clamp(spec.lookY ?? 0, -1, 1) * eyeTravel;
const dist = Math.hypot(dx, dy);
@@ -1456,12 +1475,13 @@ export function drawEyes(ctx, p) {
// --- the eye's content, clipped by the lids -------------------------
// Every shape is drawn the same way now: a white backing, then the artwork
- // (iris, heart or the shut line/3/arch), all trimmed by the upper and lower
- // lids. The shut artwork reaches past the eyeball, so it is only lid-clipped.
+ // (iris, heart or the shut line/3/arch), trimmed by the upper and lower lids.
+ // The shut artwork reaches past the eyeball, so it is only lid-clipped - but
+ // once the white is shown it is clipped to the eyeball too, so it stays in.
ctx.save();
- if (isShutShape) {
+ if (isShutShape && !showWhite) {
clipLids(ctx, eye, open, lowerLid, lidFlat, lidTilt * eye.towardNose);
- } else if (!isHeart || open < 0.999) {
+ } else if (!isHeart || open < 0.999 || showWhite) {
clipEye(ctx, eye, open, isHeart ? 0 : lowerLid, lidFlat, lidTilt * eye.towardNose);
}
if (!line && showWhite) fillCircle(ctx, eye.cx, eye.cy, eye.r, white);
@@ -1469,11 +1489,6 @@ export function drawEyes(ctx, p) {
ctx.translate(dx, dy);
drawShutEye(ctx, eye, shapeSpec, lineStyle, shapeScale, shapeHalfLimit);
} else if (isHeart) {
- // A heart is not an eyeball, so it may grow past the eye up to the plate.
- // With the white shown, its default size is what just fits inside it; the
- // single 大きさ knob can still grow it past the white.
- const heartFit = showWhite ? Math.min(1, shapeHalfLimit / (EYE_LAYOUT.irisRadius * heartScale)) : 1;
- const heartRadius = Math.min(EYE_LAYOUT.irisRadius * heartScale * shapeScale * heartFit, EYE_LAYOUT.radius * 2.1);
fillHeart(ctx, eye.cx + dx, eye.cy + dy, heartRadius, line ? lineColor : heartColor);
} else {
const irisX = eye.cx + dx;
@@ -1547,7 +1562,16 @@ export function drawEyes(ctx, p) {
// Short strokes flicking outwards from the upper-outer lid. They sit outside
// the eyeball, so they are drawn after the lid block and are not clipped.
if (spec.lashes ?? style.lashes) {
+ // The lashes ride the upper lid: they drop as the lid lowers (2r per unit of
+ // open) and tilt with まぶたの傾き, so they stay on the lid edge.
const outDir = -eye.towardNose;
+ const lidDrop = 2 * eye.r * (1 - open);
+ ctx.save();
+ if (eyeTilt) {
+ ctx.translate(eye.cx, eye.cy);
+ ctx.rotate((eyeTilt * Math.PI) / 180);
+ ctx.translate(-eye.cx, -eye.cy);
+ }
for (const deg of EYE_LAYOUT.lash.angles) {
const t = (deg * Math.PI) / 180;
const ux = outDir * Math.cos(t);
@@ -1555,10 +1579,11 @@ export function drawEyes(ctx, p) {
const r0 = eye.r * 0.96;
const r1 = r0 + EYE_LAYOUT.lash.length;
strokePolyline(ctx, [
- { x: eye.cx + ux * r0, y: eye.cy + uy * r0 },
- { x: eye.cx + ux * r1, y: eye.cy + uy * r1 },
+ { x: eye.cx + ux * r0, y: eye.cy + uy * r0 + lidDrop },
+ { x: eye.cx + ux * r1, y: eye.cy + uy * r1 + lidDrop },
], { color: lineColor, width: EYE_LAYOUT.lash.width });
}
+ ctx.restore();
}
// --- outline (line-art mode only) ---------------------------------