diff options
| author | Yasutake Yohei <61961825+yasutakeyohei@users.noreply.github.com> | 2026-10-23 10:04:31 +0900 |
|---|---|---|
| committer | Yasutake Yohei <61961825+yasutakeyohei@users.noreply.github.com> | 2026-10-23 10:04:31 +0900 |
| commit | 6c7cf859ecca2a7adb70b98458fb3f519dc1ac1a (patch) | |
| tree | 6d4abb5322ae54f935fb4ada7fc8c01de9b06554 /public | |
| parent | 33a550e8633006c453be1de454c37b3a26e2fafa (diff) | |
bluebey-studio: 帽子を描き直し、すべてランダムを削除
Diffstat (limited to 'public')
| -rw-r--r-- | public/bluebey-studio/src/hats.js | 104 | ||||
| -rw-r--r-- | public/bluebey-studio/src/panel.js | 18 |
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); |
