aboutsummaryrefslogtreecommitdiffhomepage
path: root/public/bluebey-studio/src/props.js
diff options
context:
space:
mode:
authorYasutake Yohei <61961825+yasutakeyohei@users.noreply.github.com>2026-10-03 15:44:38 +0900
committerYasutake Yohei <61961825+yasutakeyohei@users.noreply.github.com>2026-10-03 15:44:38 +0900
commitfbaf2cc671fed23b555e218499301af0f50327b0 (patch)
treedbdbf5bca92004085feebec920019800472672e5 /public/bluebey-studio/src/props.js
parent4cf9c37ecd568dab470b5714bff4ff9f860a5377 (diff)
public/bluebey-studio: 移動範囲と影・壁の追従、看板の文字
移動スライダーの範囲を広げ、キーライトと影カメラがぶるべーに追従するようにした(遠くへ動かしても影が切れない)。地面も広げた。 足もとの丸い影は既定で非表示にし、「足もとの丸い影」で切り替えられるようにした。原点に取り残されないよう、ぶるべーに追従させる。 見えない壁は体の向き(master のY回転と見る先の体向き)にも追従するようにした。 「舞台」をやめ、「小物」「擬音」「見えない壁」をそれぞれ独立したメニューにした。看板に文字を書けるようにした(複数行可)。
Diffstat (limited to 'public/bluebey-studio/src/props.js')
-rw-r--r--public/bluebey-studio/src/props.js63
1 files changed, 62 insertions, 1 deletions
diff --git a/public/bluebey-studio/src/props.js b/public/bluebey-studio/src/props.js
index a5fcbcc..ba682e4 100644
--- a/public/bluebey-studio/src/props.js
+++ b/public/bluebey-studio/src/props.js
@@ -344,7 +344,7 @@ export const PROP_DEFAULTS = {
// `faceScale` only means something for a 看板: it grows the writing area (the
// face and its frame) together, so the post and base stay put. The panel owns
// that control.
- sign: { x: 2.9, y: 0, z: 0.4, rotY: -0.25, scale: 1, faceScale: 1 },
+ sign: { x: 2.9, y: 0, z: 0.4, rotY: -0.25, scale: 1, faceScale: 1, text: '' },
plant: { x: -2.0, y: 0, z: -1.8, rotY: 0.25, scale: 1 },
box: { x: 2.0, y: 0, z: -1.7, rotY: 0.55, scale: 1 },
can: { x: 1.7, y: 0, z: 1.5, rotY: -0.5, scale: 1 },
@@ -457,6 +457,67 @@ export function applyPropFaceScale(group, faceScale) {
target.scale.setScalar(scale);
}
+/**
+ * Paint text onto a prop's writing surface (the 看板's face), or clear it.
+ *
+ * The face's material gets a `CanvasTexture`, drawn here with a system font stack
+ * (no font file is needed, and Japanese falls back to whatever the device has).
+ * Long lines and long blocks are shrunk to fit the board, and explicit newlines
+ * are honoured. The material is per-prop (see `materialCache`), so the map never
+ * leaks to another sign.
+ */
+export function applyPropText(group, text) {
+ if (typeof document === 'undefined') return; // Node (tests): no canvas
+ const face = propFace(group);
+ const material = face?.material;
+ if (!material) return;
+ const value = typeof text === 'string' ? text : '';
+
+ if (!value.trim()) {
+ if (material.map) {
+ material.map.dispose();
+ material.map = null;
+ material.needsUpdate = true;
+ }
+ material.userData.signText = '';
+ return;
+ }
+ if (material.userData.signText === value) return; // nothing changed
+
+ const canvas = document.createElement('canvas');
+ canvas.width = 512;
+ canvas.height = 330;
+ const ctx = canvas.getContext('2d');
+ ctx.fillStyle = '#f4efe2';
+ ctx.fillRect(0, 0, canvas.width, canvas.height);
+ ctx.fillStyle = '#241a30';
+ ctx.textAlign = 'center';
+ ctx.textBaseline = 'middle';
+
+ const family = '"Hiragino Kaku Gothic ProN", "Yu Gothic", "Noto Sans JP", sans-serif';
+ const lines = value.split(/\r\n|\r|\n/);
+ const maxWidth = canvas.width * 0.86;
+ let size = 150;
+ const fits = () => {
+ ctx.font = `700 ${size}px ${family}`;
+ return lines.every((line) => ctx.measureText(line).width <= maxWidth)
+ && lines.length * size * 1.15 <= canvas.height * 0.86;
+ };
+ while (size > 18 && !fits()) size -= 2;
+ ctx.font = `700 ${size}px ${family}`;
+ const step = size * 1.15;
+ const startY = canvas.height / 2 - ((lines.length - 1) * step) / 2;
+ lines.forEach((line, index) => ctx.fillText(line, canvas.width / 2, startY + index * step));
+
+ const texture = new THREE.CanvasTexture(canvas);
+ texture.colorSpace = THREE.SRGBColorSpace;
+ texture.anisotropy = 4;
+ if (material.map) material.map.dispose();
+ material.map = texture;
+ material.userData.signText = value;
+ material.needsUpdate = true;
+}
+
/** The world-space box the prop occupies; the app drops its contact shadow from it. */
export function propBounds(group) {
group.updateMatrixWorld(true);