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.js92
1 files changed, 28 insertions, 64 deletions
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(() => {