aboutsummaryrefslogtreecommitdiffhomepage
path: root/public/bluebey-studio/src
diff options
context:
space:
mode:
Diffstat (limited to 'public/bluebey-studio/src')
-rw-r--r--public/bluebey-studio/src/faceArt.js13
-rw-r--r--public/bluebey-studio/src/panel.js44
2 files changed, 45 insertions, 12 deletions
diff --git a/public/bluebey-studio/src/faceArt.js b/public/bluebey-studio/src/faceArt.js
index 01890b5..f61a865 100644
--- a/public/bluebey-studio/src/faceArt.js
+++ b/public/bluebey-studio/src/faceArt.js
@@ -281,6 +281,10 @@ export const MOUTH_LAYOUT = {
// the corners, which is what reads as a *gentler* curve at the same depth.
sagBend: 0.2,
thickness: 21,
+ // How big the round "O" oval gets at `round: 1`, as a multiple of the smile's
+ // own sag. The oval's size comes from this and `round` alone: `thickness` is
+ // only the stroke weight, so 口の太さ and 丸く開く no longer move together.
+ roundScale: 1.3,
// Kept for reference: the 2022 plane only showed these rows.
bandTop: 46,
bandBottom: 332,
@@ -1571,12 +1575,13 @@ 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 * 1.05 * roundAmount * clamp(p.thickness ?? 1, 0.5, 2);
+ let ry = L.sag * 1.05 * L.roundScale * roundAmount;
// 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);
+ // roughly half the head. The stroke's share is a fixed margin, not the live
+ // `thickness`: otherwise 口の太さ would quietly resize the oval too.
+ ry = Math.min(ry, (edgeBottom - L.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);
+ const centreY = clamp(y0 + sag * 0.5, L.noseClear + ry * 1.02, edgeBottom - L.thickness / 2 - ry * 1.02);
if (ry > 5) {
ctx.save();
ctx.translate(pivot.x, pivot.y);
diff --git a/public/bluebey-studio/src/panel.js b/public/bluebey-studio/src/panel.js
index 7d6c303..9913982 100644
--- a/public/bluebey-studio/src/panel.js
+++ b/public/bluebey-studio/src/panel.js
@@ -1310,7 +1310,7 @@ export function buildPanel(app, root) {
const mouthSliders = {
smile: slider({ label: '笑いの深さ', min: -1, max: 1.4, step: 0.01, value: 1, format: (v) => v.toFixed(2), onInput: (v) => { state.face.mouth.smile = v; state.face.mouth.cornerAngle = cornerAngleFor(v); app.actions.refresh('face'); sync(); } }),
open: slider({ label: '口の開き', min: 0, max: 1, step: 0.01, value: 0, format: (v) => v.toFixed(2), onInput: (v) => { state.face.mouth.open = v; app.actions.refresh('face'); } }),
- round: slider({ label: '丸く開く(Oの口)', min: 0, max: 1, step: 0.01, value: 0, format: (v) => v.toFixed(2), onInput: (v) => { state.face.mouth.round = v; app.actions.refresh('face'); } }),
+ round: slider({ label: 'Oの大きさ', min: 0, max: 1, step: 0.01, value: 0, format: (v) => v.toFixed(2), onInput: (v) => { state.face.mouth.round = v; app.actions.refresh('face'); } }),
width: slider({ label: '口の幅', min: 0.2, max: 1.6, step: 0.01, value: 1, format: (v) => v.toFixed(2), onInput: (v) => { state.face.mouth.width = v; app.actions.refresh('face'); } }),
length: slider({ label: '口の長さ', min: 0.2, max: 2.5, step: 0.01, value: 1, format: (v) => v.toFixed(2), onInput: (v) => { state.face.mouth.length = v; app.actions.refresh('face'); } }),
thickness: slider({ label: '口の太さ', min: 0.2, max: 3, step: 0.01, value: 1, format: (v) => v.toFixed(2), onInput: (v) => { state.face.mouth.thickness = v; app.actions.refresh('face'); } }),
@@ -1321,13 +1321,37 @@ export function buildPanel(app, root) {
corners: slider({ label: '口角の長さ', min: 0, max: 1.6, step: 0.01, value: 0.71, format: (v) => v.toFixed(2), onInput: (v) => { state.face.mouth.corners = v; app.actions.refresh('face'); } }),
cornerAngle: slider({ label: '口角の角度', min: -70, max: 70, step: 1, value: -8, format: (v) => `${Math.round(v)}°`, onInput: (v) => { state.face.mouth.cornerAngle = v; app.actions.refresh('face'); } }),
};
- const mouthVisible = ['smile', 'round', 'length', 'thickness', 'tilt', 'offsetY'];
- for (const key of mouthVisible) faceSection.add(mouthSliders[key].el);
- faceSection.add(hint('「口の長さ」は口の横の長さです。「口の幅」は丸い口(O)の横の広がりで、'
- + 'こまかい設定にあります。'));
- const mouthBox = details(faceSection.body, 'こまかい設定(幅・開き・舌・口角)');
- for (const key of ['width', 'open', 'tongue', 'tonguePos', 'corners', 'cornerAngle']) mouthBox.add(mouthSliders[key].el);
- mouthBox.add(buttons({
+ // 口には「ふつうの口」と「丸く開く(O)」の2つの形があり、使うスライダーが
+ // まったく違う。形を切り替えると、その形で使うものだけを出す。
+ const mouthModeOf = (mouth) => (mouth.round > 0.004 ? 'round' : 'curve');
+ const mouthRowKeys = {
+ curve: ['smile', 'length', 'width', 'thickness', 'open', 'tongue', 'tonguePos', 'corners', 'cornerAngle', 'tilt', 'offsetY'],
+ round: ['round', 'width', 'thickness', 'tongue', 'tilt', 'offsetY'],
+ };
+ // DOM order is fixed here; the mode only shows or hides rows.
+ const mouthRowOrder = ['round', 'smile', 'length', 'width', 'thickness', 'open', 'tongue', 'tonguePos', 'corners', 'cornerAngle', 'tilt', 'offsetY'];
+ const mouthMode = segmented({
+ label: '口の形',
+ options: [
+ { value: 'curve', label: 'ふつうの口' },
+ { value: 'round', label: '丸く開く(O)' },
+ ],
+ value: 'curve',
+ onChange: (shape) => {
+ if (shape === 'round') {
+ if (!(state.face.mouth.round > 0.004)) state.face.mouth.round = 0.6;
+ } else {
+ state.face.mouth.round = 0;
+ }
+ app.actions.refresh('face');
+ sync();
+ },
+ });
+ faceSection.add(mouthMode);
+ for (const key of mouthRowOrder) faceSection.add(mouthSliders[key].el);
+ faceSection.add(hint('「口の形」を切り替えると、その形で使うスライダーだけが出ます。'
+ + 'ふつうの口は笑いの深さと口の開き、丸く開く口(O)はOの大きさで形が決まります。'));
+ faceSection.add(buttons({
items: [{
id: 'mouthreset',
label: '口を初期値に戻す',
@@ -2830,6 +2854,10 @@ export function buildPanel(app, root) {
mouthToggle.set(state.face.mouth.visible);
for (const [key, widget] of Object.entries(mouthSliders)) widget.set(state.face.mouth[key]);
for (const [key, widget] of Object.entries(mouthColors)) widget.set(state.face.mouth[key]);
+ const mouthShape = mouthModeOf(state.face.mouth);
+ mouthMode.set(mouthShape);
+ const shown = new Set(mouthRowKeys[mouthShape]);
+ for (const key of mouthRowOrder) mouthSliders[key].el.style.display = shown.has(key) ? '' : 'none';
});
syncers.push(() => {