From 1bf7ee115176bc366315a90ab31740e6cd968744 Mon Sep 17 00:00:00 2001 From: Yasutake Yohei <61961825+yasutakeyohei@users.noreply.github.com> Date: Mon, 5 Oct 2026 14:42:08 +0900 Subject: bluebey-studio: ハートの白目内クリップ・まつげ追従・まばたき修正、帽子の線画と位置、手描き目UI削除 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 指定場所を見るときもハートが白目内に収まるようクリップ。まつげを上まぶたに連動。閉じ目はまばたきせず、ハート選択後のまばたきで3の目/わらうが一瞬出る不具合を修正。帽子をランナンスタイル登録して線画対応、横・奥行き追加。手描きの目UIを削除。 --- public/bluebey-studio/src/faceArt.js | 71 ++++++++++++++++++++++++------------ 1 file changed, 48 insertions(+), 23 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 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) --------------------------------- -- cgit v1.3.1