diff options
| author | Yasutake Yohei <61961825+yasutakeyohei@users.noreply.github.com> | 2026-10-05 22:24:22 +0900 |
|---|---|---|
| committer | Yasutake Yohei <61961825+yasutakeyohei@users.noreply.github.com> | 2026-10-05 22:24:22 +0900 |
| commit | 63c1a9dfa8b04ab9725a9a49ef34cd3fc6ade56c (patch) | |
| tree | 9a5b0970585a927bb37db09d23ba7bb1fc4190be /public/bluebey-studio/src/main.js | |
| parent | 39a6c31beed18f0d1fc211fda919fae7ce2d9505 (diff) | |
bluebey-studio: 小物の3軸回転・線画対応と、まんがのコマ枠・共有リンク短縮
小物にX/Y/Zの3軸回転とフラット/線画スタイル対応を追加し、鉛筆の芯を円錐に埋め、ノートと消しゴムを追加。まんがは各コマに枠を描きzip出力を廃止。共有リンクは既定値の省略で約2,429→321文字に短縮。縦書きは句読点のぶら下げと長音・小書き文字の位置を補正。線画の帽子にトーンを付け、removeMeshでマテリアルを復元して切替時の破損を修正。
Diffstat (limited to 'public/bluebey-studio/src/main.js')
| -rw-r--r-- | public/bluebey-studio/src/main.js | 53 |
1 files changed, 37 insertions, 16 deletions
diff --git a/public/bluebey-studio/src/main.js b/public/bluebey-studio/src/main.js index cecdce5..23ed017 100644 --- a/public/bluebey-studio/src/main.js +++ b/public/bluebey-studio/src/main.js @@ -21,7 +21,6 @@ import { buildProp, PROP_DEFAULTS, disposeProp, applyPropText } from './props.js import { rollAll, decodeSeed } from './gacha.js'; import { encodeState, decodeState } from './urlState.js'; import { createMouthFlap, levelToMouth } from './mouthFlap.js'; -import { createZip } from './zip.js'; import { createScreenOutline, BEHIND_LABEL, SOLID_LABEL, LEAF_LABEL, NOSE_LABEL } from './outline.js'; import { gionSheetUrl, stampRect, imageAspect, imageReady, drawGion, DEFAULT_STAMP_WIDTH } from './gion.js'; @@ -522,9 +521,9 @@ async function init() { // Hats are built at runtime, so they are not in the GLB's mesh list. Register // them with the style system (and the current hat) so they follow flat/line art // and get an outline hull like every other part. - hatStyleRegister = (mesh) => styles.addMesh(mesh); + hatStyleRegister = (mesh) => styles.addMesh(mesh, { tone: true }); hatStyleUnregister = (mesh) => styles.removeMesh(mesh); - for (const mesh of hatMeshes) styles.addMesh(mesh); + for (const mesh of hatMeshes) styles.addMesh(mesh, { tone: true }); const face = new Face({ eyeMesh: model.parts.eyeMesh, mouthMesh: model.parts.mouthMesh, @@ -951,11 +950,21 @@ async function init() { if (signature === propSignature) return; propSignature = signature; for (const child of [...propRoot.children]) { + // Hand every mesh back to the style system before the prop is disposed. + // `removeMesh` restores each mesh's own material (a prop under 線画 is + // wearing the shared paper material, which must not be disposed). Collect + // the meshes first: `removeMesh` deletes the outline hull, a child of the + // prop group. + const meshes = []; + child.traverse((object) => { + if (styles.originals.has(object)) meshes.push(object); + }); + for (const mesh of meshes) styles.removeMesh(mesh); propRoot.remove(child); disposeProp(child); } for (const item of items) { - const def = PROP_DEFAULTS[item.kind] ?? { x: 0, y: 0, z: 0, rotY: 0, scale: 1 }; + const def = PROP_DEFAULTS[item.kind] ?? { x: 0, y: 0, z: 0, rotX: 0, rotY: 0, rotZ: 0, scale: 1 }; let group; try { group = buildProp(item.kind, { colors: state.render.colors, scale: item.scale ?? def.scale ?? 1 }); @@ -964,12 +973,22 @@ async function init() { continue; } group.position.set(item.x ?? def.x ?? 0, item.y ?? def.y ?? 0, item.z ?? def.z ?? 0); - group.rotation.y = ((item.rotY ?? def.rotY ?? 0) * DEG); + group.rotation.set( + (item.rotX ?? def.rotX ?? 0) * DEG, + (item.rotY ?? def.rotY ?? 0) * DEG, + (item.rotZ ?? def.rotZ ?? 0) * DEG, + ); propRoot.add(group); + // Register with the style system, so フラット and 線画 reach the props too + // and each part gets an outline hull like the body and the hats. + group.traverse((object) => { + if (object.isMesh) styles.addMesh(object); + }); // 看板 only: paint the saved text onto the freshly built face (no-op for // every other prop, which has no writing surface). applyPropText(group, item.text); } + refreshOutlineExclusion?.(); app.needsRender = true; } @@ -2376,13 +2395,15 @@ async function init() { } function addProp(kind) { - const def = PROP_DEFAULTS[kind] ?? { x: 1.6, y: 0, z: 0, rotY: 0, scale: 1 }; + const def = PROP_DEFAULTS[kind] ?? { x: 1.6, y: 0, z: 0, rotX: 0, rotY: 0, rotZ: 0, scale: 1 }; state.props.items = [...(state.props.items ?? []), { kind, x: def.x ?? 0, y: def.y ?? 0, z: def.z ?? 0, + rotX: def.rotX ?? 0, rotY: def.rotY ?? 0, + rotZ: def.rotZ ?? 0, scale: def.scale ?? 1, }]; applyState({}, { scope: 'render', sync: true }); @@ -2464,10 +2485,9 @@ async function init() { } /** - * まんが: replay the panels, capture each one, and hand back a sheet of them - * plus a zip of the individual frames. + * まんが: replay the panels, capture each one, and hand back a framed sheet. */ - async function saveStory({ scale = 2, includeZip = true } = {}) { + async function saveStory({ scale = 2 } = {}) { const panels = state.story?.panels ?? []; if (!panels.length) { toast('コマがありません。「今の状態をコマに追加」で作ってください'); @@ -2509,6 +2529,11 @@ async function init() { const numbered = columns >= 2 && captures.length >= 3; if (numbered) await ensureCaptionFont(); + // A manga page reads by its panel frames. They are drawn inside each cell so + // the artwork keeps its full size, and are thick enough to survive the 1× + // export. + const frame = Math.max(3, Math.round(Math.min(cellW, cellH) * 0.012)); + const sheet = document.createElement('canvas'); sheet.width = padding * 2 + cellW * columns + gap * (columns - 1); sheet.height = padding * 2 + cellH * rows + gap * (rows - 1); @@ -2519,18 +2544,14 @@ async function init() { const x = padding + (index % columns) * (cellW + gap); const y = padding + Math.floor(index / columns) * (cellH + gap); ctx.drawImage(capture.canvas, x, y, cellW, cellH); + ctx.lineWidth = frame; + ctx.strokeStyle = '#2a1e33'; + ctx.strokeRect(x + frame / 2, y + frame / 2, cellW - frame, cellH - frame); if (numbered) drawPanelNumber(ctx, index + 1, x, y, cellW, cellH); }); const stamp = exporter.timestamp(); exporter.downloadBlob(await exporter.canvasToBlob(sheet), `bluebey-comic-${stamp}.png`); - if (includeZip) { - const files = await Promise.all(captures.map(async (capture, index) => ({ - name: `panel-${String(index + 1).padStart(2, '0')}.png`, - data: new Uint8Array(await capture.blob.arrayBuffer()), - }))); - exporter.downloadBlob(createZip(files), `bluebey-comic-${stamp}.zip`); - } toast(`まんがを書き出しました(${captures.length}コマ)`); } |
