From eed94d378af577171e24fe78b4b159be5ca50317 Mon Sep 17 00:00:00 2001 From: Yasutake Yohei <61961825+yasutakeyohei@users.noreply.github.com> Date: Sun, 4 Oct 2026 23:24:51 +0900 Subject: bluebey-studio: 下まぶたと目線の左右別設定、Oの口の拡大、白目/ハート/振れ幅の整理 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 下まぶた・目線を左右べつに設定可能に。丸く開く口を顔半分ほどまで拡大。白目を消すを一般化し「ハートのとき白目を消す」を削除。ハートの大きさは瞳の大きさに統合し、目線の振れ幅のスライダーを廃止(既定1.2)。 --- public/bluebey-studio/src/faceArt.js | 16 +++++------ public/bluebey-studio/src/main.js | 2 ++ public/bluebey-studio/src/panel.js | 56 +++++++++++++++--------------------- public/bluebey-studio/src/presets.js | 8 ++---- 4 files changed, 36 insertions(+), 46 deletions(-) (limited to 'public/bluebey-studio/src') diff --git a/public/bluebey-studio/src/faceArt.js b/public/bluebey-studio/src/faceArt.js index 48ffd67..ba748f5 100644 --- a/public/bluebey-studio/src/faceArt.js +++ b/public/bluebey-studio/src/faceArt.js @@ -1361,12 +1361,11 @@ export function drawEyes(ctx, p) { const irisScale = style.irisScale ?? 1; const lookMax = style.lookMax ?? 1; const highlightOn = style.highlight !== false; - const heartScale = style.heartScale ?? 0.85; + const heartScale = 2; const heartColor = style.heartColor ?? '#e0344f'; const irisRadius = EYE_LAYOUT.irisRadius * irisScale; const travel = Math.max(0, EYE_LAYOUT.radius - irisRadius); const heartTravel = EYE_LAYOUT.radius * 0.9; - const lowerLid = clamp(style.lowerLid ?? 0, 0, 1); const lidFlat = (style.lidShape ?? 'curve') === 'flat'; // How much the whole narrowed eye is tilted (e.g. an angry or gentle squint). const lidTilt = clamp(style.lidTilt ?? 0, -45, 45); @@ -1387,6 +1386,8 @@ export function drawEyes(ctx, p) { }; const lineStyle = { line: lineColor, width: lidWidth }; 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); drawBrow(ctx, eye, spec, style, limits); @@ -1600,12 +1601,11 @@ export function drawMouth(ctx, p) { // --- a round "O" mouth (surprised, singing) --------------------------- const roundAmount = clamp(p.round ?? 0, 0, 1); if (roundAmount > 0.004) { - let ry = L.sag * 0.42 * roundAmount * clamp(p.thickness ?? 1, 0.5, 2); - // The nose hides the middle of the upper face, so keep the whole oval below - // it and inside the plate. (`L.safeBottom` is only the round mouth's own size - // limit; it no longer has to stay clear of the canvas edge.) - ry = Math.min(ry, (L.safeBottom - thickness / 2 - L.noseClear) / 2.1); - const rx = Math.min(ry * 1.15 * clamp(p.width ?? 1, 0.2, 1.6), L.halfChord * 0.8); + let ry = L.sag * 1.05 * roundAmount * clamp(p.thickness ?? 1, 0.5, 2); + // Keep the oval below the nose and inside the plate, but allow it to grow to + // roughly half the head. + ry = Math.min(ry, (edgeBottom - thickness / 2 - L.noseClear) / 2.1); + const rx = Math.min(ry * 1.15 * clamp(p.width ?? 1, 0.2, 1.6), L.halfChord * 1.4); const centreY = clamp(y0 + sag * 0.5, L.noseClear + ry * 1.02, edgeBottom - thickness / 2 - ry * 1.02); if (ry > 5) { ctx.save(); diff --git a/public/bluebey-studio/src/main.js b/public/bluebey-studio/src/main.js index 3f61ad5..60507eb 100644 --- a/public/bluebey-studio/src/main.js +++ b/public/bluebey-studio/src/main.js @@ -1520,6 +1520,8 @@ async function init() { eyeX: own.eyeX ?? 0, tearX: own.tearX ?? 0, tearTilt: own.tearTilt ?? 0, + // The lower lid can differ per eye (falling back to the shared value). + lowerLid: own.lowerLid ?? eyes.lowerLid ?? 0, }; }; diff --git a/public/bluebey-studio/src/panel.js b/public/bluebey-studio/src/panel.js index b8f1d63..723d806 100644 --- a/public/bluebey-studio/src/panel.js +++ b/public/bluebey-studio/src/panel.js @@ -567,15 +567,27 @@ export function buildPanel(app, root) { label: `${label}の涙の傾き`, min: -60, max: 60, step: 1, value: 0, format: (v) => `${Math.round(v)}°`, onInput: (value) => setTear({ tearTilt: value }), }); - eyeWidgets[key] = { open, closed, closedLines, threeFlip, eyeX, tear, tearY, tearX, tearTilt }; + const lowerLidEach = slider({ + label: `${label}の下まぶた`, min: 0, max: 1, step: 0.01, value: 0, format: (v) => v.toFixed(2), + onInput: (value) => { state.face.eyes[key].lowerLid = value; app.actions.refresh('face'); }, + }); + const lookX = slider({ + label: `${label}の目線(よこ)`, min: -1, max: 1, step: 0.01, value: 0, format: (v) => v.toFixed(2), + onInput: (value) => { state.face.eyes[key].lookX = value; app.actions.refresh('face'); }, + }); + const lookY = slider({ + label: `${label}の目線(たて)`, min: -1, max: 1, step: 0.01, value: 0, format: (v) => v.toFixed(2), + onInput: (value) => { state.face.eyes[key].lookY = value; app.actions.refresh('face'); }, + }); + eyeWidgets[key] = { open, closed, closedLines, threeFlip, eyeX, tear, tearY, tearX, tearTilt, lowerLid: lowerLidEach, lookX, lookY }; if (key === 'left') faceSection.add(controlRow(null, linkedToggle.el, { wide: true })); faceSection.add(open.el); faceSection.add(closed.el); faceSection.add(closedLines.el); faceSection.add(threeFlip.el); faceSection.add(eyeX.el); - // The tears go into the こまかい設定 box later, with the other extras. - tearWidgets.push(tear.el, tearY.el, tearX.el, tearTilt.el); + // 目線 and 下まぶた (per eye) and the tears go into the こまかい設定 box later. + tearWidgets.push(lowerLidEach.el, lookX.el, lookY.el, tear.el, tearY.el, tearX.el, tearTilt.el); } // One button to put every eye setting back to the artwork's defaults. The @@ -673,46 +685,25 @@ export function buildPanel(app, root) { + '「まぶたの傾き」で、細めた目を左右反転で傾けられます。' + '「白目を消す」は、細めた目やハートのときに白目を出しません。')); - const eyeAdvanced = details(faceSection.body, 'こまかい設定(瞳・色・ハート・まぶた・涙・手描き)'); + const eyeAdvanced = details(faceSection.body, 'こまかい設定(瞳・色・まぶたの太さ・左右べつの設定・手描き)'); const irisScaleSlider = slider({ label: '瞳の大きさ', min: 0.4, max: 1.4, step: 0.01, value: 1, format: (v) => v.toFixed(2), onInput: (value) => { state.face.eyes.irisScale = value; app.actions.refresh('face'); }, }); - const heartScaleSlider = slider({ - label: 'ハートの大きさ', min: 0.4, max: 2.6, step: 0.01, value: 2, format: (v) => v.toFixed(2), - onInput: (value) => { state.face.eyes.heartScale = value; app.actions.refresh('face'); }, - }); - const heartWhiteToggle = check({ - label: 'ハートのとき白目を消す', - value: true, - onChange: (value) => { state.face.eyes.heartHideWhite = value; app.actions.refresh('face'); }, - }); const heartColorField = colorField({ label: 'ハートの色', value: '#e0344f', swatches: ['#e0344f', '#ff5f8f', '#c2185b', '#150e1b'], onChange: (v) => { state.face.eyes.heartColor = v; app.actions.refresh('face'); }, }); - const lookMaxSlider = slider({ - label: '目線の振れ幅', min: 0, max: 1.2, step: 0.01, value: 1, format: (v) => v.toFixed(2), - onInput: (value) => { state.face.eyes.lookMax = value; app.actions.refresh('face'); }, - }); const lidWidthSlider = slider({ label: 'まぶたの太さ', min: 0, max: 30, step: 1, value: 14, format: (v) => `${Math.round(v)}`, onInput: (value) => { state.face.eyes.lidWidth = value; app.actions.refresh('face'); }, }); - const lowerLidSlider = slider({ - label: '下まぶた', min: 0, max: 1, step: 0.01, value: 0, format: (v) => v.toFixed(2), - onInput: (value) => { state.face.eyes.lowerLid = value; app.actions.refresh('face'); }, - }); eyeAdvanced.add(irisScaleSlider.el); - eyeAdvanced.add(heartScaleSlider.el); - eyeAdvanced.add(controlRow(null, heartWhiteToggle.el, { wide: true })); eyeAdvanced.add(heartColorField.el); - eyeAdvanced.add(lookMaxSlider.el); eyeAdvanced.add(lidWidthSlider.el); - eyeAdvanced.add(lowerLidSlider.el); - eyeAdvanced.add(hint('「下まぶた」を上げると、下からもまぶたが持ち上がります。' - + '「まぶたの形」「目の傾き」「目を細める」は、この上の「目」にあります。')); + eyeAdvanced.add(hint('「まぶたの形」「まぶたの傾き」「目を細める」「白目を消す」は、この上の「目」にあります。' + + 'ハートの大きさは「瞳の大きさ」で変わります。')); const highlightToggle = check({ label: '瞳のハイライト', @@ -729,8 +720,8 @@ export function buildPanel(app, root) { const eyeColorBox = details(faceSection.body, '色(白目・瞳・まぶたの線)'); for (const widget of Object.values(eyeColors)) eyeColorBox.add(widget.el); - // Tears (per eye) live here with the other extras. - const tearBox = details(eyeAdvanced.body, '涙(左右べつ)'); + // 目線・下まぶた・涙 (all per eye) live here with the other extras. + const tearBox = details(eyeAdvanced.body, '目線・下まぶた・涙(左右べつ)'); for (const el of tearWidgets) tearBox.add(el); const eyeSource = selectField({ @@ -2679,18 +2670,17 @@ export function buildPanel(app, root) { eyeWidgets[key].tearX.set(state.face.eyes[key].tearX ?? 0); eyeWidgets[key].tearTilt.set(state.face.eyes[key].tearTilt ?? 0); eyeWidgets[key].eyeX.set(state.face.eyes[key].eyeX ?? 0); + eyeWidgets[key].lookX.set(state.face.eyes[key].lookX ?? 0); + eyeWidgets[key].lookY.set(state.face.eyes[key].lookY ?? 0); + eyeWidgets[key].lowerLid.set(state.face.eyes[key].lowerLid ?? state.face.eyes.lowerLid ?? 0); } irisShapeSegment.set(state.face.eyes.left.irisShape ?? 'circle'); lookPad.set({ x: state.face.eyes.left.lookX, y: state.face.eyes.left.lookY }); highlightToggle.set(state.face.eyes.highlight); irisScaleSlider.set(state.face.eyes.irisScale); - heartScaleSlider.set(state.face.eyes.heartScale ?? 0.85); - heartWhiteToggle.set(state.face.eyes.heartHideWhite === true); heartColorField.set(state.face.eyes.heartColor ?? '#e0344f'); - lookMaxSlider.set(state.face.eyes.lookMax ?? 1); lidWidthSlider.set(state.face.eyes.lidWidth ?? 14); - lowerLidSlider.set(state.face.eyes.lowerLid ?? 0); lidShapeSegment.set(state.face.eyes.lidShape ?? 'curve'); lidTiltSlider.set(state.face.eyes.lidTilt ?? 0); hideWhiteToggle.set(state.face.eyes.hideWhite === true); diff --git a/public/bluebey-studio/src/presets.js b/public/bluebey-studio/src/presets.js index 9578db4..e12fe9a 100644 --- a/public/bluebey-studio/src/presets.js +++ b/public/bluebey-studio/src/presets.js @@ -199,7 +199,7 @@ export function defaultState() { left: { open: 1, lookX: 0, lookY: 0, eyeX: 0, closed: 'line', closedLines: 1, irisShape: 'circle', threeFlip: false, tear: 0, tearY: 0, tearX: 0, tearTilt: 0 }, right: { open: 1, lookX: 0, lookY: 0, eyeX: 0, closed: 'line', closedLines: 1, irisShape: 'circle', threeFlip: false, tear: 0, tearY: 0, tearX: 0, tearTilt: 0 }, irisScale: 1, - lookMax: 1, + lookMax: 1.2, highlight: true, lidWidth: 14, lowerLid: 0, @@ -512,11 +512,9 @@ export const FACE_PRESETS = [ left: { open: 1, lookX: -0.1, lookY: -0.3, closed: 'line', irisShape: 'heart' }, right: { open: 1, lookX: 0.1, lookY: -0.3, closed: 'line', irisShape: 'heart' }, irisScale: 1, - // Size 2 with the eyeball's white hidden is the size that reads best: - // any white behind the heart pokes out around its lobes. - heartScale: 2, + // The white behind a heart would poke out around its lobes, so hide it. + hideWhite: true, heartColor: '#e0344f', - heartHideWhite: true, highlight: false, }, mouth: { smile: 1.15, width: 1, thickness: 1.05, open: 0.16, tilt: 4, offsetY: 0, tongue: 0.9, corners: 1.3 }, -- cgit v1.3.1