aboutsummaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
authorYasutake Yohei <61961825+yasutakeyohei@users.noreply.github.com>2026-10-04 23:24:51 +0900
committerYasutake Yohei <61961825+yasutakeyohei@users.noreply.github.com>2026-10-04 23:24:51 +0900
commiteed94d378af577171e24fe78b4b159be5ca50317 (patch)
tree688d2da48d6823a976af33f7661d9a7ce5c0bf19
parentbb0d752b04eae60b88ec162d07e9409483159d95 (diff)
bluebey-studio: 下まぶたと目線の左右別設定、Oの口の拡大、白目/ハート/振れ幅の整理
下まぶた・目線を左右べつに設定可能に。丸く開く口を顔半分ほどまで拡大。白目を消すを一般化し「ハートのとき白目を消す」を削除。ハートの大きさは瞳の大きさに統合し、目線の振れ幅のスライダーを廃止(既定1.2)。
-rw-r--r--public/bluebey-studio/src/faceArt.js16
-rw-r--r--public/bluebey-studio/src/main.js2
-rw-r--r--public/bluebey-studio/src/panel.js56
-rw-r--r--public/bluebey-studio/src/presets.js8
4 files changed, 36 insertions, 46 deletions
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 },