From 7892cc6f46cdfcd6dfbbdc94d34cbf0c68f29fba Mon Sep 17 00:00:00 2001 From: Yasutake Yohei <61961825+yasutakeyohei@users.noreply.github.com> Date: Sat, 3 Oct 2026 13:51:53 +0900 Subject: public/bluebey-studio: 寝ているポーズと鼻ちょうちんを追加し、動きを修正 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - ポーズ例に「ねている」を追加(体を横に倒した姿勢) - 「顔」タブに「鼻ちょうちん」を追加。鼻の横に泡を描き、大きさ・位置 ・色を調整できる。寝ている顔に使う - 「歩く」の腕と脚の位相を逆にし、右手が前に出るときは左足が前に出る ように修正 - 「てをふる」の腕の上げ幅を抑え、手が顔にめり込まないように修正 --- public/bluebey-studio/src/faceArt.js | 89 ++++++++++++++++++++++++++++++++++-- 1 file changed, 86 insertions(+), 3 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 61bfa0d..1beedad 100644 --- a/public/bluebey-studio/src/faceArt.js +++ b/public/bluebey-studio/src/faceArt.js @@ -177,6 +177,22 @@ export const HEAD_MARK_LAYOUT = { reach: 190, // artwork px the boundary moves per unit of `size` }; +/** + * 鼻ちょうちん: the little snot bubble that grows and shrinks while someone sleeps. + * + * It hangs off the nose, which is a *separate mesh* in front of the plate, so the + * bubble sits out to the side of it (a bubble drawn straight below would run off + * the plate's bottom edge). The neck starts at the nose and reaches out to the + * bubble, one thin wisp, the way manga draws it. + */ +export const SNOT_LAYOUT = { + cx: 511.5, // the face midline, the same centre the mouth uses + cy: 336, // just below the eye row, level with the nose + offset: 122, // how far out to the side the bubble hangs, clear of the nose + radius: 48, // bubble radius at size 1 + lineWidth: 6, +}; + /** * 眼鏡 / サングラス, drawn into the same texture as the eyes and brows. * @@ -839,10 +855,77 @@ function drawHeadCover(ctx, spec, limits) { * The shared extras (ほっぺ and 頭の模様) are drawn once per face, before the eyes, * so the eyes, brows and glasses win any overlap and the cheeks are not doubled. */ -function drawFaceExtras(ctx, style) { +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); + drawSnotBubble(ctx, style.snot, lineOnly); +} + +/** + * 鼻ちょうちん: a bubble hanging off the nose, joined to it by a thin neck. The + * neck starts at the nose (which the nose mesh hides, so it reads as growing out + * from under it) and swells into the round bubble out to the side. In a line + * drawing the bubble is left empty and only its outline is inked. + */ +function drawSnotBubble(ctx, spec, lineOnly) { + if (!spec?.enabled) return; + const size = clamp(spec.size ?? 1, 0.2, 3); + const dx = spec.offsetX ?? 0; + const dy = spec.offsetY ?? 0; + const r = SNOT_LAYOUT.radius * size; + const cx = SNOT_LAYOUT.cx + SNOT_LAYOUT.offset + dx; + const cy = SNOT_LAYOUT.cy + dy; + const color = spec.color ?? '#e3ecff'; + const line = spec.line ?? '#55386e'; + const lw = Math.max(1, SNOT_LAYOUT.lineWidth * size); + + // The nose end sits on the midline; the bubble hangs out to one side. + const nx = SNOT_LAYOUT.cx + dx; + const ny = SNOT_LAYOUT.cy + dy - r * 0.1; + const dist = Math.hypot(cx - nx, cy - ny) || 1; + const ux = (cx - nx) / dist; + const uy = (cy - ny) / dist; + // Where the neck meets the bubble: a little inside its edge, so the two merge. + const reach = Math.max(0, dist - r * 0.72); + const ex = nx + ux * reach; + const ey = ny + uy * reach; + + ctx.save(); + ctx.lineJoin = 'round'; + ctx.lineCap = 'round'; + // The neck: a wedge from a narrow end at the nose to a wide end at the bubble. + const px = -uy; + const py = ux; + const nW = lw * 0.55; + const bW = r * 0.5; + ctx.beginPath(); + ctx.moveTo(nx + px * nW, ny + py * nW); + ctx.lineTo(ex + px * bW, ey + py * bW); + ctx.lineTo(ex - px * bW, ey - py * bW); + ctx.lineTo(nx - px * nW, ny - py * nW); + ctx.closePath(); + if (lineOnly) { + ctx.strokeStyle = line; + ctx.lineWidth = lw; + ctx.stroke(); + } else { + ctx.fillStyle = color; + ctx.fill(); + ctx.strokeStyle = line; + ctx.lineWidth = lw * 0.7; + ctx.stroke(); + } + // The bubble itself, over the neck's wide end. + circlePath(ctx, cx, cy, r); + if (!lineOnly) { + ctx.fillStyle = color; + ctx.fill(); + } + ctx.strokeStyle = line; + ctx.lineWidth = lw; + ctx.stroke(); + ctx.restore(); } /** @@ -1030,8 +1113,8 @@ export function drawEyes(ctx, p) { const lowerLid = clamp(style.lowerLid ?? 0, 0, 1); // ほっぺ and 頭の模様 belong to the whole face, so they are drawn once here - // rather than inside the per-eye loop below. - drawFaceExtras(ctx, style); + // rather than inside the per-eye loop below. 鼻ちょうちん comes along too. + drawFaceExtras(ctx, style, line); for (const layout of EYE_LAYOUT.eyes) { const spec = (p.eyes ?? {})[layout.key] ?? {}; -- cgit v1.3.1