diff options
| author | Yasutake Yohei <61961825+yasutakeyohei@users.noreply.github.com> | 2026-10-05 13:37:46 +0900 |
|---|---|---|
| committer | Yasutake Yohei <61961825+yasutakeyohei@users.noreply.github.com> | 2026-10-05 13:37:46 +0900 |
| commit | ee7c87a56252ec295119823a2821e5c1f8ec5163 (patch) | |
| tree | 773fa9322a0e3625acbee20788ddff6db2f2584f /public/bluebey-studio/src/panel.js | |
| parent | 645b204e26962c9adfc11047052020a777b1ca1d (diff) | |
bluebey-studio: ぎざぎざの歯の深さ、形の大きさ、涙スイッチ、まぶたの高さを整理
Diffstat (limited to 'public/bluebey-studio/src/panel.js')
| -rw-r--r-- | public/bluebey-studio/src/panel.js | 119 |
1 files changed, 72 insertions, 47 deletions
diff --git a/public/bluebey-studio/src/panel.js b/public/bluebey-studio/src/panel.js index d95826f..7d6c303 100644 --- a/public/bluebey-studio/src/panel.js +++ b/public/bluebey-studio/src/panel.js @@ -523,10 +523,14 @@ export function buildPanel(app, root) { const group = eyeGroup(mode === 'both' ? '両目' : (mode === 'left' ? '左の目' : '右の目')); const open = slider({ - label: '目の開き', min: 0, max: 1, step: 0.01, value: 1, + label: '上まぶたの高さ', min: 0, max: 1, step: 0.01, value: 1, format: (v) => (v < 0.02 ? '閉じ' : v > 0.98 ? '開き' : `${Math.round(v * 100)}%`), onInput: (v) => { write((e) => { e.open = v; }); touched(); }, }); + const lowerLid = slider({ + label: '下まぶたの高さ', min: 0, max: 1, step: 0.01, value: 0, format: (v) => v.toFixed(2), + onInput: (v) => { write((e) => { e.lowerLid = v; }); touched(); }, + }); const shape = segmented({ label: '目の形', options: [ @@ -539,6 +543,10 @@ 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(); }, + }); const closedLines = segmented({ label: '線の数', options: [ @@ -569,10 +577,6 @@ export function buildPanel(app, root) { value: true, onChange: (v) => { write((e) => { e.highlight = v; }); touched(); }, }); - const eyeX = slider({ - label: '横位置', min: -200, max: 200, step: 1, value: 0, format: (v) => `${Math.round(v)}`, - onInput: (v) => { write((e) => { e.eyeX = v; }); touched(); }, - }); const pad = xyPad({ value: { x: 0, y: 0 }, center: 'eye', @@ -580,6 +584,11 @@ export function buildPanel(app, root) { }); // 涙: per eye. Linked, one set drives both - size and height match, while the // sideways position and the tilt mirror so the pair stays symmetric. + const tearOn = check({ + label: '涙を出す', + value: false, + onChange: (v) => { write((e) => { e.tearOn = v; }); touched(); }, + }); const writeTear = (patch) => { Object.assign(state.face.eyes[primary], patch); if (mode === 'both') { @@ -591,7 +600,7 @@ export function buildPanel(app, root) { touched(); }; const tear = slider({ - label: '涙', min: 0, max: 1.6, step: 0.01, value: 0, format: (v) => v.toFixed(2), + label: '涙の大きさ', min: 0.3, max: 1.6, step: 0.01, value: 0.3, format: (v) => v.toFixed(2), onInput: (v) => writeTear({ tear: v }), }); const tearY = slider({ @@ -608,36 +617,44 @@ export function buildPanel(app, root) { }); group.add(open); + group.add(lowerLid); group.add(shape); + group.add(shapeScale); group.add(closedLines); group.add(threeFlip); group.add(controlRow(null, white.el, { wide: true })); group.add(controlRow(null, highlight.el, { wide: true })); - group.add(eyeX); - group.add(controlRow('目線', h('div', { class: 'pad-wrap' }, pad.el, - h('p', { class: 'hint', text: 'ドラッグで目線。ダブルクリックで正面に戻ります。' })))); + group.add(controlRow(null, tearOn.el, { wide: true })); group.add(tear); group.add(tearY); group.add(tearX); group.add(tearTilt); + group.add(controlRow('目線', h('div', { class: 'pad-wrap' }, pad.el, + h('p', { class: 'hint', text: 'ドラッグで目線。ダブルクリックで正面に戻ります。' })))); const syncOne = () => { const e = state.face.eyes[primary]; const shapeValue = eyeShapeOf(e); open.set(e.open); + lowerLid.set(e.lowerLid ?? state.face.eyes.lowerLid ?? 0); shape.set(shapeValue); + shapeScale.set(e.shapeScale ?? 1); closedLines.set(String(e.closedLines ?? 1)); closedLines.el.style.display = shapeValue === 'line' ? '' : 'none'; threeFlip.set(e.threeFlip ? 'flip' : 'normal'); threeFlip.el.style.display = shapeValue === 'three' ? '' : 'none'; white.set(eyeWhiteOf(e)); highlight.set(eyeHighlightOf(e)); - eyeX.set(e.eyeX ?? 0); - pad.set({ x: e.lookX ?? 0, y: e.lookY ?? 0 }); - tear.set(e.tear ?? 0); + const tearShown = e.tearOn === true; + tearOn.set(tearShown); + for (const widget of [tear, tearY, tearX, tearTilt]) { + widget.el.style.display = tearShown ? '' : 'none'; + } + tear.set(e.tear ?? 0.3); tearY.set(e.tearY ?? 0); tearX.set(e.tearX ?? 0); tearTilt.set(e.tearTilt ?? 0); + pad.set({ x: e.lookX ?? 0, y: e.lookY ?? 0 }); }; return { group, sync: syncOne }; }; @@ -651,40 +668,20 @@ export function buildPanel(app, root) { faceSection.add(leftControls.group); faceSection.add(rightControls.group); - // 下まぶた (per eye) lives in こまかい設定. + // 横位置 (per eye) rarely changes, so it lives in こまかい設定. const eyeAdvancedWidgets = {}; for (const key of ['left', 'right']) { const label = key === 'left' ? '左の目' : '右の目'; - const lowerLid = 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 eyeX = slider({ + label: `${label}の横位置`, min: -200, max: 200, step: 1, value: 0, format: (v) => `${Math.round(v)}`, + onInput: (value) => { state.face.eyes[key].eyeX = value; app.actions.refresh('face'); }, }); - eyeAdvancedWidgets[key] = { lowerLid }; - tearWidgets.push(lowerLid.el); + eyeAdvancedWidgets[key] = { eyeX }; + tearWidgets.push(eyeX.el); } - // One button to put every eye setting back to the artwork's defaults. The - // artwork *source* is kept: resetting the numbers should not throw away a - // hand-drawn image the user loaded. - faceSection.add(buttons({ - items: [{ - id: 'eyesReset', - label: '目の設定をリセット', - onClick: () => { - const source = state.face.eyes.source; - state.face.eyes = { ...defaultState().face.eyes, source }; - app.actions.refresh('face'); - sync(); - }, - }], - })); - - faceSection.add(hint('「左右を連動させる」を入れると「両目」の設定が1つだけ出て、開き・形・白目・光彩・目線が両目でそろいます。' - + 'はずすと「左の目」「右の目」の設定がそれぞれに出て、目線のパッドも2つになります。' - + '涙のよこ位置と傾きは左右で反転し、線対称になります。')); - // 目を細める (and its undo) grouped with the lid shape and tilt that shape the - // squint, so the whole narrowed-eye look sits together. + // squint. It sits right under the lid-height sliders it drives. const squintGroup = eyeGroup('目を細める'); squintGroup.add(buttons({ items: [ @@ -693,10 +690,11 @@ export function buildPanel(app, root) { label: '目を細める', onClick: () => { // The upper lid comes partway down and the lower lid rises to meet it. - // `open` is per eye (kept in step here); `lowerLid` is shared. + // Both lids are per eye, so both eyes are set here. state.face.eyes.left.open = 0.5; state.face.eyes.right.open = 0.5; - state.face.eyes.lowerLid = 0.5; + state.face.eyes.left.lowerLid = 0.5; + state.face.eyes.right.lowerLid = 0.5; app.actions.refresh('face'); sync(); }, @@ -707,7 +705,8 @@ export function buildPanel(app, root) { onClick: () => { state.face.eyes.left.open = 1; state.face.eyes.right.open = 1; - state.face.eyes.lowerLid = 0; + state.face.eyes.left.lowerLid = 0; + state.face.eyes.right.lowerLid = 0; app.actions.refresh('face'); sync(); }, @@ -733,6 +732,25 @@ export function buildPanel(app, root) { + '「まぶたの傾き」で、細めた目を左右反転で傾けられます。')); faceSection.add(squintGroup); + // One button to put every eye setting back to the artwork's defaults. The + // artwork *source* is kept: resetting the numbers should not throw away a + // hand-drawn image the user loaded. + faceSection.add(buttons({ + items: [{ + id: 'eyesReset', + label: '目の設定をリセット', + onClick: () => { + const source = state.face.eyes.source; + state.face.eyes = { ...defaultState().face.eyes, source }; + app.actions.refresh('face'); + sync(); + }, + }], + })); + + 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), @@ -761,8 +779,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) lives here with the other extras. + const tearBox = details(eyeAdvanced.body, '横位置(左右べつ)'); for (const el of tearWidgets) tearBox.add(el); const eyeSource = selectField({ @@ -1122,7 +1140,7 @@ export function buildPanel(app, root) { { value: 'curve', label: 'カーブ' }, ], value: 'off', - onChange: (value) => { state.face.eyes.headMark.shape = value; app.actions.refresh('face'); }, + onChange: (value) => { state.face.eyes.headMark.shape = value; app.actions.refresh('face'); sync(); }, }); faceSection.add(headMarkShape.el); @@ -1138,6 +1156,10 @@ export function buildPanel(app, root) { label: '大きさ', min: 0.05, max: 2.2, step: 0.01, value: 1, format: (v) => v.toFixed(2), onInput: (v) => { state.face.eyes.headMark.size = v; app.actions.refresh('face'); }, }), + teeth: slider({ + label: '歯の深さ', min: 0.02, max: 0.4, step: 0.01, value: 0.12, format: (v) => v.toFixed(2), + onInput: (v) => { state.face.eyes.headMark.teeth = v; app.actions.refresh('face'); }, + }), offsetX: slider({ label: 'よこの位置', min: -260, max: 260, step: 1, value: 0, format: (v) => `${Math.round(v)}`, onInput: (v) => { state.face.eyes.headMark.offsetX = v; app.actions.refresh('face'); }, @@ -1148,6 +1170,8 @@ export function buildPanel(app, root) { }), }; faceSection.add(headMarkSliders.size.el); + faceSection.add(headMarkSliders.teeth.el); + //「歯の深さ」はぎざぎざのときだけ意味があるので、それ以外では隠します。 const headMarkBox = details(faceSection.body, 'こまかい設定(位置・色)'); headMarkBox.add(headMarkSliders.offsetX.el); headMarkBox.add(headMarkSliders.offsetY.el); @@ -1163,7 +1187,7 @@ export function buildPanel(app, root) { }); headMarkBox.add(headMarkColor.el); headMarkBox.add(headMarkColor2.el); - headMarkBox.add(hint('「かぶせ」は頭にかぶせる丸い髪、「ぎざぎざ」は生え際がギザギザにとがった髪、' + headMarkBox.add(hint('「かぶせ」は頭にかぶせる丸い髪、「ぎざぎざ」は生え際がギザギザにとがった髪(「歯の深さ」でギザギザの深さを変えられます)、' + '「はちわれ」は中央が鋭く持ち上がった生え際、「カーブ」はドラえもんのように単純にカーブした髪です。' + '「たかさ」で生え際を目の中心あたりまで下げられます。' + '「髪を目の前に出す」を切ると、髪が目の後ろに回ります。')); @@ -2747,7 +2771,7 @@ export function buildPanel(app, root) { for (const key of ['left', 'right']) { const eye = state.face.eyes[key]; const w = eyeAdvancedWidgets[key]; - w.lowerLid.set(eye.lowerLid ?? state.face.eyes.lowerLid ?? 0); + w.eyeX.set(eye.eyeX ?? 0); } irisScaleSlider.set(state.face.eyes.irisScale); @@ -2786,6 +2810,7 @@ export function buildPanel(app, root) { for (const [key, widget] of Object.entries(cheekSliders)) widget.set(state.face.eyes.cheeks[key]); cheekColor.set(state.face.eyes.cheeks.color); headMarkShape.set(state.face.eyes.headMark.shape ?? 'off'); + headMarkSliders.teeth.el.style.display = (state.face.eyes.headMark.shape === 'fringe') ? '' : 'none'; headMarkOverEyes.set(state.face.eyes.headMark.overEyes !== false); for (const [key, widget] of Object.entries(headMarkSliders)) widget.set(state.face.eyes.headMark[key]); headMarkColor.set(state.face.eyes.headMark.color); |
