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.js120
1 files changed, 63 insertions, 57 deletions
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);