diff options
Diffstat (limited to 'public/bluebey-studio/src')
| -rw-r--r-- | public/bluebey-studio/src/faceArt.js | 57 | ||||
| -rw-r--r-- | public/bluebey-studio/src/panel.js | 10 | ||||
| -rw-r--r-- | public/bluebey-studio/src/presets.js | 3 |
3 files changed, 53 insertions, 17 deletions
diff --git a/public/bluebey-studio/src/faceArt.js b/public/bluebey-studio/src/faceArt.js index a14f53a..cf61c68 100644 --- a/public/bluebey-studio/src/faceArt.js +++ b/public/bluebey-studio/src/faceArt.js @@ -871,6 +871,39 @@ function drawFaceExtras(ctx, style, lineOnly) { drawBeard(ctx, style.beard, lineOnly, style.beards); } +const BEARD_TINT_IDS = new WeakMap(); +const BEARD_TINT_CACHE = new Map(); +let beardTintSeq = 0; + +/** + * A copy of a beard drawing in the chosen colour. + * + * The supplied files are drawn in near-black, so repainting the ink with the + * beard colour (`source-in` keeps the anti-aliased alpha) is what lets the colour + * picker work on a drawing. Cached per (sprite, colour). + */ +function tintedSprite(image, color) { + let id = BEARD_TINT_IDS.get(image); + if (!id) { + beardTintSeq += 1; + id = beardTintSeq; + BEARD_TINT_IDS.set(image, id); + } + const key = `${id}|${color}`; + const cached = BEARD_TINT_CACHE.get(key); + if (cached) return cached; + const canvas = document.createElement('canvas'); + canvas.width = image.width; + canvas.height = image.height; + const ctx = canvas.getContext('2d'); + ctx.drawImage(image, 0, 0); + ctx.globalCompositeOperation = 'source-in'; + ctx.fillStyle = color; + ctx.fillRect(0, 0, canvas.width, canvas.height); + BEARD_TINT_CACHE.set(key, canvas); + return canvas; +} + /** * ひげ: a mustache or beard under the nose. Each kind is a small shape built on * the spot; `scotch` and `cat` are drawn with round-capped strokes, `kaiser` is a @@ -884,38 +917,40 @@ function drawBeard(ctx, spec, lineOnly = false, textures = null) { const cx = BEARD_LAYOUT.cx; const cy = BEARD_LAYOUT.cy + clamp(spec.offsetY ?? 0, -600, 600); const w = BEARD_LAYOUT.width * size; + const color = spec.color ?? '#3a2a4a'; // A supplied drawing for this kind (assets/beards/<shape>.png) replaces the // built-in strokes. The drawings are trimmed to their ink, so `w` is the - // beard's real width; it is centred on the nose and scaled by `size`. + // beard's real width; it is centred on the nose and scaled by `size`, and it is + // tinted with the beard colour (the files are drawn in near-black). const entry = textures?.[shape]; if (entry?.image) { - const { image, half } = entry; + const sprite = tintedSprite(entry.image, color); // Slightly wider than the built-in base: a moustache drawn to the same width // as a beard's stroke sat mostly behind the nose. const drawW = w * 1.4; - if (half) { + if (entry.half) { // One side, mirrored to make the pair. The drawing is the model's own right - // side, which sits in the artwork's left half; `spacing` opens the middle. + // side, which sits in the artwork's left half; `spacing` opens the middle + // (a negative value brings the two halves together). const halfW = drawW / 2; - const dh = (halfW * (image.height || 1)) / (image.width || 1); - const gap = clamp(spec.spacing ?? 0, 0, 800); + const dh = (halfW * (sprite.height || 1)) / (sprite.width || 1); + const gap = clamp(spec.spacing ?? 0, -400, 800); const y = cy - dh / 2; - ctx.drawImage(image, cx - gap - halfW, y, halfW, dh); + ctx.drawImage(sprite, cx - gap - halfW, y, halfW, dh); ctx.save(); ctx.translate(cx + gap, cy); ctx.scale(-1, 1); // Draw into [-halfW, 0] so that, mirrored, it lands in [cx+gap, cx+gap+halfW]. - ctx.drawImage(image, -halfW, -dh / 2, halfW, dh); + ctx.drawImage(sprite, -halfW, -dh / 2, halfW, dh); ctx.restore(); return; } - const dh = (drawW * (image.height || 1)) / (image.width || 1); - ctx.drawImage(image, cx - drawW / 2, cy - dh / 2, drawW, dh); + const dh = (drawW * (sprite.height || 1)) / (sprite.width || 1); + ctx.drawImage(sprite, cx - drawW / 2, cy - dh / 2, drawW, dh); return; } - const color = spec.color ?? '#3a2a4a'; const line = spec.line ?? '#55386e'; const lw = Math.max(2, 9 * size); const stroke = (points, width) => strokePolyline(ctx, points, { diff --git a/public/bluebey-studio/src/panel.js b/public/bluebey-studio/src/panel.js index 3f63eb6..d63055a 100644 --- a/public/bluebey-studio/src/panel.js +++ b/public/bluebey-studio/src/panel.js @@ -168,7 +168,7 @@ export function buildPanel(app, root) { left: randomEye(), right: randomEye(), cheeks: { enabled: Math.random() < 0.5 }, - beard: { shape: beardShape, ...(BEARD_KIND_DEFAULTS[beardShape] ?? { size: 1, offsetY: 0 }) }, + beard: { shape: beardShape, ...(BEARD_KIND_DEFAULTS[beardShape] ?? { size: 1, offsetY: 0, spacing: 0 }) }, headMark: { shape: pick(['off', 'cap', 'fringe', 'fringe-up', 'wave', 'side']) }, brow: { enabled: true, @@ -1135,7 +1135,7 @@ export function buildPanel(app, root) { // Each kind starts at its own size and height (a textured kind is drawn at a // different scale from the built-in strokes). A kind with no entry resets to // the neutral values, so switching kinds cannot leave a stray size behind. - Object.assign(beard, BEARD_KIND_DEFAULTS[value] ?? { size: 1, offsetY: 0 }); + Object.assign(beard, BEARD_KIND_DEFAULTS[value] ?? { size: 1, offsetY: 0, spacing: 0 }); app.actions.refresh('face'); sync(); }, @@ -1147,7 +1147,7 @@ export function buildPanel(app, root) { onInput: (v) => { state.face.eyes.beard.size = v; app.actions.refresh('face'); }, }), spacing: slider({ - label: '左右の間隔', min: 0, max: 240, step: 1, value: 0, format: (v) => `${Math.round(v)}`, + label: '左右の間隔', min: -10, max: 240, step: 1, value: 0, format: (v) => `${Math.round(v)}`, onInput: (v) => { state.face.eyes.beard.spacing = v; app.actions.refresh('face'); }, }), offsetY: slider({ @@ -1170,8 +1170,8 @@ export function buildPanel(app, root) { beardBox.add(beardSliders.length.el); beardBox.add(beardSliders.lineGap.el); const beardColor = colorField({ - label: 'ひげの色', value: '#3a2a4a', - swatches: ['#3a2a4a', '#1c1624', '#6b4a2a', '#8a8a94'], + label: 'ひげの色', value: '#1c1624', + swatches: ['#1c1624', '#000000', '#3a2a4a', '#6b4a2a', '#8a8a94'], onChange: (v) => { state.face.eyes.beard.color = v; app.actions.refresh('face'); }, }); beardBox.add(beardColor.el); diff --git a/public/bluebey-studio/src/presets.js b/public/bluebey-studio/src/presets.js index 2214cb1..33fdd45 100644 --- a/public/bluebey-studio/src/presets.js +++ b/public/bluebey-studio/src/presets.js @@ -47,6 +47,7 @@ function captionDefaults() { */ export const BEARD_KIND_DEFAULTS = { scotch: { size: 0.46, offsetY: 74 }, + kaiser: { size: 1, offsetY: 100, spacing: -10 }, }; /** @@ -256,7 +257,7 @@ export function defaultState() { // three lines of one side sit (`lineGap`). Neither changes the thickness. length: 1, lineGap: 1, - color: '#3a2a4a', + color: '#1c1624', }, // Eyebrows are drawn into the eye texture. Off by default, because the // original character has no eyebrows. |
