From 174886a483d90756b1320c12aef0e6f296c6d425 Mon Sep 17 00:00:00 2001 From: Yasutake Yohei <61961825+yasutakeyohei@users.noreply.github.com> Date: Mon, 5 Oct 2026 14:13:21 +0900 Subject: bluebey-studio: 目メニューを再編し、閉じ目にもまぶたを効かせる MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 閉じ目を眼球の中身として上まぶた・下まぶたで隠せるようにした。まぶた設定を「まぶた」グループに、ハートの色を目の形の直下に、瞳と形の大きさを「大きさ」に統合。こまかい設定は解体し、横位置・手描きは個別の箱に分けた。 --- public/bluebey-studio/src/panel.js | 120 +++++++++++++++++++------------------ 1 file changed, 63 insertions(+), 57 deletions(-) (limited to 'public/bluebey-studio/src/panel.js') diff --git a/public/bluebey-studio/src/panel.js b/public/bluebey-studio/src/panel.js index b942c22..bc8e243 100644 --- a/public/bluebey-studio/src/panel.js +++ b/public/bluebey-studio/src/panel.js @@ -457,6 +457,7 @@ export function buildPanel(app, root) { state.face.eyes.right.closed = state.face.eyes.left.closed; state.face.eyes.right.closedLines = state.face.eyes.left.closedLines; state.face.eyes.right.irisShape = state.face.eyes.left.irisShape; + state.face.eyes.right.shape = state.face.eyes.left.shape; state.face.eyes.right.white = state.face.eyes.left.white ?? null; state.face.eyes.right.highlight = state.face.eyes.left.highlight ?? null; state.face.eyes.right.lookX = state.face.eyes.left.lookX; @@ -482,15 +483,19 @@ export function buildPanel(app, root) { // The shut line's stroke count is part of the shape itself. return `line${Math.min(3, Math.max(1, Math.round(eye.closedLines ?? 1)))}`; } + if (eye.shape) return eye.shape; return (eye.irisShape ?? 'circle') === 'heart' ? 'heart' : 'open'; }; const applyEyeShape = (eye, value) => { + eye.shape = value; eye.irisShape = value === 'heart' ? 'heart' : 'circle'; if (value === 'open' || value === 'heart') { eye.open = 1; return; } - eye.open = 0; + // A shut artwork is drawn as the eye's content with the lids open; the lid + // height is its own slider (上まぶたの高さ), so selecting one opens the lid. + eye.open = 1; if (value.startsWith('line')) { eye.closed = 'line'; eye.closedLines = Number(value.slice(4)) || 1; @@ -535,21 +540,8 @@ export function buildPanel(app, root) { label: '下まぶたの高さ', min: 0, max: 1, step: 0.01, value: 0, format: (v) => v.toFixed(2), onInput: (v) => { write((e) => { e.lowerLid = v; }); touched(); }, }); - // まぶたの形 and tilt are whole-face settings (the drawing mirrors the tilt - // between the eyes), so every group shows the same pair, right above 目の形. - const lidShape = segmented({ - label: 'まぶたの形', - options: [ - { value: 'curve', label: '曲線' }, - { value: 'flat', label: '直線(平ら)' }, - ], - value: 'curve', - onChange: (value) => { state.face.eyes.lidShape = value; touched(); }, - }); - const lidTilt = slider({ - label: 'まぶたの傾き', min: -40, max: 40, step: 1, value: 0, format: (v) => `${Math.round(v)}°`, - onInput: (value) => { state.face.eyes.lidTilt = value; touched(); }, - }); + // 目の形: the artwork the eye shows. A shut shape is drawn with the lids open, + // so 上まぶたの高さ can then be lowered onto it. const shape = segmented({ label: '目の形', options: [ @@ -564,9 +556,17 @@ export function buildPanel(app, root) { value: 'open', onChange: (v) => { write((e) => applyEyeShape(e, v)); touched(); }, }); - const shapeScale = slider({ - label: '形の大きさ', min: 0.5, max: 1.8, step: 0.01, value: 1, format: (v) => v.toFixed(2), - onInput: (v) => { write((e) => { e.shapeScale = v; }); touched(); }, + // The heart colour only matters for the heart, so it sits right under 目の形 + // and shows only then. + const heartColor = colorField({ + label: 'ハートの色', value: '#e0344f', + swatches: ['#e0344f', '#ff5f8f', '#c2185b', '#150e1b'], + onChange: (v) => { state.face.eyes.heartColor = v; touched(); }, + }); + // 大きさ: one knob for the iris, the heart and the shut artwork. + const size = slider({ + label: '大きさ', min: 0.4, max: 1.8, step: 0.01, value: 1, format: (v) => v.toFixed(2), + onInput: (v) => { state.face.eyes.irisScale = v; touched(); }, }); const threeFlip = segmented({ label: '3の向き', @@ -628,10 +628,9 @@ export function buildPanel(app, root) { group.add(open); group.add(lowerLid); - group.add(lidShape); - group.add(lidTilt); group.add(shape); - group.add(shapeScale); + group.add(heartColor); + group.add(size); group.add(threeFlip); group.add(controlRow(null, white.el, { wide: true })); group.add(controlRow(null, highlight.el, { wide: true })); @@ -648,10 +647,10 @@ export function buildPanel(app, root) { const shapeValue = eyeShapeOf(e); open.set(e.open); lowerLid.set(e.lowerLid ?? state.face.eyes.lowerLid ?? 0); - lidShape.set(state.face.eyes.lidShape ?? 'curve'); - lidTilt.set(state.face.eyes.lidTilt ?? 0); shape.set(shapeValue); - shapeScale.set(e.shapeScale ?? 1); + heartColor.set(state.face.eyes.heartColor ?? '#e0344f'); + heartColor.el.style.display = shapeValue === 'heart' ? '' : 'none'; + size.set(state.face.eyes.irisScale ?? 1); threeFlip.set(e.threeFlip ? 'flip' : 'normal'); threeFlip.el.style.display = shapeValue === 'three' ? '' : 'none'; white.set(eyeWhiteOf(e)); @@ -674,15 +673,40 @@ export function buildPanel(app, root) { const leftControls = makeEyeControls('left'); const rightControls = makeEyeControls('right'); + // --- まぶた (whole-face lid settings, shown above the per-eye groups) ------- + const lidGroup = eyeGroup('まぶた'); + const lidShapeSegment = segmented({ + label: 'まぶたの形', + options: [ + { value: 'curve', label: '曲線' }, + { value: 'flat', label: '直線(平ら)' }, + ], + value: 'curve', + onChange: (value) => { state.face.eyes.lidShape = 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 lidTiltSlider = slider({ + label: 'まぶたの傾き', min: -40, max: 40, step: 1, value: 0, format: (v) => `${Math.round(v)}°`, + onInput: (value) => { state.face.eyes.lidTilt = value; app.actions.refresh('face'); }, + }); + lidGroup.add(lidShapeSegment.el); + lidGroup.add(lidWidthSlider.el); + lidGroup.add(lidTiltSlider.el); + lidGroup.add(hint('「直線(平ら)」にすると、まぶたが平らに下りてきます。' + + '「まぶたの傾き」で、細めた目を左右反転で傾けられます。')); + faceSection.add(controlRow(null, linkedToggle.el, { wide: true })); + faceSection.add(lidGroup); faceSection.add(bothControls.group); faceSection.add(leftControls.group); faceSection.add(rightControls.group); - faceSection.add(hint('「まぶたの形」を「直線(平ら)」にすると、まぶたが平らに下りてきます。' - + '「まぶたの傾き」で、細めた目を左右反転で傾けられます。' - + '「目の形」の「2線」「3線」は、端点がつながった形になります。')); + faceSection.add(hint('「目の形」の「2線」「3線」は端点がつながった形になります。' + + '閉じ目の形も、上まぶた・下まぶたで上下から隠せます。')); - // 横位置 (per eye) rarely changes, so it lives in こまかい設定. + // 横位置 (per eye) rarely changes, so it gets its own small box. const eyeAdvancedWidgets = {}; for (const key of ['left', 'right']) { const label = key === 'left' ? '左の目' : '右の目'; @@ -710,29 +734,9 @@ export function buildPanel(app, root) { }], })); - faceSection.add(hint('「左右を連動させる」を入れると「両目」の設定が1つだけ出て、上まぶた・下まぶた・まぶたの形・形・白目・光彩・目線が両目でそろいます。' + faceSection.add(hint('「左右を連動させる」を入れると「両目」の設定が1つだけ出て、上まぶた・下まぶた・目の形・大きさ・白目・光彩・目線が両目でそろいます。' + 'はずすと「左の目」「右の目」の設定がそれぞれに出て、目線のパッドも2つになります。')); - 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 heartColorField = colorField({ - label: 'ハートの色', value: '#e0344f', - swatches: ['#e0344f', '#ff5f8f', '#c2185b', '#150e1b'], - onChange: (v) => { state.face.eyes.heartColor = v; 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'); }, - }); - eyeAdvanced.add(irisScaleSlider.el); - eyeAdvanced.add(heartColorField.el); - eyeAdvanced.add(lidWidthSlider.el); - eyeAdvanced.add(hint('「まぶたの形」「まぶたの傾き」「白目」「光彩」は、この上の「目」にあります。' - + 'ハートの大きさは「瞳の大きさ」で変わります。')); - const eyeColors = { white: colorField({ label: '白目', value: '#ffffff', onChange: (v) => { state.face.eyes.white = v; app.actions.refresh('face'); } }), iris: colorField({ label: '瞳', value: '#150e1b', onChange: (v) => { state.face.eyes.iris = v; app.actions.refresh('face'); } }), @@ -741,8 +745,8 @@ export function buildPanel(app, root) { const eyeColorBox = details(faceSection.body, '色(白目・瞳・まぶたの線)'); for (const widget of Object.values(eyeColors)) eyeColorBox.add(widget.el); - // 横位置 (per eye) lives here with the other extras. - const tearBox = details(eyeAdvanced.body, '横位置(左右べつ)'); + // 横位置 (per eye) is rarely used, so it stays in its own small box. + const tearBox = details(faceSection.body, '横位置(左右べつ)'); for (const el of tearWidgets) tearBox.add(el); const eyeSource = selectField({ @@ -756,7 +760,9 @@ export function buildPanel(app, root) { value: 'parametric', onChange: (value) => { state.face.eyes.source = value; app.actions.refresh('face'); }, }); - eyeAdvanced.add(eyeSource.el); + // 手描きの目(パーツの選択と読み込み)は、大きさやまぶたとは別の箱にまとめる。 + const eyeArtBox = details(faceSection.body, '手描きの目(パーツ・読み込み)'); + eyeArtBox.add(eyeSource.el); // 手描き画像の読み込み(GIMP などで描いた PNG をそのまま使えます) const rebuildEyeOptions = () => { @@ -793,8 +799,8 @@ export function buildPanel(app, root) { toast('画像を読み込めませんでした(PNG / JPG をお使いください)'); } }); - eyeAdvanced.add(eyeImagePicker); - eyeAdvanced.add(buttons({ + eyeArtBox.add(eyeImagePicker); + eyeArtBox.add(buttons({ items: [{ id: 'eyeimg', label: '目の画像を読み込む(PNG)', onClick: () => { if (confirmImageLicense()) eyeImagePicker.click(); } }], })); @@ -2760,9 +2766,9 @@ export function buildPanel(app, root) { w.eyeX.set(eye.eyeX ?? 0); } - irisScaleSlider.set(state.face.eyes.irisScale); - heartColorField.set(state.face.eyes.heartColor ?? '#e0344f'); lidWidthSlider.set(state.face.eyes.lidWidth ?? 14); + lidShapeSegment.set(state.face.eyes.lidShape ?? 'curve'); + lidTiltSlider.set(state.face.eyes.lidTilt ?? 0); browToggle.set(state.face.eyes.brow.enabled); browSliders.angle.set(state.face.eyes.brow.angle); browSliders.height.set(state.face.eyes.brow.height); -- cgit v1.3.1