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