From fbaf2cc671fed23b555e218499301af0f50327b0 Mon Sep 17 00:00:00 2001 From: Yasutake Yohei <61961825+yasutakeyohei@users.noreply.github.com> Date: Sat, 3 Oct 2026 15:44:38 +0900 Subject: public/bluebey-studio: 移動範囲と影・壁の追従、看板の文字 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 移動スライダーの範囲を広げ、キーライトと影カメラがぶるべーに追従するようにした(遠くへ動かしても影が切れない)。地面も広げた。 足もとの丸い影は既定で非表示にし、「足もとの丸い影」で切り替えられるようにした。原点に取り残されないよう、ぶるべーに追従させる。 見えない壁は体の向き(master のY回転と見る先の体向き)にも追従するようにした。 「舞台」をやめ、「小物」「擬音」「見えない壁」をそれぞれ独立したメニューにした。看板に文字を書けるようにした(複数行可)。 --- public/bluebey-studio/src/props.js | 63 +++++++++++++++++++++++++++++++++++++- 1 file changed, 62 insertions(+), 1 deletion(-) (limited to 'public/bluebey-studio/src/props.js') 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); -- cgit v1.3.1