diff options
| author | Yasutake Yohei <61961825+yasutakeyohei@users.noreply.github.com> | 2026-10-04 00:06:06 +0900 |
|---|---|---|
| committer | Yasutake Yohei <61961825+yasutakeyohei@users.noreply.github.com> | 2026-10-04 00:06:06 +0900 |
| commit | c81104129a9ba6e26aa33a00770f76a6aa97b2a9 (patch) | |
| tree | 6329d9b3cd779c7373fd20e63f7db0ff6d4f45ac /public/bluebey-studio/src/faceArt.js | |
| parent | cb0cce903f8ea3ad4943cabab83b74a40d87fc00 (diff) | |
bluebey-studio: 髪の板(hair-plate)で頭の模様を後ろまで回す
モデルに追加された hair-plate(頭をぐるりと覆う円筒UVの板)を読み込み、頭の模様をその板にも描くようにした。これで横や後ろから見ても髪が見える。髪の板が無いモデルは従来どおり前面の板に描く。
Diffstat (limited to 'public/bluebey-studio/src/faceArt.js')
| -rw-r--r-- | public/bluebey-studio/src/faceArt.js | 80 |
1 files changed, 80 insertions, 0 deletions
diff --git a/public/bluebey-studio/src/faceArt.js b/public/bluebey-studio/src/faceArt.js index e999045..1083e52 100644 --- a/public/bluebey-studio/src/faceArt.js +++ b/public/bluebey-studio/src/faceArt.js @@ -178,6 +178,26 @@ export const HEAD_MARK_LAYOUT = { }; /** + * 頭の模様 on the *hair plate*: the head's cover when the model carries a plate + * that wraps all the way round (material `hair-plate`, cylindrical UV). + * + * The front plate only reaches the head's silhouette, so hair drawn on it stops + * at the front half. The hair plate's UV is `u` = angle round the head (front at + * 0.5, back at the seam 0/1) and `v` = height (0 at the model's feet, 1 at the + * top of the head), so the covering is everything *above* a boundary line: a cap + * that is visible from behind as well. + */ +export const HAIR_WRAP_LAYOUT = { + // The wrap's v runs top-of-head (0) to the model's feet (1) after the GLB's + // V-flip, so `base` is how far down from the top the hairline sits at size 1. + // `reach` is how much a unit of `size` moves it; `offsetScale` converts the + // `offsetY` slider (artwork px) to the same units, so one slider drives both. + base: 0.30, + reach: 0.30, + offsetScale: 0.30 / 190, +}; + +/** * ひげ: the mustaches and beards that hang under the nose. Drawn on the eye plate * at the nose, the same spot the snot bubble uses. `shape` picks a well-known * kind; `size` scales it about the nose, and the colours are adjustable. @@ -861,6 +881,66 @@ function drawHeadCover(ctx, spec, limits) { } /** + * 頭の模様 drawn onto the hair plate, which wraps right round the head. + * + * The plate's canvas is the cylindrical UV unrolled: `x` is the angle (front at + * the middle, the seam at the back at both ends) and `y` is the height. The GLB + * flips V on export, so the top of the head lands at the top of the canvas and + * the covering is everything above the boundary line - the same direction as + * `drawHeadCover`, which fills down from the top edge of the front plate. + * + * The same `shape` names are reused, with their front-to-back modulation tied to + * how far the column leans towards the front (`t`). + */ +export function drawHeadCoverWrap(ctx, spec, layout) { + const shape = spec?.shape ?? 'off'; + // `split` is a retired front-plate shape; it has no wrap equivalent. + if (shape === 'off' || shape === 'none' || shape === 'split') return; + + const size = clamp(spec.size ?? 1, 0.02, 3); + const W = layout.width; + const H = layout.height; + // After the GLB's V-flip the wrap's v is 0 at the top of the head and 1 at the + // model's feet, so a *larger* boundary means more hair (it reaches further + // down) - the same direction as `size` and `offsetY` on the front plate. + const base = HAIR_WRAP_LAYOUT.base + + (size - 1) * HAIR_WRAP_LAYOUT.reach + + clamp(spec.offsetY ?? 0, -600, 600) * HAIR_WRAP_LAYOUT.offsetScale; + const color = spec.color ?? '#8a4fe0'; + + ctx.save(); + ctx.beginPath(); + ctx.rect(0, 0, W, H); + ctx.clip(); + + // `t` is -1..1 across the front (+-90 deg around u = 0.5); everything behind is + // pinned to +-1, so the hairline runs flat round the back of the head. + const half = W * 0.25; + const cx = W / 2 + (clamp(spec.offsetX ?? 0, -600, 600) / 512) * half; + const vBound = (x) => { + const t = clamp((x - cx) / half, -1, 1); + if (shape === 'fringe') return base + 0.14 * (1 - Math.abs(t)) - 0.05 * Math.abs(Math.sin(t * Math.PI)); + if (shape === 'fringe-up') return base - 0.14 * (1 - Math.abs(t)) + 0.05 * Math.abs(Math.sin(t * Math.PI)); + if (shape === 'wave') return base + 0.10 * Math.sin(t * Math.PI * 0.9 + 0.5); + if (shape === 'side') return base + 0.20 * t; + return base - 0.07 * (1 - t * t); // `cap` + }; + + const steps = 96; + ctx.beginPath(); + ctx.moveTo(0, 0); + for (let i = 0; i <= steps; i += 1) { + const x = (W * i) / steps; + ctx.lineTo(x, clamp(vBound(x), 0, 1) * H); + } + ctx.lineTo(W, 0); + ctx.closePath(); + ctx.fillStyle = color; + ctx.fill(); + ctx.restore(); +} + +/** * The shared extras (ほっぺ and 頭の模様) are drawn once per face, before the eyes, * so the eyes, brows and glasses win any overlap and the cheeks are not doubled. */ |
