aboutsummaryrefslogtreecommitdiffhomepage
path: root/public/bluebey-studio/src/main.js
diff options
context:
space:
mode:
Diffstat (limited to 'public/bluebey-studio/src/main.js')
-rw-r--r--public/bluebey-studio/src/main.js53
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}コマ)`);
}