diff options
| author | Yasutake Yohei <61961825+yasutakeyohei@users.noreply.github.com> | 2026-10-03 14:27:24 +0900 |
|---|---|---|
| committer | Yasutake Yohei <61961825+yasutakeyohei@users.noreply.github.com> | 2026-10-03 14:27:24 +0900 |
| commit | a1337fab526a9babd5c492537bd7aee4cc5e7d26 (patch) | |
| tree | 57c6df805fe9824daa51de8db06d633e9bd1de3a /public/bluebey-studio/src/panel.js | |
| parent | 7892cc6f46cdfcd6dfbbdc94d34cbf0c68f29fba (diff) | |
public/bluebey-studio: 立体の鼻ちょうちん・仰向けポーズ・ひげなどを追加
- 鼻ちょうちんをテクスチャ描画から3Dオブジェクトに変更。鼻の下に泡を
出し、master ボーンに親子付けて体の傾きに追従する
- 寝ているポーズを仰向けにし、背中が地面につく高さに調整
- ポーズを変えたら「てをふる」の動きを「とまる」に戻す
- スマホでメニュー表示中に画面外をタップするとメニューを閉じる
- 眉の長さを、左右がつながるくらいまで伸ばせるように
- 眼鏡・サングラスの「種類」を選ぶと「眼鏡をかける」を自動でオンに
- マウス操作からボーン選択を外し、ドラッグで移動・Shift+ドラッグで
向き変更にした(ボーンの操作はメニューの一覧から)
- 頭の模様: 大きさをより小さくでき、ツートンを廃止、「わけめ」に分け目
を入れ、「まえがみ(逆)」(上に凸)を追加
- ひげ(ちょびひげ・カイゼル・エプロン・ねこ)を追加
Diffstat (limited to 'public/bluebey-studio/src/panel.js')
| -rw-r--r-- | public/bluebey-studio/src/panel.js | 69 |
1 files changed, 64 insertions, 5 deletions
diff --git a/public/bluebey-studio/src/panel.js b/public/bluebey-studio/src/panel.js index bad4059..c26c88c 100644 --- a/public/bluebey-studio/src/panel.js +++ b/public/bluebey-studio/src/panel.js @@ -201,7 +201,8 @@ export function buildPanel(app, root) { app.needsRender = true; } - poseSection.add(hint('モデルをクリックすると、その場所にいちばん近いボーンが選ばれます。スライダーは「休めの姿勢からの差」です。')); + poseSection.add(hint('ボーンは上の一覧から選びます。スライダーは「休めの姿勢からの差」です。' + + 'ビューポートのぶるべーは、ドラッグで移動、Shift+ドラッグで向きを変えられます。')); // こまかい設定: the whole-body nudge and the rotation gizmo are tweaks, not the // main way in, so they stay folded away inside the bone section. @@ -246,10 +247,19 @@ export function buildPanel(app, root) { ? POSE_PRESETS.find((item) => item.id === presetOrId) : presetOrId; if (!preset) return; + // Some poses already raise the arms; leaving the てをふる motion running would + // add to the arm the pose just set and fold the flipper over the face, so + // switching to any pose stops that motion. + if (state.anim.mode === 'wave') { + state.anim.mode = 'off'; + app.animator.reset(); + app.needsRender = true; + } state.pose = { bones: {}, root: [0, 0, 0] }; applyPatch(state.pose, preset.pose ?? {}); if (!state.pose.root) state.pose.root = [0, 0, 0]; replace({}, 'pose'); + sync(); } function randomPose() { @@ -622,7 +632,7 @@ export function buildPanel(app, root) { onInput: (v) => { state.face.eyes.brow.height = v; app.actions.refresh('face'); }, }), length: slider({ - label: '長さ', min: 0, max: 1.6, step: 0.01, value: 1, + 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'); }, }), @@ -763,6 +773,10 @@ export function buildPanel(app, root) { value: 'glasses', onChange: (value) => { setGlassesKind(value); + // Picking a kind means you mean to wear it, so put the glasses on rather + // than leaving the checkbox off and nothing showing on the face. + state.face.eyes.glasses.enabled = true; + glassesToggle.set(true); app.actions.refresh('face'); sync(); }, @@ -884,9 +898,9 @@ export function buildPanel(app, root) { { value: 'off', label: 'なし' }, { value: 'cap', label: 'かぶせ' }, { value: 'fringe', label: 'まえがみ' }, + { value: 'fringe-up', label: 'まえがみ(逆)' }, { value: 'wave', label: 'なみ' }, { value: 'side', label: 'わけめ' }, - { value: 'split', label: 'ツートン' }, ], value: 'off', onChange: (value) => { state.face.eyes.headMark.shape = value; app.actions.refresh('face'); }, @@ -895,7 +909,7 @@ export function buildPanel(app, root) { const headMarkSliders = { size: slider({ - label: '大きさ', min: 0.4, max: 2.2, step: 0.01, value: 1, format: (v) => v.toFixed(2), + 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'); }, }), offsetX: slider({ @@ -924,7 +938,8 @@ export function buildPanel(app, root) { headMarkBox.add(headMarkColor.el); headMarkBox.add(headMarkColor2.el); headMarkBox.add(hint('「頭の模様」は、頭にかぶせる髪のような塗りです。' - + '「ツートン」は頭を左右2色に分けます(2色目を使います)。' + + '「まえがみ(逆)」は、まえがみを上下さかさまにした形です。' + + '「わけめ」は、髪を分けた筋が入ります。' + '決まったキャラクターの髪ではなく、よくあるマンガの記号として用意しました。')); // --- 鼻ちょうちん: the snot bubble of a sleeping face. @@ -962,6 +977,47 @@ export function buildPanel(app, root) { + '「ねている」ポーズと組み合わせると、いねむりしているように見えます。' + '泡は鼻の横に出るので、位置は「よこの位置」「たかさ」で調整できます。')); + // --- ひげ: mustaches and beards, hung under the nose. + faceSection = section(tabFace, 'ひげ'); + const beardShape = segmented({ + label: 'ひげ', + options: [ + { value: 'off', label: 'なし' }, + { value: 'scotch', label: 'ちょびひげ' }, + { value: 'kaiser', label: 'カイゼル' }, + { value: 'apron', label: 'エプロン' }, + { value: 'cat', label: 'ねこ' }, + ], + value: 'off', + onChange: (value) => { state.face.eyes.beard.shape = value; app.actions.refresh('face'); }, + }); + 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'); }, + }), + offsetX: slider({ + label: 'よこの位置', min: -200, max: 200, step: 1, value: 0, format: (v) => `${Math.round(v)}`, + onInput: (v) => { state.face.eyes.beard.offsetX = v; 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'); }, + }), + }; + faceSection.add(beardSliders.size.el); + const beardBox = details(faceSection.body, 'こまかい設定(位置・色)'); + beardBox.add(beardSliders.offsetX.el); + beardBox.add(beardSliders.offsetY.el); + const beardColor = colorField({ + label: 'ひげの色', value: '#3a2a4a', + swatches: ['#3a2a4a', '#1c1624', '#6b4a2a', '#8a8a94'], + onChange: (v) => { state.face.eyes.beard.color = v; app.actions.refresh('face'); }, + }); + beardBox.add(beardColor.el); + beardBox.add(hint('鼻の下に、ちょびひげ・カイゼル・エプロン・ねこ、の髭を描きます。')); + // --- mouth faceSection = section(tabFace, '口'); @@ -2394,6 +2450,9 @@ export function buildPanel(app, root) { 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]); + beardColor.set(state.face.eyes.beard.color); }); syncers.push(() => { |
