aboutsummaryrefslogtreecommitdiffhomepage
path: root/bluebey-studio/src/panel.js
diff options
context:
space:
mode:
authorYasutake Yohei <61961825+yasutakeyohei@users.noreply.github.com>2026-10-09 08:33:42 +0900
committerYasutake Yohei <61961825+yasutakeyohei@users.noreply.github.com>2026-10-09 08:33:42 +0900
commit9deb6762ef5794b340943e2d2845da79c437dce5 (patch)
tree817a24ae56d7b06f5964f68682432ccb9febff02 /bluebey-studio/src/panel.js
parent1b883a70b9d3427e7cb65eaf99d2aa7ca5a1cb1c (diff)
bluebey-studio: 準拠版(フキダシ・眉毛・髪・ひげ・ほっぺ・体の色を削除)
ぶるべーはしゃべらない・姿を変えない方針に合わせた準拠版。セリフ/ナレーション/吹き出し・口パク・眉毛(ゴルゴ等)・髪(頭の模様)・ひげ・ほっぺ・体の色/テーマを削除。目・口・眼鏡・帽子・鼻ちょうちん・ポーズ・背景・小物・光・書き出しは維持。caption.js / mouthFlap.js は削除。
Diffstat (limited to 'bluebey-studio/src/panel.js')
-rw-r--r--bluebey-studio/src/panel.js722
1 files changed, 42 insertions, 680 deletions
diff --git a/bluebey-studio/src/panel.js b/bluebey-studio/src/panel.js
index 0ee94ac..a6e971d 100644
--- a/bluebey-studio/src/panel.js
+++ b/bluebey-studio/src/panel.js
@@ -1,10 +1,9 @@
import {
h, section as uiSection, subhead, hint, slider, check, segmented, buttons,
- colorField, xyPad, tailPad, selectField, controlRow, tabs, details, toast, icon, svgIcon,
+ colorField, xyPad, selectField, controlRow, tabs, details, toast, icon, svgIcon,
} from './ui.js';
import {
- FACE_PRESETS, POSE_PRESETS, THEMES, BUBBLE_STYLES, CAPTION_PRESETS,
- defaultState, applyPatch, BEARD_KIND_DEFAULTS, HEAD_MARK_KIND_DEFAULTS,
+ FACE_PRESETS, POSE_PRESETS, defaultState, applyPatch,
} from './presets.js';
import { STYLE_DEFS } from './styles.js';
import { ENVIRONMENTS } from './look.js';
@@ -26,21 +25,14 @@ const SECTION_ICONS = {
'うごき': 'M8 5l10 7-10 7V5Z',
'プリセット': 'M12 3.6a8.4 8.4 0 1 1 0 16.8 8.4 8.4 0 1 1 0-16.8ZM8.9 9.8v1M15.1 9.8v1M8.3 13.9a4.7 4.7 0 0 0 7.4 0',
'目': 'M2.5 12s3.6-6 9.5-6 9.5 6 9.5 6-3.6 6-9.5 6-9.5-6-9.5-6ZM12 14.6a2.6 2.6 0 1 0 0-5.2 2.6 2.6 0 0 0 0 5.2Z',
- '眉': 'M4 11q3.2-3.2 7-2M13 9q3.8-1.2 7 2',
'眼鏡・サングラス': 'M8 10.2a3.2 3.2 0 1 0 0 6.4 3.2 3.2 0 0 0 0-6.4ZM16 10.2a3.2 3.2 0 1 0 0 6.4 3.2 3.2 0 0 0 0-6.4ZM11.2 13h1.6M4.8 11.7 3 10.3M19.2 11.7 21 10.3',
- '髪': 'M4 14c0-4.8 3.6-7.6 8-7.6s8 2.8 8 7.6M4 14c2.6-1.4 5.3-1.9 8-1.8M20 14c-2.6-1.4-5.3-1.9-8-1.8',
- 'ほっぺ': 'M6.4 14.6a2 1.4 0 1 0 0 .01ZM17.6 14.6a2 1.4 0 1 0 0 .01ZM12 4a8 8 0 1 0 0 16 8 8 0 0 0 0-16Z',
'鼻ちょうちん': 'M15 4.6a4.6 4.6 0 1 0 0 9.2 4.6 4.6 0 0 0 0-9.2ZM10.6 9.6 4.8 11.4',
- 'ひげ': 'M4.4 11.2c2.6-1.2 5.2-.6 7.6 2 2.4-2.6 5-3.2 7.6-2',
'口': 'M4.6 10c3 4.8 11.8 4.8 14.8 0',
- '口パク(声は出ません)': 'M4 9v6h3l4 3V6L7 9H4ZM15.5 9.5l4 5M19.5 9.5l-4 5',
'見る先': 'M12 3.5v3M12 17.5v3M3.5 12h3M17.5 12h3M12 8.2a3.8 3.8 0 1 0 0 7.6 3.8 3.8 0 0 0 0-7.6Z',
'スタイル': 'M4 20l3.4-.8L20 6.6 17.4 4 4.8 16.6 4 20Z',
- '体の色': 'M12 3.4s6 6.2 6 10.1a6 6 0 0 1-12 0C6 9.6 12 3.4 12 3.4Z',
'見た目の調整': 'M4 8h16M4 16h16M9.5 8h.01M15 16h.01',
'背景': 'M4 4.5h16a1.5 1.5 0 0 1 1.5 1.5v12a1.5 1.5 0 0 1-1.5 1.5H4A1.5 1.5 0 0 1 2.5 18V6A1.5 1.5 0 0 1 4 4.5ZM3 16l4-3.5 3.6 3 3.1-2.8 4.3 3.7M15.8 8.8h.01',
'舞台': 'M4 20V9l8-5 8 5v11M4 20h16M9.5 20v-5h5v5',
- 'セリフ(フキダシ)': 'M4 5h16v10H9l-4 4v-4H4V5Z',
'画面・光': 'M12 7.5a4.5 4.5 0 1 0 0 9 4.5 4.5 0 0 0 0-9ZM12 2.5v2M12 19.5v2M2.5 12h2M19.5 12h2M5.2 5.2l1.4 1.4M17.4 17.4l1.4 1.4M18.8 5.2l-1.4 1.4M6.6 17.4 5.2 18.8',
'画面の情報': 'M12 4a8 8 0 1 0 0 16 8 8 0 0 0 0-16ZM12 11v5M12 8h.01',
'画像': 'M4 5h16v14H4zM4 15l5-4 4 3 4-3.5 3 2.5M15.5 9h.01',
@@ -54,15 +46,6 @@ const SECTION_ICONS = {
const section = (parent, title, options) => uiSection(parent, title, { icon: SECTION_ICONS[title], ...options });
/** Fallbacks for the fields an older settings file may predate. */
-const BODY_COLOR_FALLBACKS = {
- body: '#c8b0f0', accent: '#8a4fe0', nose: '#7a4fb0',
- leaf: '#a6dd6a', vein: '#7fbf3f', feet: '#7a4fb0',
-};
-const CAPTION_SLIDER_FALLBACKS = {
- fontSize: 34, lineHeight: 1.42, padding: 18, radius: 24, borderWidth: 4,
-};
-const CAPTION_COLOR_FALLBACKS = { textColor: '#3f2b52', bubbleColor: '#ffffff', borderColor: '#55386e' };
-const MOUTH_FLAP_FALLBACKS = { rate: 1, mouthGain: 1 };
const LOOK_AT_FALLBACKS = { x: 0, y: 2.6, z: 3, amount: 1 };
/**
@@ -78,9 +61,8 @@ function confirmImageLicense() {
}
/**
- * The panel had no multi-line field, and the two features that take free prose
- * (the caption and the spoken text) want the same shape, so it is built once
- * here rather than growing the widget kit for a single caller.
+ * The panel's multi-line field. The 看板 prop's lettering is the only caller, but
+ * the shape (a control row wrapping a textarea) is worth keeping in one place.
*/
function textArea({ label, value = '', rows = 3, onChange }) {
const input = h('textarea', {
@@ -147,18 +129,6 @@ export function buildPanel(app, root) {
const sync = () => { for (const fn of syncers) fn(); };
const syncBones = () => { for (const fn of boneSyncers) fn(); };
- /**
- * Grey out (and disable) a control whose setting has no effect in the current
- * mode - e.g. the brow's angle while the brow is a loaded picture.
- */
- const setRowEnabled = (widget, on) => {
- const row = widget.el.closest?.('.row') ?? widget.el;
- const input = row.querySelector?.('input, select, button');
- if (input) input.disabled = !on;
- row.classList?.toggle('is-off', !on);
- };
-
- /** Patch one scope of the state without re-reading the whole UI. */
/** Replace the state and re-read every widget (used by presets and loads). */
const replace = (value, scope = 'all') => app.actions.applyState(value, { scope, sync: true });
@@ -190,55 +160,25 @@ export function buildPanel(app, root) {
tilt: Math.round(rnd(-12, 12)),
};
};
- const randomBeard = (shape) => {
- const base = BEARD_KIND_DEFAULTS[shape] ?? { size: 1, offsetY: 0, spacing: 0 };
- if (shape === 'off') return { shape };
- return {
- shape,
- ...base,
- size: +((base.size ?? 1) * rnd(0.8, 1.25)).toFixed(2),
- offsetY: Math.round((base.offsetY ?? 0) + rnd(-16, 16)),
- };
- };
- const randomHeadMark = () => {
- const shape = pick(['off', 'cap', 'fringe', 'fringe-up', 'curve']);
- return { shape, ...(HEAD_MARK_KIND_DEFAULTS[shape] ?? {}) };
- };
const randomFacePatch = () => {
- const beardShape = pick(['off', 'scotch', 'kaiser', 'cat']);
return {
eyes: {
left: randomEye(),
right: randomEye(),
- cheeks: { enabled: Math.random() < 0.5 },
// 眼鏡・サングラスも要素に含める(かける/かけない、各種設定)。
glasses: Math.random() < 0.5 ? { enabled: false } : randomGlasses(),
- beard: randomBeard(beardShape),
- headMark: randomHeadMark(),
- brow: {
- enabled: true,
- // Random brows are line drawings, never ゴル風's loaded picture.
- image: false,
- angle: Math.round(rnd(-30, 40)),
- height: Math.round(rnd(-10, 50)),
- length: +rnd(0.6, 2.6).toFixed(2),
- thickness: +rnd(1.2, 3.4).toFixed(2),
- curve: +rnd(0, 0.3).toFixed(2),
- spacing: Math.round(rnd(-120, 30)),
- taper: +rnd(0, 1).toFixed(2),
},
- },
- // The mouth takes the height and the tilt too, so the face really changes.
- mouth: {
- smile: +rnd(-1, 1.2).toFixed(2),
- // ときどき丸く開く口(O)にする。
- round: Math.random() < 0.3 ? +rnd(0.35, 1).toFixed(2) : 0,
- open: +rnd(0, 0.5).toFixed(2),
- width: +rnd(0.7, 1.3).toFixed(2),
- thickness: +rnd(0.7, 1.6).toFixed(2),
- tilt: Math.round(rnd(-12, 12)),
- offsetY: Math.round(rnd(-16, 16)),
- },
+ // The mouth takes the height and the tilt too, so the face really changes.
+ mouth: {
+ smile: +rnd(-1, 1.2).toFixed(2),
+ // ときどき丸く開く口(O)にする。
+ round: Math.random() < 0.3 ? +rnd(0.35, 1).toFixed(2) : 0,
+ open: +rnd(0, 0.5).toFixed(2),
+ width: +rnd(0.7, 1.3).toFixed(2),
+ thickness: +rnd(0.7, 1.6).toFixed(2),
+ tilt: Math.round(rnd(-12, 12)),
+ offsetY: Math.round(rnd(-16, 16)),
+ },
};
};
const randomFace = () => replace({ face: randomFacePatch() }, 'face');
@@ -249,10 +189,9 @@ export function buildPanel(app, root) {
return Math.abs(m[0]) <= 45 && Math.abs(m[1]) <= 40 && Math.abs(m[2]) <= 40;
});
const pose = pick(poses.length ? poses : POSE_PRESETS);
- const theme = pick(THEMES);
app.actions.applyPosePreset?.(pose.id);
- app.actions.applyTheme?.(theme.id);
- // The viewer style (実写/フラット/線画) and 左右反転 are deliberately left alone.
+ // The viewer style (実写/フラット/線画), the body colours, and 左右反転 are
+ // deliberately left alone.
replace({ face: randomFacePatch() }, 'face');
sync();
};
@@ -441,18 +380,16 @@ export function buildPanel(app, root) {
let faceSection = section(tabFace, 'プリセット');
// The face tab's sections are created here in the order they should read on
- // screen (プリセット -> 髪 -> 眉 -> 目 -> 見る先 -> ...), but filled further
- // down where the widgets are built: the DOM order is fixed at creation.
+ // screen (プリセット -> 目 -> 見る先 -> 口 -> 眼鏡 -> 鼻ちょうちん -> 帽子),
+ // but filled further down where the widgets are built: the DOM order is fixed
+ // at creation.
const faceTabSections = {
- hair: section(tabFace, '髪'),
- brow: section(tabFace, '眉'),
eye: section(tabFace, '目'),
look: section(tabFace, '見る先'),
+ mouth: section(tabFace, '口'),
glasses: section(tabFace, '眼鏡・サングラス'),
- cheeks: section(tabFace, 'ほっぺ'),
snot: section(tabFace, '鼻ちょうちん'),
- beard: section(tabFace, 'ひげ'),
- mouth: section(tabFace, '口'),
+ hat: section(tabFace, '帽子'),
};
faceSection.add(buttons({
@@ -860,128 +797,9 @@ export function buildPanel(app, root) {
const tearBox = details(faceSection.body, '横位置(左右べつ)');
for (const el of tearWidgets) tearBox.add(el);
- // --- eyebrows (the original character has none; this is extra range)
- faceSection = faceTabSections.brow;
- faceSection.add(hint('ぶるべーには眉がありません。怒った顔など、必要なときだけ出してください。'));
-
- const browToggle = check({
- label: '眉を表示',
- value: false,
- onChange: (value) => { state.face.eyes.brow.enabled = value; app.actions.refresh('face'); },
- });
- faceSection.add(controlRow(null, browToggle.el, { wide: true }));
-
- const browSliders = {
- angle: slider({
- label: '角度', min: -40, max: 40, step: 1, value: 0, format: (v) => `${Math.round(v)}°`,
- onInput: (v) => { state.face.eyes.brow.angle = v; app.actions.refresh('face'); },
- }),
- height: slider({
- label: '高さ', min: -40, max: 80, step: 1, value: 0, format: (v) => `${Math.round(v)}`,
- onInput: (v) => { state.face.eyes.brow.height = v; app.actions.refresh('face'); },
- }),
- length: slider({
- label: '長さ', min: 0, max: 4, step: 0.01, value: 1,
- format: (v) => (v < 0.02 ? '点' : v.toFixed(2)),
- onInput: (v) => { state.face.eyes.brow.length = v; app.actions.refresh('face'); },
- }),
- thickness: slider({
- label: '太さ', min: 0.4, max: 3.5, step: 0.01, value: 1, format: (v) => v.toFixed(2),
- onInput: (v) => { state.face.eyes.brow.thickness = v; app.actions.refresh('face'); },
- }),
- spacing: slider({
- label: '間隔', min: -220, max: 60, step: 1, value: 0, format: (v) => `${Math.round(v)}`,
- onInput: (v) => { state.face.eyes.brow.spacing = v; app.actions.refresh('face'); },
- }),
- curve: slider({
- label: '曲がり', min: 0, max: 0.4, step: 0.01, value: 0.14, format: (v) => v.toFixed(2),
- onInput: (v) => { state.face.eyes.brow.curve = v; app.actions.refresh('face'); },
- }),
- taper: slider({
- label: '鼻側の細さ', min: 0, max: 1, step: 0.01, value: 1,
- format: (v) => (v >= 0.995 ? 'そのまま' : (v <= 0.005 ? 'とがる' : v.toFixed(2))),
- onInput: (v) => { state.face.eyes.brow.taper = v; app.actions.refresh('face'); },
- }),
- };
- faceSection.add(hint('+の角度で内側が下がり、怒った感じになります。-で困り顔。'
- + '高さは + で上がり、- にすると目にかくれて見えなくなります。'));
- faceSection.add(browSliders.angle.el);
- const browBox = details(faceSection.body, 'こまかい設定(高さ・長さ・太さ・色)');
- browBox.add(browSliders.height.el);
- browBox.add(browSliders.length.el);
- browBox.add(browSliders.thickness.el);
- browBox.add(browSliders.spacing.el);
- browBox.add(browSliders.curve.el);
- browBox.add(browSliders.taper.el);
- const browColor = colorField({
- label: '眉の色', value: '#55386e',
- swatches: ['#55386e', '#2a1e33', '#7a5c2e', '#8a4a5e'],
- onChange: (v) => { state.face.eyes.brow.color = v; app.actions.refresh('face'); },
- });
- browBox.add(browColor.el);
- browBox.add(hint('「間隔」は左右の眉のへだたりです(+で広がり、-で内側に寄って、'
- + '大きく-にすると左右がくっつきます)。「長さ」を 0 にすると点になります。'
- + '「鼻側の細さ」を 0 にすると鼻側がとがります。'));
- browBox.add(buttons({
- items: [
- {
- id: 'golgo',
- label: 'ゴル風(画像)',
- onClick: () => {
- // ゴルゴ13: a very thick, almost straight black wedge, wide at the outer
- // end and tapering to a point at the nose, angled steeply down towards
- // it, with the two brows sitting close together.
- Object.assign(state.face.eyes.brow, {
- enabled: true,
- image: true,
- angle: 34,
- height: 33,
- length: 2.82,
- thickness: 3.6,
- curve: 0,
- taper: 0,
- spacing: -59,
- color: '#2a1e33',
- });
- app.actions.refresh('face');
- sync();
- },
- },
- {
- id: 'ryotsu',
- label: '両風(線の眉)',
- onClick: () => {
- // 両津勘吉: thick げじげじ brows that are joined in the middle, sitting
- // low over the eyes. `spacing` is what closes the gap - at this length
- // the two inner ends meet over the nose (see the panel's note). Turning
- // `image` off is what makes the line drawing take over from ゴル風's
- // loaded picture.
- Object.assign(state.face.eyes.brow, {
- enabled: true,
- image: false,
- angle: 10,
- height: 4,
- length: 2.86,
- thickness: 3.29,
- curve: 0.4,
- taper: 1,
- spacing: -50,
- color: '#181220',
- });
- app.actions.refresh('face');
- sync();
- },
- },
- ],
- }));
-
- faceSection.add(hint('「ゴル風(画像)」は読み込んだ画像(assets/brows/gol-right.png)で眉を描きます。'
- + '画像のときは角度・太さ・曲がり・鼻側の細さが効かないので、灰色になります。'
- + '「両風(線の眉)」など線で描く眉に切り替えると、この画像は使われません。'));
-
- // --- glasses / sunglasses (drawn into the same texture as the eyes and brows)
+ // --- glasses / sunglasses (drawn into the same texture as the eyes)
faceSection = faceTabSections.glasses;
- faceSection.add(hint('眼鏡やサングラスを、目や眉と同じ絵にかけます。'
+ faceSection.add(hint('眼鏡やサングラスを、目と同じ絵にかけます。'
+ '線画のときはレンズを塗らず、フレームの線だけになります。'));
// 眼鏡とサングラスの違いは、レンズの形だけではない。サングラスはレンズが不透明
@@ -1114,121 +932,6 @@ export function buildPanel(app, root) {
],
}));
- // --- ほっぺ (a manga blush)
- faceSection = faceTabSections.cheeks;
- faceSection.add(hint('ほっぺ(マンガの赤らみ)を、目や眉と同じ絵にかけます。'
- + '決まったキャラクターの絵ではなく、よくあるマンガの記号として用意しました。'
- + 'はじめは出ていません。'));
-
- const cheeksToggle = check({
- label: 'ほっぺを出す',
- value: false,
- onChange: (value) => { state.face.eyes.cheeks.enabled = value; app.actions.refresh('face'); },
- });
- faceSection.add(controlRow(null, cheeksToggle.el, { wide: true }));
-
- const cheekSliders = {
- size: slider({
- label: '大きさ', min: 0.4, max: 2.2, step: 0.01, value: 1, format: (v) => v.toFixed(2),
- onInput: (v) => { state.face.eyes.cheeks.size = v; app.actions.refresh('face'); },
- }),
- offsetY: slider({
- label: 'たかさ', min: -80, max: 120, step: 1, value: 0, format: (v) => `${Math.round(v)}`,
- onInput: (v) => { state.face.eyes.cheeks.offsetY = v; app.actions.refresh('face'); },
- }),
- spacing: slider({
- label: 'よこの間隔', min: -60, max: 80, step: 1, value: 0, format: (v) => `${Math.round(v)}`,
- onInput: (v) => { state.face.eyes.cheeks.spacing = v; app.actions.refresh('face'); },
- }),
- };
- faceSection.add(cheekSliders.size.el);
- const cheekBox = details(faceSection.body, 'こまかい設定(位置・色)');
- cheekBox.add(cheekSliders.offsetY.el);
- cheekBox.add(cheekSliders.spacing.el);
- const cheekColor = colorField({
- label: 'ほっぺの色', value: '#f6a6b8',
- swatches: ['#f6a6b8', '#f19ab0', '#f7b9a0', '#e88aa8'],
- onChange: (v) => { state.face.eyes.cheeks.color = v; app.actions.refresh('face'); },
- });
- cheekBox.add(cheekColor.el);
- cheekBox.add(hint('線はいつも4本です。「よこの間隔」は左右のほっぺのへだたり、'
- + '「たかさ」は目からの下がり具合です。'));
-
- // --- 髪 (a hair-like covering over the whole head; its own section, above 眉)
- faceSection = faceTabSections.hair;
- faceSection.add(hint('髪を頭にかぶせます。決まったキャラクターの髪ではなく、'
- + 'よくあるマンガの形として用意しました。はじめは出ていません。'));
-
- const headMarkShape = segmented({
- label: '髪',
- options: [
- { value: 'off', label: 'なし' },
- { value: 'cap', label: 'かぶせ' },
- { value: 'fringe', label: 'ぎざぎざ' },
- { value: 'fringe-up', label: 'はちわれ' },
- { value: 'curve', label: 'カーブ' },
- ],
- value: 'off',
- onChange: (value) => {
- const mark = state.face.eyes.headMark;
- mark.shape = value;
- // Entering a shape applies its own starting size (ぎざぎざ is smaller and
- // shallower than the shared defaults). Other shapes just keep the values.
- Object.assign(mark, HEAD_MARK_KIND_DEFAULTS[value] ?? {});
- app.actions.refresh('face');
- sync();
- },
- });
- faceSection.add(headMarkShape.el);
-
- const headMarkOverEyes = check({
- label: '髪を目の前に出す',
- value: true,
- onChange: (value) => { state.face.eyes.headMark.overEyes = value; app.actions.refresh('face'); },
- });
- faceSection.add(controlRow(null, headMarkOverEyes.el, { wide: true }));
-
- const headMarkSliders = {
- size: slider({
- 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'); },
- }),
- offsetY: slider({
- label: 'たかさ', min: -300, max: 300, step: 1, value: 0, format: (v) => `${Math.round(v)}`,
- onInput: (v) => { state.face.eyes.headMark.offsetY = v; app.actions.refresh('face'); },
- }),
- };
- 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);
- const headMarkColor = colorField({
- label: '髪の色', value: '#8a4fe0',
- swatches: ['#8a4fe0', '#2a1e33', '#e0344f', '#7a5c2e'],
- onChange: (v) => { state.face.eyes.headMark.color = v; app.actions.refresh('face'); },
- });
- const headMarkColor2 = colorField({
- label: '2色目', value: '#ffffff',
- swatches: ['#ffffff', '#f3ead9', '#ffe08a', '#7fd8ff'],
- onChange: (v) => { state.face.eyes.headMark.color2 = v; app.actions.refresh('face'); },
- });
- headMarkBox.add(headMarkColor.el);
- headMarkBox.add(headMarkColor2.el);
- headMarkBox.add(hint('「かぶせ」は頭にかぶせる丸い髪、「ぎざぎざ」は生え際がギザギザにとがった髪(「歯の深さ」でギザギザの深さを変えられます)、'
- + '「はちわれ」は中央が鋭く持ち上がった生え際、「カーブ」はドラえもんのように単純にカーブした髪です。'
- + '「たかさ」で生え際を目の中心あたりまで下げられます。'
- + '「髪を目の前に出す」を切ると、髪が目の後ろに回ります。'));
-
// --- 鼻ちょうちん: the snot bubble of a sleeping face.
faceSection = faceTabSections.snot;
const snotToggle = check({
@@ -1268,67 +971,6 @@ export function buildPanel(app, root) {
+ '「ねている」ポーズと組み合わせると、いねむりしているように見えます。'
+ '泡は鼻の横に出るので、位置は「よこの位置」「たかさ」で調整できます。'));
- // --- ひげ: mustaches and beards, hung under the nose.
- faceSection = faceTabSections.beard;
- const beardShape = segmented({
- label: 'ひげ',
- options: [
- { value: 'off', label: 'なし' },
- { value: 'scotch', label: 'ちょびひげ' },
- { value: 'kaiser', label: 'ダリ' },
- { value: 'cat', label: 'ねこ' },
- ],
- value: 'off',
- onChange: (value) => {
- const beard = state.face.eyes.beard;
- beard.shape = value;
- // Each kind starts at its own size and height (a textured kind is drawn at a
- // different scale from the built-in strokes). A kind with no entry resets to
- // the neutral values, so switching kinds cannot leave a stray size behind.
- Object.assign(beard, BEARD_KIND_DEFAULTS[value] ?? { size: 1, offsetY: 0, spacing: 0 });
- app.actions.refresh('face');
- sync();
- },
- });
- faceSection.add(beardShape.el);
- const beardSliders = {
- size: slider({
- label: '大きさ', min: 0.4, max: 2.4, step: 0.01, value: 1, format: (v) => v.toFixed(2),
- onInput: (v) => { state.face.eyes.beard.size = v; app.actions.refresh('face'); },
- }),
- spacing: slider({
- label: '左右の間隔', min: 0, max: 250, step: 1, value: 0, format: (v) => `${Math.round(v)}`,
- // Shown 10 higher than the value stored, so the default (stored -10) reads 0.
- onInput: (v) => { state.face.eyes.beard.spacing = v - 10; app.actions.refresh('face'); },
- }),
- offsetY: slider({
- label: 'たかさ', min: -160, max: 160, step: 1, value: 0, format: (v) => `${Math.round(v)}`,
- onInput: (v) => { state.face.eyes.beard.offsetY = v; app.actions.refresh('face'); },
- }),
- length: slider({
- label: 'ひげの長さ(ねこ)', min: 0.2, max: 3, step: 0.01, value: 1, format: (v) => v.toFixed(2),
- onInput: (v) => { state.face.eyes.beard.length = v; app.actions.refresh('face'); },
- }),
- lineGap: slider({
- label: '線の間隔(ねこ)', min: 0.2, max: 3, step: 0.01, value: 1, format: (v) => v.toFixed(2),
- onInput: (v) => { state.face.eyes.beard.lineGap = v; app.actions.refresh('face'); },
- }),
- };
- faceSection.add(beardSliders.size.el);
- const beardBox = details(faceSection.body, 'こまかい設定(位置・色)');
- beardBox.add(beardSliders.spacing.el);
- beardBox.add(beardSliders.offsetY.el);
- beardBox.add(beardSliders.length.el);
- beardBox.add(beardSliders.lineGap.el);
- const beardColor = colorField({
- label: 'ひげの色', value: '#1c1624',
- swatches: ['#1c1624', '#000000', '#3a2a4a', '#6b4a2a', '#8a8a94'],
- onChange: (v) => { state.face.eyes.beard.color = v; app.actions.refresh('face'); },
- });
- beardBox.add(beardColor.el);
- beardBox.add(hint('鼻の下に、ちょびひげ・ダリ・ねこ、の瓢を描きます。'
- + '「左右の間隔」「瓢の長さ」「線の間隔」は「ねこ」で使います。'));
-
// --- mouth
faceSection = faceTabSections.mouth;
@@ -1408,153 +1050,6 @@ export function buildPanel(app, root) {
const mouthColorBox = details(faceSection.body, '色(口の線・中・舌・口角)');
for (const widget of Object.values(mouthColors)) mouthColorBox.add(widget.el);
- /* ---------------------------------------------------------------- caption */
-
- const captionSection = section(tabScene, 'セリフ(フキダシ)');
-
- // Two bubbles share this one set of controls: the selector says which bubble
- // they are editing, and every handler below reads the active one when it runs,
- // so switching does not have to rewire anything.
- let activeCaption = 'caption';
- const cap = () => state[activeCaption];
- const captionBubbleSegment = segmented({
- label: 'どの吹き出し',
- options: [
- { value: 'caption', label: '①' },
- { value: 'caption2', label: '②' },
- { value: 'narration', label: 'ナレーション' },
- ],
- value: 'caption',
- onChange: (value) => { activeCaption = value; sync(); },
- });
- captionSection.add(captionBubbleSegment.el);
- captionSection.add(hint('吹き出しは①と②の2つ、ナレーション枠が1つ出せます。切り替えて、それぞれ別の位置・形・色・縦書きにできます。'));
-
- const captionToggle = check({
- label: 'この吹き出しを出す',
- value: false,
- onChange: (value) => { cap().enabled = value; app.actions.refresh('caption'); sync(); },
- });
- captionSection.add(controlRow(null, captionToggle.el, { wide: true }));
-
- const captionText = textArea({
- label: 'セリフ',
- value: '',
- rows: 3,
- onChange: (value) => { cap().text = value; app.actions.refresh('caption'); },
- });
- captionSection.add(captionText.el);
-
- const verticalToggle = check({
- label: '縦書きにする',
- value: false,
- onChange: (v) => { cap().vertical = v; app.actions.refresh('caption'); sync(); },
- });
- captionSection.add(controlRow(null, verticalToggle.el, { wide: true }));
-
- captionSection.add(buttons({
- label: '例',
- items: CAPTION_PRESETS.map((preset) => ({
- id: preset.id,
- label: preset.label,
- onClick: () => { Object.assign(cap(), preset.caption); app.actions.refresh('caption'); sync(); },
- })),
- }));
-
- const bubbleSegment = segmented({
- label: '形',
- options: BUBBLE_STYLES,
- value: 'round',
- onChange: (value) => { cap().bubble = value; app.actions.refresh('caption'); },
- });
- const tailSegment = tailPad({
- label: 'しっぽ',
- value: 'left',
- onChange: (value) => { cap().tail = value; app.actions.refresh('caption'); },
- });
- captionSection.add(bubbleSegment.el);
- captionSection.add(tailSegment.el);
-
- const captionSliders = {
- fontSize: slider({
- label: '文字の大きさ', min: 16, max: 72, step: 1, value: 34, format: (v) => `${Math.round(v)}px`,
- onInput: (v) => { cap().fontSize = v; app.actions.refresh('caption'); },
- }),
- lineHeight: slider({
- label: '行の高さ', min: 1, max: 2, step: 0.01, value: 1.42, format: (v) => v.toFixed(2),
- onInput: (v) => { cap().lineHeight = v; app.actions.refresh('caption'); },
- }),
- padding: slider({
- label: '内側の余白', min: 4, max: 40, step: 1, value: 18, format: (v) => `${Math.round(v)}px`,
- onInput: (v) => { cap().padding = v; app.actions.refresh('caption'); },
- }),
- radius: slider({
- label: '角の丸み', min: 0, max: 60, step: 1, value: 24, format: (v) => `${Math.round(v)}px`,
- onInput: (v) => { cap().radius = v; app.actions.refresh('caption'); },
- }),
- borderWidth: slider({
- label: '線の太さ', min: 0, max: 12, step: 0.5, value: 4, format: (v) => `${v.toFixed(1)}px`,
- onInput: (v) => { cap().borderWidth = v; app.actions.refresh('caption'); },
- }),
- };
- for (const widget of Object.values(captionSliders)) captionSection.add(widget.el);
-
- const captionPosition = {
- x: slider({
- label: '横位置', min: 0, max: 1, step: 0.01, value: 0.58, format: (v) => v.toFixed(2),
- onInput: (v) => { cap().x = v; app.actions.refresh('caption'); },
- }),
- y: slider({
- label: '縦位置', min: 0, max: 1, step: 0.01, value: 0.16, format: (v) => v.toFixed(2),
- onInput: (v) => { cap().y = v; app.actions.refresh('caption'); },
- }),
- };
- captionSection.add(captionPosition.x.el);
- captionSection.add(captionPosition.y.el);
-
- const captionMaxWidthSlider = slider({
- label: 'はば', min: 0.15, max: 0.8, step: 0.01, value: 0.36, format: (v) => v.toFixed(2),
- onInput: (v) => { cap().maxWidth = v; app.actions.refresh('caption'); },
- });
- captionSection.add(captionMaxWidthSlider.el);
-
- const alignSegment = segmented({
- label: 'そろえ',
- options: [
- { value: 'left', label: '左' },
- { value: 'center', label: '中' },
- { value: 'right', label: '右' },
- ],
- value: 'left',
- onChange: (value) => { cap().align = value; app.actions.refresh('caption'); },
- });
- captionSection.add(alignSegment.el);
-
- const boldToggle = check({
- label: '太字', value: false,
- onChange: (value) => { cap().bold = value; app.actions.refresh('caption'); },
- });
- const systemFontToggle = check({
- label: '標準フォントを使う', value: false,
- onChange: (value) => { cap().font = value ? 'system' : 'rounded'; app.actions.refresh('caption'); },
- });
- captionSection.add(controlRow(null, boldToggle.el, { wide: true }));
- captionSection.add(controlRow(null, systemFontToggle.el, { wide: true }));
-
- const captionColors = {
- textColor: colorField({ label: '文字の色', value: '#3f2b52', onChange: (v) => { cap().textColor = v; app.actions.refresh('caption'); } }),
- bubbleColor: colorField({ label: '吹き出しの色', value: '#ffffff', onChange: (v) => { cap().bubbleColor = v; app.actions.refresh('caption'); } }),
- borderColor: colorField({ label: '線の色', value: '#55386e', onChange: (v) => { cap().borderColor = v; app.actions.refresh('caption'); } }),
- };
- for (const widget of Object.values(captionColors)) captionSection.add(widget.el);
- captionSection.add(hint('セリフはプレビューと書き出し画像の両方に描きこまれます。位置は画像全体を0〜1とした割合です。'
- + 'ビューポートで吹き出しをドラッグしても動かせます。'));
-
- /* -------------------------------------------------------------- 口パク */
-
- // 口パク lives inside うごき on the pose tab (see below): talking is something
- // the character *does*, and it is usually recorded together with the motion.
-
/* ------------------------------------------------------ movement (pose tab) */
// This is the pose tab's うごき section; the widgets sit here because they are
@@ -1607,35 +1102,6 @@ export function buildPanel(app, root) {
moveSection.add(recordButton.el);
moveSection.add(hint('スペースキーで演出の開始・停止。録画はWebM(動画)として保存されます。'));
- // 口パク: the mouth moves as if talking, and no sound is made at all. It lives
- // in うごき because talking is motion - and it is usually recorded with it.
- // (The studio used to read the line with the device's own speech voices, but
- // those belong to the device, so a recording of them is not ours to hand out.)
- const flapSection = details(moveSection.body, '口パク(声は出ません)');
- const flapText = textArea({
- label: '口パクするセリフ',
- value: '',
- rows: 3,
- onChange: (value) => { state.mouthFlap.text = value; },
- });
- flapSection.add(flapText.el);
- flapSection.add(buttons({
- items: [{ id: 'flap', label: '口パク/止める', primary: true, onClick: () => app.actions.toggleMouthFlap() }],
- }));
- const flapSliders = {
- rate: slider({
- label: 'はやさ', min: 0.5, max: 2, step: 0.05, value: 1, format: (v) => `${v.toFixed(2)}倍`,
- onInput: (v) => { state.mouthFlap.rate = v; },
- }),
- mouthGain: slider({
- label: '口の開き', min: 0.2, max: 2, step: 0.01, value: 1, format: (v) => v.toFixed(2),
- onInput: (v) => { state.mouthFlap.mouthGain = v; },
- }),
- };
- for (const widget of Object.values(flapSliders)) flapSection.add(widget.el);
- flapSection.add(hint('音は出ません。セリフの長さぶん口が動いて、自動で止まります。'
- + '「シーン」タブの「セリフ(フキダシ)」の文とは別で、こちらは長さにだけ使います。'));
-
// --- 見る先 (moved here, next to the face controls, from the pose tab) ----
const lookAtSection = faceTabSections.look;
// `lookAt` moves the eyes and the face only, so the face has to be rebuilt too.
@@ -1675,7 +1141,7 @@ export function buildPanel(app, root) {
onChange: (value) => { state.lookAt.turnBody = value; refreshLookAt(); },
});
lookAtSection.add(controlRow(null, turnBodyToggle.el, { wide: true }));
- lookAtSection.add(hint('ビューポートを**クリック**すると、その場所を見るように目と髪が向きます(ドラッグと区別するため、動かさずに押して離してください)。数値でも同じ値を動かせます。'));
+ lookAtSection.add(hint('ビューポートを**クリック**すると、その場所を見るように目が向きます(ドラッグと区別するため、動かさずに押して離してください)。数値でも同じ値を動かせます。'));
/* ---------------------------------------------------------------- display */
@@ -1745,9 +1211,9 @@ export function buildPanel(app, root) {
}],
}));
- /* ------------------------------------------------------------- 帽子 */
+ /* ------------------------------------------------------------- 帽子 (顔タブ) */
- const hatSection = section(tabView, '帽子');
+ const hatSection = faceTabSections.hat;
hatSection.add(hint('ぶるべーの頭に帽子をかぶせます。「なし」を選ぶと外れます。'));
const hatSegment = segmented({
label: '帽子',
@@ -1817,46 +1283,15 @@ export function buildPanel(app, root) {
hatSliders.tiltZ.set(state.face.hat?.tiltZ ?? 0);
});
- /* ------------------------------------------------------------- body colour */
-
- const colourSection = section(tabView, '体の色');
- const themeSegment = segmented({
- label: 'テーマ',
- options: THEMES.map((theme) => ({ value: theme.id, label: theme.label })),
- value: 'original',
- onChange: (id) => { app.actions.applyTheme(id); sync(); },
- });
- colourSection.add(themeSegment.el);
- colourSection.add(hint('テーマは出発点です。選んだあとでパーツごとに色を変えられます。'));
-
- /** Write one part colour, creating `colors` if an old settings file lacked it. */
- const setBodyColor = (part, value) => {
- state.render.colors = state.render.colors ?? {};
- state.render.colors[part] = value;
- app.actions.refresh('render');
- };
- const bodyColors = {
- body: colorField({ label: '体', value: '#c8b0f0', onChange: (v) => setBodyColor('body', v) }),
- accent: colorField({ label: '手', value: '#8a4fe0', onChange: (v) => setBodyColor('accent', v) }),
- nose: colorField({ label: '鼻', value: '#7a4fb0', onChange: (v) => setBodyColor('nose', v) }),
- leaf: colorField({ label: '葉', value: '#a6dd6a', onChange: (v) => setBodyColor('leaf', v) }),
- vein: colorField({ label: '葉脈', value: '#7fbf3f', onChange: (v) => setBodyColor('vein', v) }),
- feet: colorField({ label: '足', value: '#7a4fb0', onChange: (v) => setBodyColor('feet', v) }),
- };
- for (const widget of Object.values(bodyColors)) colourSection.add(widget.el);
+ /* ---------------------------------------------------------- 環境の映りこみ */
+ // The body colours are fixed (see `render.colors` in presets.js), so the 体の色
+ // section is gone. The strength of the environment reflection is still a look
+ // setting, so it lives in 見た目の調整 below.
const envIntensitySlider = slider({
label: '環境の映りこみ', min: 0, max: 2, step: 0.05, value: 1, format: (v) => v.toFixed(2),
onInput: (v) => { state.render.envIntensity = v; app.actions.refresh('render'); },
});
- colourSection.add(envIntensitySlider.el);
- colourSection.add(buttons({
- items: [{
- id: 'resetColors',
- label: 'プリセットの色に戻す',
- onClick: () => { app.actions.applyTheme(state.render.theme ?? 'original'); sync(); },
- }],
- }));
/* -------------------------------------------------------------------- look */
@@ -1900,6 +1335,9 @@ export function buildPanel(app, root) {
lookSection.add(hint('影の向きは「ひかり」の光の向き(パッド)にしたがって変わります。'
+ '「接地影だけ」は足もとの丸い影だけにします。'
+ '「足もとの丸い影」は、足もとに薄く敷く丸い影です(はじめは出ていません)。'));
+ // 環境の映りこみ: the strength of the environment reflection, moved here from
+ // the old 体の色 section (the body colours themselves are fixed now).
+ lookSection.add(envIntensitySlider.el);
const environmentSegment = segmented({
label: '照明の種類',
@@ -2709,7 +2147,7 @@ export function buildPanel(app, root) {
comicSection.add(controlRow(null, storyPanelBox, { wide: true }));
let storyShape = '';
- /** Bring back everything a panel saved: pose, face, both bubbles and the camera. */
+ /** Bring back everything a panel saved: pose, face, 擬音 and the camera. */
const recallStoryPanel = (index) => {
const panel = state.story?.panels?.[index];
if (!panel) return;
@@ -2718,8 +2156,6 @@ export function buildPanel(app, root) {
app.actions.applyState({
pose: panel.pose ?? {},
face: panel.face ?? {},
- caption: panel.caption ?? {},
- caption2: panel.caption2 ?? {},
// The 擬音 on the shot. A panel saved before this existed clears them, so
// recalling it shows what it actually recorded rather than a leftover.
gion: panel.gion ?? { items: [] },
@@ -2734,7 +2170,7 @@ export function buildPanel(app, root) {
state.story.panels = (state.story.panels ?? []).filter((_, i) => i !== index);
// The empty patch is only here to route the edit through the app's own apply
// path: that is what records an undo step and re-reads the panel.
- app.actions.applyState({}, { scope: 'caption', sync: true });
+ app.actions.applyState({}, { scope: 'all', sync: true });
};
/**
@@ -2813,16 +2249,11 @@ export function buildPanel(app, root) {
items: [
{ id: 'facemap-eyes', label: '目の下地を書き出す', onClick: () => app.actions.saveFaceMap('eyes') },
{ id: 'facemap-mouth', label: '口の下地を書き出す', onClick: () => app.actions.saveFaceMap('mouth') },
- { id: 'facemap-hair', label: '髪の下地を書き出す', onClick: () => app.actions.saveFaceMap('hair') },
],
}));
faceMapSection.add(hint('書き出したPNGは、スタジオが読み込む大きさ・位置そのものです。'
- + 'そのまま絵を描いて(たとえば目にゴルゴ風の眉を描く)「目の画像を読み込む(PNG)」や'
- + '「口の画像を読み込む(PNG)」で読み込むと、描いた場所にそのまま入ります。'
- + '青い線は位置合わせの目安です。使う前に消すか、上から塗ってください。'
- + '「髪の下地」は、頭をぐるりと巻いたテクスチャを開いたもので、横が頭の角度、'
- + 'たてが高さです。左右の端が頭の後ろ(合わせ目)、青いたて線が顔の正面、'
- + '青いよこ線が既定の生え際です。'));
+ + 'そのまま絵を描いて「目の画像を読み込む(PNG)」や「口の画像を読み込む(PNG)」で読み込むと、'
+ + '描いた場所にそのまま入ります。青い線は位置合わせの目安です。使う前に消すか、上から塗ってください。'));
/* --------------------------------------------------------------- settings */
@@ -2859,13 +2290,12 @@ export function buildPanel(app, root) {
infoSection.add(statsLine);
infoSection.add(hint('動きが重い環境では、負荷を下げるために画質(描画バッファの倍率)を自動で下げます。'));
- // シーン: the scene controls are built wherever they belong in the code (the
- // speech bubble sits with the face controls), so put them on the tab in the
- // order the user meets them: what is behind the character, what is beside it,
- // the speech bubble, then the light and the read-out.
+ // シーン: the scene controls are built wherever they belong in the code, so put
+ // them on the tab in the order the user meets them: what is behind the
+ // character, what is beside it, then the light and the read-out.
tabScene.append(
backdropSection.el, propSection.el, gionSection.el, wallSection.el,
- captionSection.el, screenLightSection.el, infoSection.el,
+ screenLightSection.el, infoSection.el,
);
boneSyncers.push(() => {
@@ -2896,21 +2326,6 @@ export function buildPanel(app, root) {
w.eyeX.set(eye.eyeX ?? 0);
}
- browToggle.set(state.face.eyes.brow.enabled);
- browSliders.angle.set(state.face.eyes.brow.angle);
- browSliders.height.set(state.face.eyes.brow.height);
- browSliders.length.set(state.face.eyes.brow.length);
- browSliders.thickness.set(state.face.eyes.brow.thickness);
- browSliders.spacing.set(state.face.eyes.brow.spacing ?? 0);
- browSliders.curve.set(state.face.eyes.brow.curve);
- browSliders.taper.set(state.face.eyes.brow.taper ?? 1);
- // A loaded ゴル風 picture ignores the shape sliders, so grey those out rather
- // than leave them looking like they should do something.
- const browIsImage = state.face.eyes.brow.image === true;
- for (const key of ['angle', 'thickness', 'curve', 'taper']) {
- setRowEnabled(browSliders[key], !browIsImage);
- }
- browColor.set(state.face.eyes.brow.color);
glassesToggle.set(state.face.eyes.glasses.enabled === true);
glassesKindSegment.set(state.face.eyes.glasses.kind ?? 'glasses');
for (const [key, widget] of Object.entries(glassesSliders)) widget.set(state.face.eyes.glasses[key]);
@@ -2922,24 +2337,10 @@ export function buildPanel(app, root) {
});
syncers.push(() => {
- cheeksToggle.set(state.face.eyes.cheeks.enabled === true);
- 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);
- headMarkColor2.set(state.face.eyes.headMark.color2 ?? '#ffffff');
snotToggle.set(state.face.eyes.snot.enabled === true);
snotSize.set(state.face.eyes.snot.size);
for (const [key, widget] of Object.entries(snotSliders)) widget.set(state.face.eyes.snot[key]);
snotColor.set(state.face.eyes.snot.color);
- beardShape.set(state.face.eyes.beard.shape ?? 'off');
- for (const [key, widget] of Object.entries(beardSliders)) widget.set(state.face.eyes.beard[key]);
- // 「左右の間隔」は内部値より+10して表示する(既定の -10 が 0 に見える)。
- beardSliders.spacing.set((state.face.eyes.beard.spacing ?? 0) + 10);
- beardColor.set(state.face.eyes.beard.color);
});
syncers.push(() => {
@@ -2996,10 +2397,6 @@ export function buildPanel(app, root) {
});
syncers.push(() => {
- themeSegment.set(state.render?.theme ?? 'original');
- for (const [key, widget] of Object.entries(bodyColors)) {
- widget.set(state.render?.colors?.[key] ?? BODY_COLOR_FALLBACKS[key]);
- }
envIntensitySlider.set(state.render?.envIntensity ?? 1);
});
@@ -3073,35 +2470,6 @@ export function buildPanel(app, root) {
});
syncers.push(() => {
- captionBubbleSegment.set(activeCaption);
- const bubble = cap() ?? {};
- captionToggle.set(bubble.enabled === true);
- captionText.set(bubble.text ?? '');
- verticalToggle.set(bubble.vertical === true);
- bubbleSegment.set(bubble.bubble ?? 'round');
- tailSegment.set(bubble.tail ?? 'left');
- for (const [key, widget] of Object.entries(captionSliders)) {
- widget.set(bubble[key] ?? CAPTION_SLIDER_FALLBACKS[key]);
- }
- captionPosition.x.set(bubble.x ?? 0.58);
- captionPosition.y.set(bubble.y ?? 0.16);
- captionMaxWidthSlider.set(bubble.maxWidth ?? 0.36);
- alignSegment.set(bubble.align ?? 'left');
- boldToggle.set(bubble.bold === true);
- systemFontToggle.set(bubble.font === 'system');
- for (const [key, widget] of Object.entries(captionColors)) {
- widget.set(bubble[key] ?? CAPTION_COLOR_FALLBACKS[key]);
- }
- });
-
- syncers.push(() => {
- flapText.set(state.mouthFlap?.text ?? '');
- for (const [key, widget] of Object.entries(flapSliders)) {
- widget.set(state.mouthFlap?.[key] ?? MOUTH_FLAP_FALLBACKS[key]);
- }
- });
-
- syncers.push(() => {
storyScaleSegment.set(String(state.story?.scale ?? 1.5));
storyColumnsSlider.set(state.story?.columns ?? 1);
storyGapSlider.set(state.story?.gap ?? 12);
@@ -3121,12 +2489,6 @@ export function buildPanel(app, root) {
return {
sync,
onRigChanged: syncBones,
- /** Which bubble the caption controls are editing (a drag on it calls this). */
- selectCaption(key) {
- if (key !== 'caption' && key !== 'caption2' && key !== 'narration') return;
- activeCaption = key;
- sync();
- },
/** Which 擬音 stamp the viewport should outline (a drag on it calls this). */
selectGion(id) {
app.gionSelected = id ?? null;