aboutsummaryrefslogtreecommitdiffhomepage
path: root/public
diff options
context:
space:
mode:
authorYasutake Yohei <61961825+yasutakeyohei@users.noreply.github.com>2026-10-05 14:13:21 +0900
committerYasutake Yohei <61961825+yasutakeyohei@users.noreply.github.com>2026-10-05 14:13:21 +0900
commit174886a483d90756b1320c12aef0e6f296c6d425 (patch)
tree9683ef2b818cbb13293f0fab6d3096c76e541d49 /public
parent4808fc1cd44737200cfd51747a628585185f1f9f (diff)
bluebey-studio: 目メニューを再編し、閉じ目にもまぶたを効かせる
閉じ目を眼球の中身として上まぶた・下まぶたで隠せるようにした。まぶた設定を「まぶた」グループに、ハートの色を目の形の直下に、瞳と形の大きさを「大きさ」に統合。こまかい設定は解体し、横位置・手描きは個別の箱に分けた。
Diffstat (limited to 'public')
-rw-r--r--public/bluebey-studio/src/faceArt.js169
-rw-r--r--public/bluebey-studio/src/gacha.js25
-rw-r--r--public/bluebey-studio/src/panel.js120
-rw-r--r--public/bluebey-studio/src/presets.js14
4 files changed, 186 insertions, 142 deletions
diff --git a/public/bluebey-studio/src/faceArt.js b/public/bluebey-studio/src/faceArt.js
index 3eb106f..04dd6e3 100644
--- a/public/bluebey-studio/src/faceArt.js
+++ b/public/bluebey-studio/src/faceArt.js
@@ -489,10 +489,8 @@ function rotate(points, pivot, degrees) {
* shaded face behind it. That is what makes the eyelid blend perfectly without
* baking a face-coloured background into the texture.
*/
-function clipEye(ctx, eye, open, lowerLid, flat = false, tiltDeg = 0) {
- circlePath(ctx, eye.cx, eye.cy, eye.r);
- ctx.clip();
-
+/** Clip to the upper and lower lids only, without the eyeball circle. */
+function clipLids(ctx, eye, open, lowerLid, flat = false, tiltDeg = 0) {
const tilt = (clamp(tiltDeg, -45, 45) * Math.PI) / 180;
const rotated = Math.abs(tilt) > 1e-4;
const spin = () => {
@@ -557,6 +555,13 @@ function clipEye(ctx, eye, open, lowerLid, flat = false, tiltDeg = 0) {
}
}
+/** Clip to the eyeball circle as well (the iris and heart are eyeball content). */
+function clipEye(ctx, eye, open, lowerLid, flat = false, tiltDeg = 0) {
+ circlePath(ctx, eye.cx, eye.cy, eye.r);
+ ctx.clip();
+ clipLids(ctx, eye, open, lowerLid, flat, tiltDeg);
+}
+
function lidPath(ctx, eye, amount, lower, flat = false) {
ctx.beginPath();
if (flat) {
@@ -1316,6 +1321,23 @@ function drawGlasses(ctx, eye, line, style, limits) {
* @param {object} p.eyes `{ left, right }`, each `{ open, lookX, lookY, closed }`
* @param {object} [p.style] colours and shared shaping parameters
*/
+/**
+ * Which artwork an eye shows: 'open', 'heart', 'line1'..'line3', 'three' or
+ * 'arch'. `shape` is the explicit choice; older saved states and the おまかせ
+ * rolls only set `open`/`closed`/`irisShape`, so this falls back to those.
+ */
+function eyeShapeKey(spec) {
+ // A fully lowered upper lid always means a shut artwork, even if a stale
+ // `shape` says otherwise - this is what keeps older saved links working.
+ if ((spec.open ?? 1) <= 0.02) {
+ if (spec.closed === 'three') return 'three';
+ if (spec.closed === 'arch') return 'arch';
+ return `line${clamp(Math.round(spec.closedLines ?? 1), 1, 3)}`;
+ }
+ if (spec.shape) return spec.shape;
+ return (spec.irisShape ?? 'circle') === 'heart' ? 'heart' : 'open';
+}
+
export function drawEyes(ctx, p) {
const mode = p.mode ?? 'paint';
const line = mode === 'line';
@@ -1325,12 +1347,9 @@ export function drawEyes(ctx, p) {
const irisColor = style.iris ?? '#150e1b';
const lineColor = style.line ?? '#55386e';
const lidWidth = style.lidWidth ?? EYE_LAYOUT.lidStroke;
- const irisScale = style.irisScale ?? 1;
const lookMax = style.lookMax ?? 1;
const heartScale = 2;
const heartColor = style.heartColor ?? '#e0344f';
- const irisRadius = EYE_LAYOUT.irisRadius * irisScale;
- const travel = Math.max(0, EYE_LAYOUT.radius - irisRadius);
const heartTravel = EYE_LAYOUT.radius * 0.9;
const lidFlat = (style.lidShape ?? 'curve') === 'flat';
// How much the whole narrowed eye is tilted (e.g. an angry or gentle squint).
@@ -1342,7 +1361,6 @@ export function drawEyes(ctx, p) {
for (const layout of EYE_LAYOUT.eyes) {
const spec = (p.eyes ?? {})[layout.key] ?? {};
- const open = clamp(spec.open ?? 1, 0, 1);
const eye = {
// `eyeX` slides this eye - and its lid, brow and tear - sideways on its own.
cx: layout.cx + clamp(spec.eyeX ?? 0, -400, 400),
@@ -1351,14 +1369,35 @@ export function drawEyes(ctx, p) {
towardNose: layout.towardNose,
};
const lineStyle = { line: lineColor, width: lidWidth };
- const isHeart = (spec.irisShape ?? 'circle') === 'heart';
- // The lower lid is per eye (falling back to the shared value).
+ // Which artwork this eye shows. `shape` is the explicit choice; older saved
+ // states and the おまかせ rolls only set `open`/`closed`, so fall back.
+ const shapeKey = eyeShapeKey(spec);
+ const explicitShut = typeof spec.shape === 'string'
+ && (spec.shape === 'three' || spec.shape === 'arch' || spec.shape.startsWith('line'));
+ const isHeart = shapeKey === 'heart';
+ const isShutShape = shapeKey === 'three' || shapeKey === 'arch' || shapeKey.startsWith('line');
+ // The shut artwork reads `closed`/`closedLines`; build them from `shape` so a
+ // line, the 3 and the arch share one path with the older fields.
+ const shapeSpec = isShutShape
+ ? {
+ ...spec,
+ closed: shapeKey === 'three' ? 'three' : (shapeKey === 'arch' ? 'arch' : 'line'),
+ closedLines: shapeKey.startsWith('line') ? Number(shapeKey.slice(4)) : (spec.closedLines ?? 1),
+ }
+ : spec;
+ // The upper lid (`open`) and the lower lid are independent, and both shape
+ // every eye now: the shut artwork is drawn as the eye's content and is
+ // trimmed by them. A shut eye saved the old way stored open = 0 meaning
+ // "shut"; lift it or the artwork would be clipped away. An explicit shut
+ // `shape` keeps its own lid height, so the lid can be lowered onto it.
+ const rawOpen = spec.open ?? 1;
+ const open = clamp(explicitShut ? rawOpen : (rawOpen <= 0.02 ? 1 : rawOpen), 0, 1);
const lowerLid = clamp(spec.lowerLid ?? style.lowerLid ?? 0, 0, 1);
- // 形の大きさ: scales the drawn shape (shut lines, the 3, the arch, the heart).
- const shapeScale = clamp(spec.shapeScale ?? 1, 0.4, 2);
- // `white` is tri-state: an open eye shows its white, a shut eye or a heart
- // does not, unless the switch forces it one way.
- const showWhite = spec.white != null ? spec.white === true : (open > 0.02 && !isHeart);
+ // 大きさ: one knob for the iris, the heart and the shut artwork.
+ const shapeScale = clamp(style.irisScale ?? 1, 0.4, 2);
+ const irisRadius = EYE_LAYOUT.irisRadius * shapeScale;
+ // `white` is tri-state: only the plain open eye shows it by default.
+ const showWhite = spec.white != null ? spec.white === true : shapeKey === 'open';
// While the white is shown, a shape that would poke out of the eyeball is
// scaled back inside it; `shapeHalfLimit` is the half-size it may reach.
const shapeHalfLimit = showWhite ? eye.r * 0.92 : Infinity;
@@ -1399,9 +1438,11 @@ export function drawEyes(ctx, p) {
ctx.restore();
};
- // Where the pupil is looking. Every eye shape shares it: a shut eye's line, a
- // 3 or an arch shift with the gaze too, not just the round eyeball.
- const eyeTravel = isHeart ? heartTravel * lookMax : travel * lookMax;
+ // Where the pupil is looking. Every eye shape shares it: the shut artwork
+ // shifts with the gaze too, not just the round eyeball.
+ const eyeTravel = isHeart
+ ? heartTravel * lookMax
+ : Math.max(0, EYE_LAYOUT.radius - irisRadius) * lookMax;
let dx = clamp(spec.lookX ?? 0, -1, 1) * eyeTravel;
let dy = -clamp(spec.lookY ?? 0, -1, 1) * eyeTravel;
const dist = Math.hypot(dx, dy);
@@ -1410,64 +1451,48 @@ export function drawEyes(ctx, p) {
dy = (dy / dist) * eyeTravel;
}
- if (open <= 0.02 || open <= lowerLid + 0.02) {
- // A shut eye can still be given a white backing (the "白目" switch), which
- // reads as a line drawn across an eyeball rather than a bare line. This also
- // catches a lower lid raised past the upper one, which reads as shut too.
- if (!line && showWhite) fillCircle(ctx, eye.cx, eye.cy, eye.r, white);
- // 目線 moves the drawn shape as well, so a shut eye can glance sideways.
- ctx.save();
- ctx.translate(dx, dy);
- drawShutEye(ctx, eye, spec, lineStyle, shapeScale, shapeHalfLimit);
- ctx.restore();
- // The glasses sit over the shut-eye artwork too, but under the tear below.
- drawGlasses(ctx, eye, line, style, limits);
- drawTear();
- continue;
- }
-
- // --- eyeball -------------------------------------------------------
+ // --- the eye's content, clipped by the lids -------------------------
+ // Every shape is drawn the same way now: a white backing, then the artwork
+ // (iris, heart or the shut line/3/arch), all trimmed by the upper and lower
+ // lids. The shut artwork reaches past the eyeball, so it is only lid-clipped.
ctx.save();
- // A heart is not an eyeball, so once the eye is fully open it is not clipped
- // to the eyeball circle - that is what lets a heart be much larger than the
- // iris. While the lid is partly closed the clip is still needed.
- if (!isHeart || open < 0.999) clipEye(ctx, eye, open, isHeart ? 0 : lowerLid, lidFlat, lidTilt * eye.towardNose);
-
- // The white of the eye (see `showWhite` above).
+ if (isShutShape) {
+ clipLids(ctx, eye, open, lowerLid, lidFlat, lidTilt * eye.towardNose);
+ } else if (!isHeart || open < 0.999) {
+ clipEye(ctx, eye, open, isHeart ? 0 : lowerLid, lidFlat, lidTilt * eye.towardNose);
+ }
if (!line && showWhite) fillCircle(ctx, eye.cx, eye.cy, eye.r, white);
- const irisX = eye.cx + dx;
- const irisY = eye.cy + dy;
- if (isHeart) {
- // A heart is wider than a circle, so cap it at the plane rather than the
- // eyeball; a red heart has no pupil highlight (it would sit on top of it).
- // A heart is not an eyeball, so it may grow well past the eye: the plate
- // keeps the whole canvas, and the eyes plate is drawn over the mouth, so a
- // big heart has room. The cap only stops it from running off the canvas.
- // With the white shown, the default size is the one that just fits inside
- // the eyeball; 形の大きさ can still grow it past the white.
- const heartFit = showWhite ? Math.min(1, shapeHalfLimit / (irisRadius * heartScale)) : 1;
- const heartRadius = Math.min(irisRadius * heartScale * shapeScale * heartFit, EYE_LAYOUT.radius * 2.1);
- fillHeart(ctx, irisX, irisY, heartRadius, line ? lineColor : heartColor);
+ if (isShutShape) {
+ ctx.translate(dx, dy);
+ drawShutEye(ctx, eye, shapeSpec, lineStyle, shapeScale, shapeHalfLimit);
+ } else if (isHeart) {
+ // A heart is not an eyeball, so it may grow past the eye up to the plate.
+ // With the white shown, its default size is what just fits inside it; the
+ // single 大きさ knob can still grow it past the white.
+ const heartFit = showWhite ? Math.min(1, shapeHalfLimit / (EYE_LAYOUT.irisRadius * heartScale)) : 1;
+ const heartRadius = Math.min(EYE_LAYOUT.irisRadius * heartScale * shapeScale * heartFit, EYE_LAYOUT.radius * 2.1);
+ fillHeart(ctx, eye.cx + dx, eye.cy + dy, heartRadius, line ? lineColor : heartColor);
} else {
+ const irisX = eye.cx + dx;
+ const irisY = eye.cy + dy;
fillCircle(ctx, irisX, irisY, irisRadius, line ? lineColor : irisColor);
- }
- // 光彩 (the white glint) is per eye, falling back to the shared switch.
- const highlightOn = spec.highlight != null ? spec.highlight === true : style.highlight !== false;
- if (highlightOn && !isHeart) {
- const hx = irisX + layout.towardNose * EYE_LAYOUT.highlightOffset.x * irisScale;
- const hy = irisY + EYE_LAYOUT.highlightOffset.y * irisScale;
- const hr = EYE_LAYOUT.highlightRadius * irisScale;
- if (line) {
- // Punch the sparkle out of the iris rather than painting it white. On
- // screen the paper behind shows through, and in the SVG export (which
- // only sees alpha) it stays a hole in the pupil - both of which read as
- // the white glint of the original artwork.
- ctx.save();
- ctx.globalCompositeOperation = 'destination-out';
- fillCircle(ctx, hx, hy, hr, '#000000');
- ctx.restore();
- } else {
- fillCircle(ctx, hx, hy, hr, white);
+ // 光彩 (the white glint) is per eye, falling back to the shared switch.
+ const highlightOn = spec.highlight != null ? spec.highlight === true : style.highlight !== false;
+ if (highlightOn) {
+ const hx = irisX + layout.towardNose * EYE_LAYOUT.highlightOffset.x * shapeScale;
+ const hy = irisY + EYE_LAYOUT.highlightOffset.y * shapeScale;
+ const hr = EYE_LAYOUT.highlightRadius * shapeScale;
+ if (line) {
+ // Punch the sparkle out of the iris rather than painting it white. On
+ // screen the paper behind shows through, and in the SVG export (which
+ // only sees alpha) it stays a hole in the pupil.
+ ctx.save();
+ ctx.globalCompositeOperation = 'destination-out';
+ fillCircle(ctx, hx, hy, hr, '#000000');
+ ctx.restore();
+ } else {
+ fillCircle(ctx, hx, hy, hr, white);
+ }
}
}
ctx.restore();
diff --git a/public/bluebey-studio/src/gacha.js b/public/bluebey-studio/src/gacha.js
index d689fa5..c44f80f 100644
--- a/public/bluebey-studio/src/gacha.js
+++ b/public/bluebey-studio/src/gacha.js
@@ -237,6 +237,8 @@ function rollEyes(random, spec) {
const [openMin, openMax] = spec.open;
let leftOpen = round2(rand(random, openMin, openMax));
let rightOpen = round2(clamp(leftOpen + rand(random, -0.06, 0.06), 0, 1));
+ let leftShape = 'open';
+ let rightShape = 'open';
let leftClosed = 'line';
let rightClosed = 'line';
let leftLines = 1;
@@ -245,28 +247,37 @@ function rollEyes(random, spec) {
if (spec.wink) {
const side = chance(random, 0.5) ? 'left' : 'right';
const lines = pick(random, [2, 3]);
+ // A shut eye is the artwork with the lids open now; the lid height is its own
+ // slider, so `open` is 1 and the shape says which line it is.
if (side === 'left') {
- leftOpen = 0;
+ leftOpen = 1;
leftLines = lines;
+ leftShape = `line${lines}`;
} else {
- rightOpen = 0;
+ rightOpen = 1;
rightLines = lines;
+ rightShape = `line${lines}`;
}
} else if (spec.shutBoth && chance(random, spec.shutBoth.chance)) {
- leftOpen = 0;
- rightOpen = 0;
+ leftOpen = 1;
+ rightOpen = 1;
leftClosed = spec.shutBoth.closed;
rightClosed = spec.shutBoth.closed;
leftLines = spec.shutBoth.closedLines;
rightLines = spec.shutBoth.closedLines;
+ const shape = spec.shutBoth.closed === 'line'
+ ? `line${spec.shutBoth.closedLines}`
+ : spec.shutBoth.closed;
+ leftShape = shape;
+ rightShape = shape;
}
const lookX = round2(rand(random, spec.lookX[0], spec.lookX[1]));
const lookY = round2(rand(random, spec.lookY[0], spec.lookY[1]));
return {
- left: { open: leftOpen, lookX, lookY, closed: leftClosed, closedLines: leftLines, irisShape: 'circle' },
- right: { open: rightOpen, lookX, lookY, closed: rightClosed, closedLines: rightLines, irisShape: 'circle' },
+ left: { shape: leftShape, open: leftOpen, lookX, lookY, closed: leftClosed, closedLines: leftLines, irisShape: 'circle' },
+ right: { shape: rightShape, open: rightOpen, lookX, lookY, closed: rightClosed, closedLines: rightLines, irisShape: 'circle' },
irisScale: round2(rand(random, spec.irisScale[0], spec.irisScale[1])),
lookMax: round2(rand(random, spec.lookMax[0], spec.lookMax[1])),
highlight: true,
@@ -330,6 +341,8 @@ export function randomFace(random) {
if (mood.heart && eyes.left.open >= 0.9 && eyes.right.open >= 0.9) {
eyes.left.irisShape = 'heart';
eyes.right.irisShape = 'heart';
+ eyes.left.shape = 'heart';
+ eyes.right.shape = 'heart';
eyes.heartScale = round2(rand(random, 0.9, 1.15));
eyes.heartColor = '#e0344f';
eyes.highlight = false;
diff --git a/public/bluebey-studio/src/panel.js b/public/bluebey-studio/src/panel.js
index b942c22..bc8e243 100644
--- a/public/bluebey-studio/src/panel.js
+++ b/public/bluebey-studio/src/panel.js
@@ -457,6 +457,7 @@ export function buildPanel(app, root) {
state.face.eyes.right.closed = state.face.eyes.left.closed;
state.face.eyes.right.closedLines = state.face.eyes.left.closedLines;
state.face.eyes.right.irisShape = state.face.eyes.left.irisShape;
+ state.face.eyes.right.shape = state.face.eyes.left.shape;
state.face.eyes.right.white = state.face.eyes.left.white ?? null;
state.face.eyes.right.highlight = state.face.eyes.left.highlight ?? null;
state.face.eyes.right.lookX = state.face.eyes.left.lookX;
@@ -482,15 +483,19 @@ export function buildPanel(app, root) {
// The shut line's stroke count is part of the shape itself.
return `line${Math.min(3, Math.max(1, Math.round(eye.closedLines ?? 1)))}`;
}
+ if (eye.shape) return eye.shape;
return (eye.irisShape ?? 'circle') === 'heart' ? 'heart' : 'open';
};
const applyEyeShape = (eye, value) => {
+ eye.shape = value;
eye.irisShape = value === 'heart' ? 'heart' : 'circle';
if (value === 'open' || value === 'heart') {
eye.open = 1;
return;
}
- eye.open = 0;
+ // A shut artwork is drawn as the eye's content with the lids open; the lid
+ // height is its own slider (上まぶたの高さ), so selecting one opens the lid.
+ eye.open = 1;
if (value.startsWith('line')) {
eye.closed = 'line';
eye.closedLines = Number(value.slice(4)) || 1;
@@ -535,21 +540,8 @@ export function buildPanel(app, root) {
label: '下まぶたの高さ', min: 0, max: 1, step: 0.01, value: 0, format: (v) => v.toFixed(2),
onInput: (v) => { write((e) => { e.lowerLid = v; }); touched(); },
});
- // まぶたの形 and tilt are whole-face settings (the drawing mirrors the tilt
- // between the eyes), so every group shows the same pair, right above 目の形.
- const lidShape = segmented({
- label: 'まぶたの形',
- options: [
- { value: 'curve', label: '曲線' },
- { value: 'flat', label: '直線(平ら)' },
- ],
- value: 'curve',
- onChange: (value) => { state.face.eyes.lidShape = value; touched(); },
- });
- const lidTilt = slider({
- label: 'まぶたの傾き', min: -40, max: 40, step: 1, value: 0, format: (v) => `${Math.round(v)}°`,
- onInput: (value) => { state.face.eyes.lidTilt = value; touched(); },
- });
+ // 目の形: the artwork the eye shows. A shut shape is drawn with the lids open,
+ // so 上まぶたの高さ can then be lowered onto it.
const shape = segmented({
label: '目の形',
options: [
@@ -564,9 +556,17 @@ export function buildPanel(app, root) {
value: 'open',
onChange: (v) => { write((e) => applyEyeShape(e, v)); touched(); },
});
- const shapeScale = slider({
- label: '形の大きさ', min: 0.5, max: 1.8, step: 0.01, value: 1, format: (v) => v.toFixed(2),
- onInput: (v) => { write((e) => { e.shapeScale = v; }); touched(); },
+ // The heart colour only matters for the heart, so it sits right under 目の形
+ // and shows only then.
+ const heartColor = colorField({
+ label: 'ハートの色', value: '#e0344f',
+ swatches: ['#e0344f', '#ff5f8f', '#c2185b', '#150e1b'],
+ onChange: (v) => { state.face.eyes.heartColor = v; touched(); },
+ });
+ // 大きさ: one knob for the iris, the heart and the shut artwork.
+ const size = slider({
+ label: '大きさ', min: 0.4, max: 1.8, step: 0.01, value: 1, format: (v) => v.toFixed(2),
+ onInput: (v) => { state.face.eyes.irisScale = v; touched(); },
});
const threeFlip = segmented({
label: '3の向き',
@@ -628,10 +628,9 @@ export function buildPanel(app, root) {
group.add(open);
group.add(lowerLid);
- group.add(lidShape);
- group.add(lidTilt);
group.add(shape);
- group.add(shapeScale);
+ group.add(heartColor);
+ group.add(size);
group.add(threeFlip);
group.add(controlRow(null, white.el, { wide: true }));
group.add(controlRow(null, highlight.el, { wide: true }));
@@ -648,10 +647,10 @@ export function buildPanel(app, root) {
const shapeValue = eyeShapeOf(e);
open.set(e.open);
lowerLid.set(e.lowerLid ?? state.face.eyes.lowerLid ?? 0);
- lidShape.set(state.face.eyes.lidShape ?? 'curve');
- lidTilt.set(state.face.eyes.lidTilt ?? 0);
shape.set(shapeValue);
- shapeScale.set(e.shapeScale ?? 1);
+ heartColor.set(state.face.eyes.heartColor ?? '#e0344f');
+ heartColor.el.style.display = shapeValue === 'heart' ? '' : 'none';
+ size.set(state.face.eyes.irisScale ?? 1);
threeFlip.set(e.threeFlip ? 'flip' : 'normal');
threeFlip.el.style.display = shapeValue === 'three' ? '' : 'none';
white.set(eyeWhiteOf(e));
@@ -674,15 +673,40 @@ export function buildPanel(app, root) {
const leftControls = makeEyeControls('left');
const rightControls = makeEyeControls('right');
+ // --- まぶた (whole-face lid settings, shown above the per-eye groups) -------
+ const lidGroup = eyeGroup('まぶた');
+ const lidShapeSegment = segmented({
+ label: 'まぶたの形',
+ options: [
+ { value: 'curve', label: '曲線' },
+ { value: 'flat', label: '直線(平ら)' },
+ ],
+ value: 'curve',
+ onChange: (value) => { state.face.eyes.lidShape = value; app.actions.refresh('face'); },
+ });
+ const lidWidthSlider = slider({
+ label: 'まぶたの太さ', min: 0, max: 30, step: 1, value: 14, format: (v) => `${Math.round(v)}`,
+ onInput: (value) => { state.face.eyes.lidWidth = value; app.actions.refresh('face'); },
+ });
+ const lidTiltSlider = slider({
+ label: 'まぶたの傾き', min: -40, max: 40, step: 1, value: 0, format: (v) => `${Math.round(v)}°`,
+ onInput: (value) => { state.face.eyes.lidTilt = value; app.actions.refresh('face'); },
+ });
+ lidGroup.add(lidShapeSegment.el);
+ lidGroup.add(lidWidthSlider.el);
+ lidGroup.add(lidTiltSlider.el);
+ lidGroup.add(hint('「直線(平ら)」にすると、まぶたが平らに下りてきます。'
+ + '「まぶたの傾き」で、細めた目を左右反転で傾けられます。'));
+
faceSection.add(controlRow(null, linkedToggle.el, { wide: true }));
+ faceSection.add(lidGroup);
faceSection.add(bothControls.group);
faceSection.add(leftControls.group);
faceSection.add(rightControls.group);
- faceSection.add(hint('「まぶたの形」を「直線(平ら)」にすると、まぶたが平らに下りてきます。'
- + '「まぶたの傾き」で、細めた目を左右反転で傾けられます。'
- + '「目の形」の「2線」「3線」は、端点がつながった形になります。'));
+ faceSection.add(hint('「目の形」の「2線」「3線」は端点がつながった形になります。'
+ + '閉じ目の形も、上まぶた・下まぶたで上下から隠せます。'));
- // 横位置 (per eye) rarely changes, so it lives in こまかい設定.
+ // 横位置 (per eye) rarely changes, so it gets its own small box.
const eyeAdvancedWidgets = {};
for (const key of ['left', 'right']) {
const label = key === 'left' ? '左の目' : '右の目';
@@ -710,29 +734,9 @@ export function buildPanel(app, root) {
}],
}));
- faceSection.add(hint('「左右を連動させる」を入れると「両目」の設定が1つだけ出て、上まぶた・下まぶた・まぶたの形・形・白目・光彩・目線が両目でそろいます。'
+ faceSection.add(hint('「左右を連動させる」を入れると「両目」の設定が1つだけ出て、上まぶた・下まぶた・目の形・大きさ・白目・光彩・目線が両目でそろいます。'
+ 'はずすと「左の目」「右の目」の設定がそれぞれに出て、目線のパッドも2つになります。'));
- const eyeAdvanced = details(faceSection.body, 'こまかい設定(瞳・色・まぶたの太さ・左右べつの設定・手描き)');
- const irisScaleSlider = slider({
- label: '瞳の大きさ', min: 0.4, max: 1.4, step: 0.01, value: 1, format: (v) => v.toFixed(2),
- onInput: (value) => { state.face.eyes.irisScale = value; app.actions.refresh('face'); },
- });
- const heartColorField = colorField({
- label: 'ハートの色', value: '#e0344f',
- swatches: ['#e0344f', '#ff5f8f', '#c2185b', '#150e1b'],
- onChange: (v) => { state.face.eyes.heartColor = v; app.actions.refresh('face'); },
- });
- const lidWidthSlider = slider({
- label: 'まぶたの太さ', min: 0, max: 30, step: 1, value: 14, format: (v) => `${Math.round(v)}`,
- onInput: (value) => { state.face.eyes.lidWidth = value; app.actions.refresh('face'); },
- });
- eyeAdvanced.add(irisScaleSlider.el);
- eyeAdvanced.add(heartColorField.el);
- eyeAdvanced.add(lidWidthSlider.el);
- eyeAdvanced.add(hint('「まぶたの形」「まぶたの傾き」「白目」「光彩」は、この上の「目」にあります。'
- + 'ハートの大きさは「瞳の大きさ」で変わります。'));
-
const eyeColors = {
white: colorField({ label: '白目', value: '#ffffff', onChange: (v) => { state.face.eyes.white = v; app.actions.refresh('face'); } }),
iris: colorField({ label: '瞳', value: '#150e1b', onChange: (v) => { state.face.eyes.iris = v; app.actions.refresh('face'); } }),
@@ -741,8 +745,8 @@ export function buildPanel(app, root) {
const eyeColorBox = details(faceSection.body, '色(白目・瞳・まぶたの線)');
for (const widget of Object.values(eyeColors)) eyeColorBox.add(widget.el);
- // 横位置 (per eye) lives here with the other extras.
- const tearBox = details(eyeAdvanced.body, '横位置(左右べつ)');
+ // 横位置 (per eye) is rarely used, so it stays in its own small box.
+ const tearBox = details(faceSection.body, '横位置(左右べつ)');
for (const el of tearWidgets) tearBox.add(el);
const eyeSource = selectField({
@@ -756,7 +760,9 @@ export function buildPanel(app, root) {
value: 'parametric',
onChange: (value) => { state.face.eyes.source = value; app.actions.refresh('face'); },
});
- eyeAdvanced.add(eyeSource.el);
+ // 手描きの目(パーツの選択と読み込み)は、大きさやまぶたとは別の箱にまとめる。
+ const eyeArtBox = details(faceSection.body, '手描きの目(パーツ・読み込み)');
+ eyeArtBox.add(eyeSource.el);
// 手描き画像の読み込み(GIMP などで描いた PNG をそのまま使えます)
const rebuildEyeOptions = () => {
@@ -793,8 +799,8 @@ export function buildPanel(app, root) {
toast('画像を読み込めませんでした(PNG / JPG をお使いください)');
}
});
- eyeAdvanced.add(eyeImagePicker);
- eyeAdvanced.add(buttons({
+ eyeArtBox.add(eyeImagePicker);
+ eyeArtBox.add(buttons({
items: [{ id: 'eyeimg', label: '目の画像を読み込む(PNG)', onClick: () => { if (confirmImageLicense()) eyeImagePicker.click(); } }],
}));
@@ -2760,9 +2766,9 @@ export function buildPanel(app, root) {
w.eyeX.set(eye.eyeX ?? 0);
}
- irisScaleSlider.set(state.face.eyes.irisScale);
- heartColorField.set(state.face.eyes.heartColor ?? '#e0344f');
lidWidthSlider.set(state.face.eyes.lidWidth ?? 14);
+ lidShapeSegment.set(state.face.eyes.lidShape ?? 'curve');
+ lidTiltSlider.set(state.face.eyes.lidTilt ?? 0);
browToggle.set(state.face.eyes.brow.enabled);
browSliders.angle.set(state.face.eyes.brow.angle);
browSliders.height.set(state.face.eyes.brow.height);
diff --git a/public/bluebey-studio/src/presets.js b/public/bluebey-studio/src/presets.js
index ca8f048..a8b10ec 100644
--- a/public/bluebey-studio/src/presets.js
+++ b/public/bluebey-studio/src/presets.js
@@ -200,8 +200,8 @@ export function defaultState() {
eyes: {
source: 'parametric', // parametric | opened | closed | ... (hand-drawn)
linked: true, // move both eyes together
- left: { open: 1, lookX: 0, lookY: 0, eyeX: 0, closed: 'line', closedLines: 1, irisShape: 'circle', threeFlip: false, tear: 0.3, tearOn: false, tearY: 0, tearX: 0, tearTilt: 0, white: null, highlight: null, shapeScale: 1, lowerLid: null },
- right: { open: 1, lookX: 0, lookY: 0, eyeX: 0, closed: 'line', closedLines: 1, irisShape: 'circle', threeFlip: false, tear: 0.3, tearOn: false, tearY: 0, tearX: 0, tearTilt: 0, white: null, highlight: null, shapeScale: 1, lowerLid: null },
+ left: { shape: 'open', open: 1, lookX: 0, lookY: 0, eyeX: 0, closed: 'line', closedLines: 1, irisShape: 'circle', threeFlip: false, tear: 0.3, tearOn: false, tearY: 0, tearX: 0, tearTilt: 0, white: null, highlight: null, shapeScale: 1, lowerLid: null },
+ right: { shape: 'open', open: 1, lookX: 0, lookY: 0, eyeX: 0, closed: 'line', closedLines: 1, irisShape: 'circle', threeFlip: false, tear: 0.3, tearOn: false, tearY: 0, tearX: 0, tearTilt: 0, white: null, highlight: null, shapeScale: 1, lowerLid: null },
irisScale: 1,
lookMax: 1.2,
highlight: true,
@@ -407,8 +407,8 @@ export const FACE_PRESETS = [
label: 'にっこり',
face: {
eyes: {
- left: { open: 0, lookX: 0, lookY: 0, closed: 'arch' },
- right: { open: 0, lookX: 0, lookY: 0, closed: 'arch' },
+ left: { shape: 'arch', open: 1, lookX: 0, lookY: 0, closed: 'arch' },
+ right: { shape: 'arch', open: 1, lookX: 0, lookY: 0, closed: 'arch' },
highlight: false,
},
// A laugh is a wide smile with squeezed-shut eyes. An *open* mouth on a
@@ -437,7 +437,7 @@ export const FACE_PRESETS = [
eyes: {
// The shut eye is the original `eyes-close-tight` artwork: three strokes
// sharing one vertex, pointing at the nose.
- left: { open: 0, lookX: 0, lookY: 0, closed: 'line', closedLines: 3 },
+ left: { shape: 'line3', open: 1, lookX: 0, lookY: 0, closed: 'line', closedLines: 3 },
right: { open: 1, lookX: 0, lookY: 0, closed: 'line', closedLines: 1 },
irisScale: 1,
highlight: true,
@@ -513,8 +513,8 @@ export const FACE_PRESETS = [
label: 'すき',
face: {
eyes: {
- left: { open: 1, lookX: -0.1, lookY: -0.3, closed: 'line', irisShape: 'heart', white: false },
- right: { open: 1, lookX: 0.1, lookY: -0.3, closed: 'line', irisShape: 'heart', white: false },
+ left: { shape: 'heart', open: 1, lookX: -0.1, lookY: -0.3, closed: 'line', irisShape: 'heart', white: false },
+ right: { shape: 'heart', open: 1, lookX: 0.1, lookY: -0.3, closed: 'line', irisShape: 'heart', white: false },
irisScale: 1,
// The white behind a heart would poke out around its lobes, so hide it.
heartColor: '#e0344f',