aboutsummaryrefslogtreecommitdiffhomepage
path: root/public/bluebey-studio/src/panel.js
diff options
context:
space:
mode:
Diffstat (limited to 'public/bluebey-studio/src/panel.js')
-rw-r--r--public/bluebey-studio/src/panel.js119
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);