diff options
| author | Yasutake Yohei <61961825+yasutakeyohei@users.noreply.github.com> | 2026-10-04 23:48:24 +0900 |
|---|---|---|
| committer | Yasutake Yohei <61961825+yasutakeyohei@users.noreply.github.com> | 2026-10-04 23:48:24 +0900 |
| commit | 793bfd720525f7f1f14a507e1c962101db5ef283 (patch) | |
| tree | 83417b5030d8a25ee148e6caadafe131a35178ac /public | |
| parent | fa3411f87c72f249168b00e6338027512ce7e894 (diff) | |
bluebey-studio: まえがみをM字に修正し、わけめを削除、眉のUIを整理
Diffstat (limited to 'public')
| -rw-r--r-- | public/bluebey-studio/src/faceArt.js | 82 | ||||
| -rw-r--r-- | public/bluebey-studio/src/panel.js | 25 | ||||
| -rw-r--r-- | public/bluebey-studio/src/presets.js | 4 |
3 files changed, 31 insertions, 80 deletions
diff --git a/public/bluebey-studio/src/faceArt.js b/public/bluebey-studio/src/faceArt.js index ba748f5..1f7c4ee 100644 --- a/public/bluebey-studio/src/faceArt.js +++ b/public/bluebey-studio/src/faceArt.js @@ -835,7 +835,7 @@ function drawCheeks(ctx, spec) { */ function drawHeadCover(ctx, spec, limits) { const shape = spec?.shape ?? 'off'; - if (shape === 'off' || shape === 'none' || shape === 'wave') return; + if (shape === 'off' || shape === 'none' || shape === 'wave' || shape === 'side') return; const size = clamp(spec.size ?? 1, 0.02, 3); const left = limits.left; @@ -867,18 +867,19 @@ function drawHeadCover(ctx, spec, limits) { const halfSpan = width / 2; const boundary = (x) => { const t = clamp((x - cx) / halfSpan, -1, 1); - // まえがみ: 富士額 / M字 (a widow's peak). + // まえがみ: 富士額 / M字 (a widow's peak). The covering dips to a point at + // the centre and at the two temples, receding in between - the two valleys + // and three points of an "M". The points scale with `size`, so a smaller + // まえがみ is a smaller M rather than a thin band with a tall spike. if (shape === 'fringe') { - const m = (1 + Math.cos(2 * Math.PI * t)) / 2; - const peak = Math.max(0, 1 - Math.abs(t) * 3); - return base + 300 * m + 160 * peak; + const dip = (x, centre, reach) => Math.max(0, 1 - Math.abs(x - centre) / reach); + const shape01 = Math.max(dip(t, 0, 0.36), 0.6 * Math.max(dip(t, -0.82, 0.26), dip(t, 0.82, 0.26))); + return base + 190 * size * shape01; } // はちわれ: the same V upside down - a widow's peak. if (shape === 'fringe-up') return base - 320 * Math.max(0, 1 - Math.abs(t) * 1.5); // カーブ: a plain shallow curve, higher (more hair) round the sides. if (shape === 'curve') return base + 160 * t * t; - // わけめ: a 7:3 parting - a step at 30% from the left. - if (shape === 'side') return base + 60 - 320 / (1 + Math.exp(-(t + 0.4) * 10)); // `cap`: a rounded helmet, lowest in the middle. return base - 100 * (1 - t * t); }; @@ -894,23 +895,6 @@ function drawHeadCover(ctx, spec, limits) { ctx.closePath(); ctx.fillStyle = color; ctx.fill(); - - if (shape === 'side') { - // わけめ: cut a real parting in the hair, so the two halves read as combed - // apart rather than as one smooth sweep. - const partX = cx - halfSpan * 0.4; - const gw = Math.max(5, width * 0.02) * (0.6 + size); - ctx.save(); - ctx.globalCompositeOperation = 'destination-out'; - ctx.beginPath(); - ctx.moveTo(partX - gw * 0.16, top); - ctx.lineTo(partX + gw * 0.16, top); - ctx.lineTo(partX + gw * 0.7, boundary(partX) + 4); - ctx.lineTo(partX - gw * 0.7, boundary(partX) + 4); - ctx.closePath(); - ctx.fill(); - ctx.restore(); - } ctx.restore(); } @@ -928,8 +912,9 @@ function drawHeadCover(ctx, spec, limits) { */ export function drawHeadCoverWrap(ctx, spec, layout) { const shape = spec?.shape ?? 'off'; - // `split` and `wave` are retired front-plate shapes; they have no wrap equal. - if (shape === 'off' || shape === 'none' || shape === 'split' || shape === 'wave') return; + // `split`, `wave` and `side` are retired front-plate shapes; they have no wrap + // equal. An old saved state may still carry one, so it simply draws no hair. + if (shape === 'off' || shape === 'none' || shape === 'split' || shape === 'wave' || shape === 'side') return; const size = clamp(spec.size ?? 1, 0.02, 3); const W = layout.width; @@ -962,30 +947,19 @@ export function drawHeadCoverWrap(ctx, spec, layout) { // centre (the peak) and at the temples, with the corners between receded - // the three peaks and two valleys of an "M". `t` is -1..1 across the front. if (shape === 'fringe') { - const m = (1 + Math.cos(2 * Math.PI * t)) / 2; // wide peaks at t = 0, +-1 - const peak = Math.max(0, 1 - Math.abs(t) * 1.6); // the central point - const shape01 = 0.55 * m + 0.45 * peak; // 0..1 across the M - // The recessed corners keep a hairline, and the peaks scale with `size`, so - // a small まえがみ stays an M instead of collapsing into a spindle. - const valley = Math.max(base, 0.10); - // The peaks deepen with `size`, so a small まえがみ is a small M rather - // than a tall spike on a thin band. - const amp = 0.36 * (0.55 + 0.45 * Math.min(clamp(size, 0.05, 3), 1)); - return valley + amp * shape01; + const dip = (x, centre, reach) => Math.max(0, 1 - Math.abs(x - centre) / reach); + // The centre dips the lowest, the two temples dip less, and the hairline + // recedes between them - the two valleys and three points of an "M". + const shape01 = Math.max(dip(t, 0, 0.36), 0.6 * Math.max(dip(t, -0.82, 0.26), dip(t, 0.82, 0.26))); + // The points grow with `size`, so a small まえがみ is a small M rather than + // a thin band with a tall spike (a spindle). + return base + 0.16 * size * shape01; } // はちわれ: the same V upside down - a widow's peak. if (shape === 'fringe-up') return base - 0.18 * Math.max(0, 1 - Math.abs(t) * 1.5); // カーブ: a deep round cover - low at the sides, high in the middle, like the // blue of a certain robot cat's head. if (shape === 'curve') return base + 0.42 * t * t; - // わけめ: a 7:3 part. The hairline is asymmetric across the *front* only and - // comes back to the base level at the sides, so it is continuous at the back - // seam (an angle-varying hairline there would ride the seam and step). The - // parting line is the gap cut out below. - if (shape === 'side') { - const w = Math.max(0, 1 - t * t); // 0 at the sides, 1 at the front - return base + 0.18 * w * (0.5 + 0.5 * t); - } return base - 0.07 * (1 - t * t); // `cap` }; @@ -1000,26 +974,6 @@ export function drawHeadCoverWrap(ctx, spec, layout) { ctx.closePath(); ctx.fillStyle = color; ctx.fill(); - - if (shape === 'side') { - // The parting: a shallow notch cut up from the hairline at 30% (a 7:3 split). - // Cutting all the way to the top of the head made it look bald, so only the - // lower part of the covering is cut. - const partX = span * 0.4; - const gw = Math.max(6, span * 0.012) * (0.6 + size); - const bY = clamp(vBound(partX), 0, 1) * H; - const top = Math.max(0, bY - Math.min(110, bY * 0.6)); - ctx.save(); - ctx.globalCompositeOperation = 'destination-out'; - ctx.beginPath(); - ctx.moveTo(partX - gw * 0.35, top); - ctx.lineTo(partX + gw * 0.35, top); - ctx.lineTo(partX + gw * 1.1, bY + 2); - ctx.lineTo(partX - gw * 1.1, bY + 2); - ctx.closePath(); - ctx.fill(); - ctx.restore(); - } ctx.restore(); } diff --git a/public/bluebey-studio/src/panel.js b/public/bluebey-studio/src/panel.js index 67022aa..ed7f766 100644 --- a/public/bluebey-studio/src/panel.js +++ b/public/bluebey-studio/src/panel.js @@ -171,9 +171,11 @@ export function buildPanel(app, root) { right: randomEye(), cheeks: { enabled: Math.random() < 0.5 }, beard: { shape: beardShape, ...(BEARD_KIND_DEFAULTS[beardShape] ?? { size: 1, offsetY: 0, spacing: 0 }) }, - headMark: { shape: pick(['off', 'cap', 'fringe', 'fringe-up', 'curve', 'side']) }, + headMark: { shape: pick(['off', 'cap', 'fringe', 'fringe-up', 'curve']) }, brow: { enabled: true, + // Random brows are line drawings, never ゴル風's loaded picture. + image: false, angle: Math.round(rnd(-30, 40)), height: Math.round(rnd(-10, 50)), length: +rnd(0.6, 2.6).toFixed(2), @@ -871,9 +873,12 @@ export function buildPanel(app, root) { onClick: () => { // 両津勘吉: thick げじげじ brows that are joined in the middle, sitting // low over the eyes. `spacing` is what closes the gap - at this length - // the two inner ends meet over the nose (see the panel's note). + // the two inner ends meet over the nose (see the panel's note). Turning + // `image` off is what makes the line drawing take over from ゴル風's + // loaded picture. Object.assign(state.face.eyes.brow, { enabled: true, + image: false, angle: 10, height: 4, length: 2.86, @@ -890,14 +895,8 @@ export function buildPanel(app, root) { ], })); - const browImageToggle = check({ - label: 'ゴル風の画像を使う', - value: false, - onChange: (value) => { state.face.eyes.brow.image = value; app.actions.refresh('face'); }, - }); - faceSection.add(controlRow(null, browImageToggle.el, { wide: true })); - faceSection.add(hint('読み込んだ画像(assets/brows/gol-right.png)を眉として左右に描きます。' - + '「ゴル風」を押すと自動で入ります。画像が無いときは今までの線で描きます。')); + faceSection.add(hint('「ゴル風」は読み込んだ画像(assets/brows/gol-right.png)で眉を描きます。' + + '「両風」など線で描く眉に切り替えると、この画像は使われません。')); // --- glasses / sunglasses (drawn into the same texture as the eyes and brows) faceSection = faceTabSections.glasses; @@ -1082,7 +1081,6 @@ export function buildPanel(app, root) { { value: 'fringe', label: 'まえがみ' }, { value: 'fringe-up', label: 'はちわれ' }, { value: 'curve', label: 'カーブ' }, - { value: 'side', label: 'わけめ' }, ], value: 'off', onChange: (value) => { state.face.eyes.headMark.shape = value; app.actions.refresh('face'); }, @@ -1127,8 +1125,8 @@ export function buildPanel(app, root) { headMarkBox.add(headMarkColor.el); headMarkBox.add(headMarkColor2.el); headMarkBox.add(hint('「かぶせ」は頭にかぶせる丸い髪、「まえがみ」は中央とこめかみが下がるM字(富士額)の前髪、' - + '「はちわれ」は中央が鋭く持ち上がった生え際、「カーブ」はドラえもんのように単純にカーブした髪、' - + '「わけめ」は7:3に分けた髪です。「たかさ」で生え際を目の中心あたりまで下げられます。' + + '「はちわれ」は中央が鋭く持ち上がった生え際、「カーブ」はドラえもんのように単純にカーブした髪です。' + + '「たかさ」で生え際を目の中心あたりまで下げられます。' + '「髪を目の前に出す」を切ると、髪が目の後ろに回ります。')); // --- 鼻ちょうちん: the snot bubble of a sleeping face. @@ -2706,7 +2704,6 @@ export function buildPanel(app, root) { browSliders.spacing.set(state.face.eyes.brow.spacing ?? 0); browSliders.curve.set(state.face.eyes.brow.curve); browSliders.taper.set(state.face.eyes.brow.taper ?? 1); - browImageToggle.set(state.face.eyes.brow.image === true); browColor.set(state.face.eyes.brow.color); glassesToggle.set(state.face.eyes.glasses.enabled === true); glassesKindSegment.set(state.face.eyes.glasses.kind ?? 'glasses'); diff --git a/public/bluebey-studio/src/presets.js b/public/bluebey-studio/src/presets.js index 43fc12f..7f091f1 100644 --- a/public/bluebey-studio/src/presets.js +++ b/public/bluebey-studio/src/presets.js @@ -231,8 +231,8 @@ export function defaultState() { color: '#f6a6b8', }, // The hair covering's `shape` is one of `off`, `cap`, `fringe`, - // `fringe-up`, `curve` or `side`. `overEyes` puts the hair in front of - // the eyes (bangs over the face); off leaves the eyes on top. + // `fringe-up` or `curve`. `overEyes` puts the hair in front of the eyes + // (bangs over the face); off leaves the eyes on top. headMark: { shape: 'off', size: 1, |
