aboutsummaryrefslogtreecommitdiffhomepage
path: root/public/bluebey-studio/src/main.js
diff options
context:
space:
mode:
authorYasutake Yohei <61961825+yasutakeyohei@users.noreply.github.com>2026-10-05 00:08:30 +0900
committerYasutake Yohei <61961825+yasutakeyohei@users.noreply.github.com>2026-10-05 00:08:30 +0900
commit629dfd09777d1abb3cdecd8fc58d18234d1063cf (patch)
tree6f0f01befc1a4283d716a24187e0fc570a7a905e /public/bluebey-studio/src/main.js
parent793bfd720525f7f1f14a507e1c962101db5ef283 (diff)
bluebey-studio: 帽子の高さ・傾き、髪の下地書き出し、目の形メニューの再構成
Diffstat (limited to 'public/bluebey-studio/src/main.js')
-rw-r--r--public/bluebey-studio/src/main.js67
1 files changed, 48 insertions, 19 deletions
diff --git a/public/bluebey-studio/src/main.js b/public/bluebey-studio/src/main.js
index 01ee85a..d4866c7 100644
--- a/public/bluebey-studio/src/main.js
+++ b/public/bluebey-studio/src/main.js
@@ -381,30 +381,50 @@ async function init() {
let hatKindCurrent = null;
let hatObject = null;
const hatMeshes = [];
+ /** Last transform applied, so the render loop only wakes when it changes. */
+ const hatXform = { height: null, tiltX: null, tiltZ: null };
/** Set once the outline pass exists, so a hat change can refresh the exclude set. */
let refreshOutlineExclusion = null;
function applyHat(kind) {
- if (kind === hatKindCurrent) return;
- hatKindCurrent = kind;
if (!hatMount) return;
+ if (kind !== hatKindCurrent) {
+ hatKindCurrent = kind;
+ if (hatObject) {
+ hatMount.remove(hatObject);
+ hatObject.traverse((o) => {
+ o.geometry?.dispose();
+ if (Array.isArray(o.material)) o.material.forEach((m) => m.dispose());
+ else o.material?.dispose();
+ });
+ hatObject = null;
+ }
+ hatMeshes.length = 0;
+ const built = buildHat(kind);
+ if (built) {
+ hatMount.add(built);
+ hatObject = built;
+ built.traverse((o) => { if (o.isMesh) hatMeshes.push(o); });
+ }
+ refreshOutlineExclusion?.();
+ }
+
+ // 高さ and 傾き. The mount is world-aligned (see above), so +y is up, +z is the
+ // character's front and +x its right: a plain translation and rotation read
+ // the way the sliders say.
+ const hat = state.face.hat ?? {};
+ const height = hat.height ?? 0;
+ const tiltX = hat.tiltX ?? 0;
+ const tiltZ = hat.tiltZ ?? 0;
if (hatObject) {
- hatMount.remove(hatObject);
- hatObject.traverse((o) => {
- o.geometry?.dispose();
- if (Array.isArray(o.material)) o.material.forEach((m) => m.dispose());
- else o.material?.dispose();
- });
- hatObject = null;
+ hatObject.position.y = height;
+ hatObject.rotation.set((tiltX * Math.PI) / 180, 0, (tiltZ * Math.PI) / 180);
}
- hatMeshes.length = 0;
- const built = buildHat(kind);
- if (built) {
- hatMount.add(built);
- hatObject = built;
- built.traverse((o) => { if (o.isMesh) hatMeshes.push(o); });
+ if (height !== hatXform.height || tiltX !== hatXform.tiltX || tiltZ !== hatXform.tiltZ) {
+ hatXform.height = height;
+ hatXform.tiltX = tiltX;
+ hatXform.tiltZ = tiltZ;
+ app.needsRender = true;
}
- refreshOutlineExclusion?.();
- app.needsRender = true;
}
applyHat(state.face.hat?.kind ?? 'none');
@@ -1932,14 +1952,23 @@ async function init() {
// `face.flush(now, 26)`), so force any pending one through first or the map
// would show the expression before last.
face.flush(performance.now(), 0);
- return exporter.buildFaceMap(face, kind);
+ return kind === 'hair' ? exporter.buildHairMap(face) : exporter.buildFaceMap(face, kind);
}
async function saveFaceMap(kind) {
const canvas = faceMapCanvas(kind);
+ if (!canvas) {
+ toast('このモデルには髪のプレートがありません');
+ return;
+ }
const blob = await exporter.canvasToBlob(canvas);
- const label = kind === 'eyes' ? '目' : '口';
+ const label = kind === 'eyes' ? '目' : kind === 'mouth' ? '口' : '髪';
exporter.downloadBlob(blob, `bluebey-face-${kind}-${exporter.timestamp()}.png`);
+ if (kind === 'hair') {
+ toast(`${label}の下地を書き出しました(${canvas.width}×${canvas.height})。`
+ + 'この画像に前髪を描いて渡してください');
+ return;
+ }
toast(`${label}の下地を書き出しました(${canvas.width}×${canvas.height})。`
+ `この画像に描いて「${label}の画像を読み込む(PNG)」で読み込めます`);
}