aboutsummaryrefslogtreecommitdiffhomepage
path: root/public/bluebey-studio/src
diff options
context:
space:
mode:
authorYasutake Yohei <61961825+yasutakeyohei@users.noreply.github.com>2026-10-23 10:04:31 +0900
committerYasutake Yohei <61961825+yasutakeyohei@users.noreply.github.com>2026-10-23 10:04:31 +0900
commit6c7cf859ecca2a7adb70b98458fb3f519dc1ac1a (patch)
tree6d4abb5322ae54f935fb4ada7fc8c01de9b06554 /public/bluebey-studio/src
parent33a550e8633006c453be1de454c37b3a26e2fafa (diff)
bluebey-studio: 帽子を描き直し、すべてランダムを削除
Diffstat (limited to 'public/bluebey-studio/src')
-rw-r--r--public/bluebey-studio/src/hats.js104
-rw-r--r--public/bluebey-studio/src/panel.js18
2 files changed, 72 insertions, 50 deletions
diff --git a/public/bluebey-studio/src/hats.js b/public/bluebey-studio/src/hats.js
index 7a54c46..3763f82 100644
--- a/public/bluebey-studio/src/hats.js
+++ b/public/bluebey-studio/src/hats.js
@@ -45,53 +45,89 @@ function tone(mesh, strength) {
return mesh;
}
-/** シルクハット: a tall black crown, a narrow band and a flat brim. */
+/** シルクハット: a tall felt crown that widens a little at the top, a rolled brim
+ * and a ribbon with a flat bow. */
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);
- tone(add(group, cylinder(1.05, 1.05, 0.24, 32), band, 0, 0.30, 0), 0.34);
+ const felt = material('#17131e', { roughness: 0.55 });
+ const ribbon = material('#352a45');
+
+ // Brim: a shallow disc with a rolled edge, so it has a thickness in line art.
+ add(group, cylinder(1.80, 1.66, 0.10, 40), felt, 0, 0.05, 0);
+ const rim = add(group, new THREE.TorusGeometry(1.785, 0.075, 10, 48), felt, 0, 0.095, 0);
+ rim.rotation.x = Math.PI / 2;
+ tone(rim, 0.22);
+
+ // Crown: a lathe, very slightly wider at the top - the classic top-hat flare.
+ const profile = [
+ [1.00, 0.00], [0.985, 0.10], [0.975, 0.88], [0.99, 1.32],
+ [1.02, 1.49], [1.03, 1.57], [0.95, 1.64], [0.62, 1.67], [0.0, 1.68],
+ ].map(([x, y]) => new THREE.Vector2(x, y));
+ add(group, new THREE.LatheGeometry(profile, 40), felt, 0, 0.09, 0);
+
+ // Ribbon: a band around the base, with a flat bow on the left side.
+ tone(add(group, cylinder(1.015, 1.02, 0.20, 40), ribbon, 0, 0.20, 0), 0.30);
+ const bow = tone(add(group, box(0.07, 0.20, 0.30), ribbon, -0.99, 0.20, 0), 0.14);
+ bow.rotation.y = 0.16;
return group;
}
-/** キャップ: a baseball cap - a low dome, a rim and a bill at the front. */
+/** キャップ: a low six-panel crown with a button, a band and a long curved bill. */
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);
- tone(add(group, cylinder(1.10, 1.10, 0.18, 28), dark, 0, 0.09, 0), 0.30);
- // The bill, tipped down a little at the front.
- const bill = tone(add(group, cylinder(0.86, 0.86, 0.09, 24), dark, 0, 0.06, 0.92), 0.30);
- bill.scale.set(1, 1, 1.25);
- bill.rotation.x = -0.12;
- tone(add(group, sphere(0.13, 12, 10), dark, 0, 1.02, 0), 0.30);
+ const cloth = material('#3f6fd0', { roughness: 0.82 });
+ const dark = material('#2c4d96', { roughness: 0.86 });
+ const button = material('#243c74', { roughness: 0.7 });
+
+ // The crown: a hemisphere whose flat face sits on y = 0, squashed and leaning
+ // a touch forward so it reads as fabric, not a bowl.
+ const crown = add(group, sphere(1.05, 32, 20, 0, Math.PI * 2, 0, Math.PI / 2), cloth, 0, 0.02, 0.02);
+ crown.scale.set(1, 0.88, 1.03);
+
+ // The band around the rim, and the button on top.
+ tone(add(group, cylinder(1.075, 1.075, 0.14, 32), dark, 0, 0.07, 0.02), 0.22);
+ tone(add(group, sphere(0.10, 14, 10), button, 0, 0.925, 0.02), 0.18);
+
+ // The bill: an oval that is long at the front and tucked under the crown at
+ // the back, tipped down a little.
+ const bill = tone(add(group, cylinder(1.12, 1.12, 0.075, 32), material('#5b86e0', { roughness: 0.82 }), 0, 0.075, 0.75), 0.26);
+ bill.scale.set(1, 1, 1.9);
+ bill.rotation.x = -0.28;
return group;
}
-/** コック帽: a tall pleated toque - a cylindrical band under a big puffy crown. */
+/** コック帽: a low band under a mushroom-shaped puff with vertical pleats. */
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);
- tone(add(group, cylinder(0.90, 0.90, 0.10, 28), shade, 0, 0.90, 0), 0.26);
- // 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 cloth = material('#f8f7f3', { roughness: 0.9 });
+ const shade = material('#e4e1d7', { roughness: 0.9 });
+
+ // The band that sits on the head.
+ add(group, cylinder(0.86, 0.90, 0.62, 32), cloth, 0, 0.31, 0);
+ tone(add(group, cylinder(0.885, 0.885, 0.06, 32), shade, 0, 0.59, 0), 0.24);
+
+ // The puff: a lathe profile that swells out past the band and closes over the
+ // top, which gives the silhouette a toque has (a ball just looks like a head).
+ const profile = [
+ [0.86, 0.60], [1.10, 0.68], [1.28, 0.85], [1.35, 1.10],
+ [1.32, 1.35], [1.18, 1.56], [0.86, 1.68], [0.44, 1.73], [0.0, 1.74],
+ ].map(([x, y]) => new THREE.Vector2(x, y));
+ add(group, new THREE.LatheGeometry(profile, 36), cloth, 0, 0, 0);
+
+ // Pleats: soft vertical lobes around the swell, thin across and deep along the
+ // radius, ten of them as a real toque has.
+ for (let i = 0; i < 10; i += 1) {
+ const a = (i / 10) * Math.PI * 2;
+ const lobe = add(group, sphere(0.5, 12, 10), cloth,
+ Math.cos(a) * 1.16, 1.18, Math.sin(a) * 1.16);
+ lobe.scale.set(0.40, 0.85, 0.55);
+ lobe.rotation.y = -(a + Math.PI / 2);
}
- const top = add(group, sphere(0.62, 16, 12), white, 0, 1.92, 0);
- top.scale.set(1, 0.78, 1);
+
+ // A rolled seam where the band meets the puff.
+ const roll = add(group, new THREE.TorusGeometry(1.00, 0.07, 10, 36), shade, 0, 0.64, 0);
+ roll.rotation.x = Math.PI / 2;
+ tone(roll, 0.16);
return group;
}
diff --git a/public/bluebey-studio/src/panel.js b/public/bluebey-studio/src/panel.js
index ae45e48..db784cb 100644
--- a/public/bluebey-studio/src/panel.js
+++ b/public/bluebey-studio/src/panel.js
@@ -134,7 +134,7 @@ export function buildPanel(app, root) {
root.replaceChildren();
// --- randomising ----------------------------------------------------------
- // Shared by 「顔をランダムに」 (face tab) and 「すべてをランダムに」 (the top bar).
+ // The patch behind 「顔をランダムに」 in the face tab.
const rnd = (min, max) => min + (max - min) * Math.random();
const pick = (list) => list[Math.floor(Math.random() * list.length)];
const randomEye = () => ({
@@ -181,22 +181,9 @@ export function buildPanel(app, root) {
};
};
const randomFace = () => replace({ face: randomFacePatch() }, 'face');
- const randomAll = () => {
- // 後ろを向く(180°)や大きく傾くポーズは避ける。見える顔が変わる範囲だけ。
- const poses = POSE_PRESETS.filter((preset) => {
- const m = preset.pose?.bones?.master ?? [0, 0, 0];
- return Math.abs(m[0]) <= 45 && Math.abs(m[1]) <= 40 && Math.abs(m[2]) <= 40;
- });
- const pose = pick(poses.length ? poses : POSE_PRESETS);
- app.actions.applyPosePreset?.(pose.id);
- // The viewer style (実写/フラット/線画), the body colours, and 左右反転 are
- // deliberately left alone.
- replace({ face: randomFacePatch() }, 'face');
- sync();
- };
// A small action bar above the tabs: undo/redo apply to every tab, so they must
- // not be buried inside one of them. 「すべてをランダムに」 sits here too.
+ // not be buried inside one of them.
const topBar = h('div', { class: 'panel-topbar' });
const iconButton = (label, d, onClick) => {
const button = h('button', { type: 'button', class: 'icon-btn', title: label, 'aria-label': label });
@@ -207,7 +194,6 @@ export function buildPanel(app, root) {
topBar.append(
iconButton('元に戻す', 'M4.8 11.5h9.7a5.2 5.2 0 0 1 0 10.4h-3.6M4.8 11.5 9.6 6.7M4.8 11.5 9.6 16.3', () => app.actions.undo()),
iconButton('やり直す', 'M19.2 11.5h-9.7a5.2 5.2 0 0 0 0 10.4h3.6M19.2 11.5 14.4 6.7M19.2 11.5 14.4 16.3', () => app.actions.redo()),
- iconButton('すべてをランダムに', 'M7.2 9.4a2.6 2.6 0 1 1 3.9 2.3c-.9.5-1.3 1.2-1.3 2.1v.4M9.2 17.4h.01M16.8 6.8v6.1M16.8 15.6h.01', () => randomAll()),
);
root.append(topBar);