aboutsummaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
-rw-r--r--public/bluebey-studio/src/faceArt.js57
-rw-r--r--public/bluebey-studio/src/panel.js10
-rw-r--r--public/bluebey-studio/src/presets.js3
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.