aboutsummaryrefslogtreecommitdiffhomepage
path: root/public/bluebey-studio/src/hats.js
blob: 3fb65f30dfce44780c7da4d51ddc4624381bf001 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
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 pleated toque - a cylindrical band under a big puffy crown. */
function buildChef() {
  const group = new THREE.Group();
  const white = material('#f7f6f2');
  const shade = material('#e6e3da');
  // The band: a tall cylinder that sits on the head.
  add(group, cylinder(0.86, 0.92, 0.95, 28), white, 0, 0.475, 0);
  add(group, cylinder(0.90, 0.90, 0.10, 28), shade, 0, 0.90, 0);
  // The crown: a big squashed sphere ringed with lobes, so the top reads as the
  // classic pleated toque rather than a plain dome.
  const puff = add(group, sphere(1.24, 26, 20), white, 0, 1.30, 0);
  puff.scale.set(1, 0.82, 1);
  for (let i = 0; i < 8; i += 1) {
    const a = (i / 8) * Math.PI * 2;
    const lobe = add(group, sphere(0.5, 14, 12), white, Math.cos(a) * 0.92, 1.52, Math.sin(a) * 0.92);
    lobe.scale.set(1, 0.9, 1);
  }
  const top = add(group, sphere(0.62, 16, 12), white, 0, 1.92, 0);
  top.scale.set(1, 0.78, 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;
}