From 1bf7ee115176bc366315a90ab31740e6cd968744 Mon Sep 17 00:00:00 2001 From: Yasutake Yohei <61961825+yasutakeyohei@users.noreply.github.com> Date: Mon, 5 Oct 2026 14:42:08 +0900 Subject: bluebey-studio: ハートの白目内クリップ・まつげ追従・まばたき修正、帽子の線画と位置、手描き目UI削除 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 指定場所を見るときもハートが白目内に収まるようクリップ。まつげを上まぶたに連動。閉じ目はまばたきせず、ハート選択後のまばたきで3の目/わらうが一瞬出る不具合を修正。帽子をランナンスタイル登録して線画対応、横・奥行き追加。手描きの目UIを削除。 --- public/bluebey-studio/src/panel.js | 92 ++++++++++++-------------------------- 1 file changed, 28 insertions(+), 64 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 b5e4e6e..2b762f1 100644 --- a/public/bluebey-studio/src/panel.js +++ b/public/bluebey-studio/src/panel.js @@ -7,7 +7,6 @@ import { defaultState, applyPatch, BEARD_KIND_DEFAULTS, } from './presets.js'; import { STYLE_DEFS } from './styles.js'; -import { EYE_VARIANTS } from './model.js'; import { ENVIRONMENTS } from './look.js'; import { BACKGROUND_PRESETS, EFFECT_PRESETS } from './background.js'; import { PROP_LIBRARY, applyPropFaceScale, applyPropText } from './props.js'; @@ -483,13 +482,20 @@ export function buildPanel(app, root) { // one menu. `open`/`closed`/`irisShape` stay the stored fields; this reads and // writes them together so the menu always shows the current look. const eyeShapeOf = (eye) => { + const shape = eye.shape; + const explicitShut = shape === 'three' || shape === 'arch' + || (typeof shape === 'string' && shape.startsWith('line')); + if (explicitShut) return shape; if ((eye.open ?? 1) <= 0.02) { - if (eye.closed === 'three') return 'three'; - if (eye.closed === 'arch') return 'arch'; - // 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 (!shape) { + if (eye.closed === 'three') return 'three'; + if (eye.closed === 'arch') return 'arch'; + return `line${Math.min(3, Math.max(1, Math.round(eye.closedLines ?? 1)))}`; + } + // An open/heart eye whose lid is all the way down is just a blink. + return 'line1'; } - if (eye.shape) return eye.shape; + if (shape) return shape; return (eye.irisShape ?? 'circle') === 'heart' ? 'heart' : 'open'; }; const applyEyeShape = (eye, value) => { @@ -497,6 +503,10 @@ export function buildPanel(app, root) { eye.irisShape = value === 'heart' ? 'heart' : 'circle'; if (value === 'open' || value === 'heart') { eye.open = 1; + // Clear any stale shut fields so a blink (open -> 0) shows a plain line and + // never the 3 or the arch that was picked before. + eye.closed = 'line'; + eye.closedLines = 1; return; } // A shut artwork is drawn as the eye's content with the lids open; the lid @@ -796,61 +806,6 @@ export function buildPanel(app, root) { const tearBox = details(faceSection.body, '横位置(左右べつ)'); for (const el of tearWidgets) tearBox.add(el); - const eyeSource = selectField({ - label: '目のパーツ', - // 実際にモデルに入っている手描きパーツだけを一覧に出す(消したものが残ると混乱するため) - options: [ - { value: 'parametric', label: 'パラメータで作る(おすすめ)' }, - ...EYE_VARIANTS.filter((variant) => model.originals.eyes[variant.key]) - .map((variant) => ({ value: variant.key, label: `手描き:${variant.label}` })), - ], - value: 'parametric', - onChange: (value) => { state.face.eyes.source = value; app.actions.refresh('face'); }, - }); - // 手描きの目(パーツの選択と読み込み)は、大きさやまぶたとは別の箱にまとめる。 - const eyeArtBox = details(faceSection.body, '手描きの目(パーツ・読み込み)'); - eyeArtBox.add(eyeSource.el); - - // 手描き画像の読み込み(GIMP などで描いた PNG をそのまま使えます) - const rebuildEyeOptions = () => { - const select = eyeSource.el.querySelector('select'); - if (!select) return; - const current = select.value; - select.replaceChildren(); - select.append(h('option', { value: 'parametric', text: 'パラメータで作る(おすすめ)' })); - for (const variant of EYE_VARIANTS) { - if (model.originals.eyes[variant.key]) { - select.append(h('option', { value: variant.key, text: `手描き:${variant.label}` })); - } - } - for (const [key, name] of app.face.customNames) { - select.append(h('option', { value: key, text: `読み込み:${name}` })); - } - select.value = current; - }; - rebuildEyeOptions(); - - const eyeImagePicker = h('input', { type: 'file', accept: 'image/*', style: { display: 'none' } }); - eyeImagePicker.addEventListener('change', async () => { - const file = eyeImagePicker.files?.[0]; - eyeImagePicker.value = ''; - if (!file) return; - try { - await app.face.loadImage('eyes', file); - state.face.eyes.source = app.face.eyeSource; - rebuildEyeOptions(); - sync(); - toast(`目の画像「${file.name}」を読み込みました`); - } catch (error) { - console.error(error); - toast('画像を読み込めませんでした(PNG / JPG をお使いください)'); - } - }); - eyeArtBox.add(eyeImagePicker); - eyeArtBox.add(buttons({ - items: [{ id: 'eyeimg', label: '目の画像を読み込む(PNG)', onClick: () => { if (confirmImageLicense()) eyeImagePicker.click(); } }], - })); - // --- eyebrows (the original character has none; this is extra range) faceSection = faceTabSections.brow; faceSection.add(hint('ぶるべーには眉がありません。怒った顔など、必要なときだけ出してください。')); @@ -1732,9 +1687,17 @@ export function buildPanel(app, root) { const hatBox = details(hatSection.body, '位置と傾き'); const hatSliders = { height: slider({ - label: '高さ', min: -0.6, max: 1.2, step: 0.01, value: 0, format: (v) => v.toFixed(2), + label: '高さ', min: -1.2, max: 1.2, step: 0.01, value: 0, format: (v) => v.toFixed(2), onInput: (v) => { state.face.hat.height = v; app.actions.refresh('all'); }, }), + x: slider({ + label: 'よこ位置', min: -1.2, max: 1.2, step: 0.01, value: 0, format: (v) => v.toFixed(2), + onInput: (v) => { state.face.hat.x = v; app.actions.refresh('all'); }, + }), + z: slider({ + label: 'おくゆき', min: -1.2, max: 1.2, step: 0.01, value: 0, format: (v) => v.toFixed(2), + onInput: (v) => { state.face.hat.z = v; app.actions.refresh('all'); }, + }), tiltX: slider({ label: '前後の傾き', min: -45, max: 45, step: 1, value: 0, format: (v) => `${Math.round(v)}°`, onInput: (v) => { state.face.hat.tiltX = v; app.actions.refresh('all'); }, @@ -1745,9 +1708,11 @@ export function buildPanel(app, root) { }), }; for (const widget of Object.values(hatSliders)) hatBox.add(widget.el); - hatBox.add(hint('「高さ」で帽子を浮かせたり深くかぶせたり、「前後の傾き」「左右の傾き」で斜めにかぶせられます。')); + hatBox.add(hint('「高さ」「よこ位置」「おくゆき」で帽子を動かし、「前後の傾き」「左右の傾き」で斜めにかぶせられます。')); syncers.push(() => { hatSliders.height.set(state.face.hat?.height ?? 0); + hatSliders.x.set(state.face.hat?.x ?? 0); + hatSliders.z.set(state.face.hat?.z ?? 0); hatSliders.tiltX.set(state.face.hat?.tiltX ?? 0); hatSliders.tiltZ.set(state.face.hat?.tiltZ ?? 0); }); @@ -2836,7 +2801,6 @@ export function buildPanel(app, root) { eyeColors.white.set(state.face.eyes.white); eyeColors.iris.set(state.face.eyes.iris); eyeColors.line.set(state.face.eyes.line); - eyeSource.set(state.face.eyes.source); }); syncers.push(() => { -- cgit v1.3.1