diff options
| author | Yasutake Yohei <61961825+yasutakeyohei@users.noreply.github.com> | 2026-10-04 23:39:45 +0900 |
|---|---|---|
| committer | Yasutake Yohei <61961825+yasutakeyohei@users.noreply.github.com> | 2026-10-04 23:39:45 +0900 |
| commit | fa3411f87c72f249168b00e6338027512ce7e894 (patch) | |
| tree | 7e8a6d5a8cfdfb2d7a2d6128f5e7bb5e0220d1b7 /public/bluebey-studio/src/hats.js | |
| parent | eed94d378af577171e24fe78b4b159be5ca50317 (diff) | |
bluebey-studio: 小物にベッドと布団、帽子の着脱機能を追加
Diffstat (limited to 'public/bluebey-studio/src/hats.js')
| -rw-r--r-- | public/bluebey-studio/src/hats.js | 116 |
1 files changed, 116 insertions, 0 deletions
diff --git a/public/bluebey-studio/src/hats.js b/public/bluebey-studio/src/hats.js new file mode 100644 index 0000000..c639548 --- /dev/null +++ b/public/bluebey-studio/src/hats.js @@ -0,0 +1,116 @@ +import * as THREE from 'three'; + +/** + * Hats for ぶるべー. Each hat is a small group of primitives whose base sits at + * y = 0, so the app can drop it straight onto the top of the head (see + * `hatMount` in src/main.js). The character's front is +z, so a brim or a badge + * faces +z. + * + * These are deliberately not textured: like the props, they are built from a few + * rounded primitives so they take a clean outline and read in every render style. + */ + +const material = (color, opts = {}) => new THREE.MeshStandardMaterial({ + color, + roughness: 0.72, + metalness: 0, + ...opts, +}); + +function add(parent, geometry, mat, x = 0, y = 0, z = 0) { + const mesh = new THREE.Mesh(geometry, mat); + mesh.position.set(x, y, z); + mesh.castShadow = true; + mesh.receiveShadow = false; + parent.add(mesh); + return mesh; +} + +const cylinder = (rt, rb, h, seg = 24) => new THREE.CylinderGeometry(rt, rb, h, seg); +const sphere = (r, w = 24, h = 16, phiStart = 0, phiLength = Math.PI * 2, thetaStart = 0, thetaLength = Math.PI) => ( + new THREE.SphereGeometry(r, w, h, phiStart, phiLength, thetaStart, thetaLength) +); +const box = (w, h, d) => new THREE.BoxGeometry(w, h, d); + +/** シルクハット: a tall black crown, a narrow band and a flat brim. */ +function buildSilk() { + const group = new THREE.Group(); + const black = material('#1b1722'); + const band = material('#3a2f4a'); + add(group, cylinder(1.52, 1.52, 0.12, 32), black, 0, 0.06, 0); + add(group, cylinder(1.00, 1.04, 1.62, 32), black, 0, 0.93, 0); + add(group, cylinder(1.05, 1.05, 0.24, 32), band, 0, 0.30, 0); + return group; +} + +/** キャップ: a baseball cap - a low dome, a rim and a bill at the front. */ +function buildCap() { + const group = new THREE.Group(); + const blue = material('#3f6fd0'); + const dark = material('#2f54a4'); + // The crown: a hemisphere whose flat face sits on y = 0. + const crown = add(group, sphere(1.08, 28, 18, 0, Math.PI * 2, 0, Math.PI / 2), blue, 0, 0, 0); + crown.scale.set(1, 0.92, 1); + add(group, cylinder(1.10, 1.10, 0.18, 28), dark, 0, 0.09, 0); + // The bill, tipped down a little at the front. + const bill = add(group, cylinder(0.86, 0.86, 0.09, 24), dark, 0, 0.06, 0.92); + bill.scale.set(1, 1, 1.25); + bill.rotation.x = -0.12; + add(group, sphere(0.13, 12, 10), dark, 0, 1.02, 0); + return group; +} + +/** コック帽: a tall white band under a puffy crown. */ +function buildChef() { + const group = new THREE.Group(); + const white = material('#f7f6f2'); + const shade = material('#e6e3da'); + add(group, cylinder(0.94, 0.98, 0.62, 28), white, 0, 0.31, 0); + const puff = add(group, sphere(1.22, 24, 18), white, 0, 0.86, 0); + puff.scale.set(1, 0.82, 1); + for (let i = 0; i < 6; i += 1) { + const a = (i / 6) * Math.PI * 2; + const lobe = add(group, sphere(0.44, 14, 12), shade, Math.cos(a) * 0.86, 1.22, Math.sin(a) * 0.86); + lobe.scale.set(1, 0.85, 1); + } + return group; +} + +/** 消防士の帽子: a red helmet with a brim, a top ridge and a small front badge. */ +function buildFire() { + const group = new THREE.Group(); + const red = material('#c5342f'); + const dark = material('#8f241f'); + const gold = material('#f0c24a', { metalness: 0.4, roughness: 0.4 }); + const dome = add(group, sphere(1.06, 28, 18, 0, Math.PI * 2, 0, Math.PI / 2), red, 0, 0, 0); + dome.scale.set(1, 0.95, 1); + const brim = add(group, cylinder(1.34, 1.34, 0.12, 28), dark, 0, 0.06, 0.18); + brim.scale.set(1, 1, 1.1); + // The ridge along the top, front to back. + const crest = add(group, box(0.16, 0.42, 1.5), dark, 0, 1.02, 0); + crest.rotation.x = -0.05; + add(group, sphere(0.2, 14, 12), gold, 0, 0.62, 1.02); + return group; +} + +const BUILDERS = { + silk: buildSilk, + cap: buildCap, + chef: buildChef, + fire: buildFire, +}; + +/** The hats offered in the panel: `none` clears it. */ +export const HAT_LIBRARY = [ + { id: 'none', label: 'なし' }, + { id: 'silk', label: 'シルクハット' }, + { id: 'cap', label: 'キャップ' }, + { id: 'chef', label: 'コック帽' }, + { id: 'fire', label: '消防士' }, +]; + +/** Builds the named hat, or `null` for `none` / an unknown name. */ +export function buildHat(id) { + const builder = BUILDERS[id]; + return builder ? builder() : null; +} |
