aboutsummaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
-rw-r--r--public/bluebey-studio/src/faceArt.js37
-rw-r--r--public/bluebey-studio/src/gacha.js6
-rw-r--r--public/bluebey-studio/src/panel.js79
-rw-r--r--public/bluebey-studio/src/presets.js10
4 files changed, 54 insertions, 78 deletions
diff --git a/public/bluebey-studio/src/faceArt.js b/public/bluebey-studio/src/faceArt.js
index f61a865..9e07ac6 100644
--- a/public/bluebey-studio/src/faceArt.js
+++ b/public/bluebey-studio/src/faceArt.js
@@ -503,10 +503,12 @@ function clipEye(ctx, eye, open, lowerLid, flat = false, tiltDeg = 0) {
if (rotated) spin();
const lower = clamp(lowerLid, 0, 1);
- // The upper lid travels all the way down to the lower lid's resting line, so
- // the eye shuts at open = 0 whatever the lower lid is doing: a blink closes
- // fully even on a squint.
- const upper = (1 - lower) * (1 - clamp(open, 0, 1));
+ // The upper and lower lids are independent: 上まぶたの高さ sets where the upper
+ // lid sits on its own, so raising 下まぶたの高さ does not drag the upper lid
+ // down with it. A blink still shuts the eye because `open` reaching 0 (or the
+ // upper lid dropping to the lower one) hands over to the shut-eye drawing
+ // before this clip is used.
+ const upper = 1 - clamp(open, 0, 1);
if (upper > 0.0005) {
const rl = eye.r * EYE_LAYOUT.lidRadiusFactor;
const lowest = eye.cy - eye.r + 2 * eye.r * upper;
@@ -580,9 +582,12 @@ function lidPath(ctx, eye, amount, lower, flat = false) {
function drawShutEye(ctx, eye, spec, style, shapeScale = 1, maxHalf = Infinity) {
const layout = EYE_LAYOUT.closedLine;
const { line, width } = style;
- // 形の大きさ, clamped so the shape stays inside the white of the eye when it is
- // shown (`maxHalf` is Infinity otherwise).
- const k = (unitHalf) => Math.min(shapeScale, maxHalf / unitHalf);
+ // 形の大きさ is a multiplier on the shape's *fitting* size. With the white shown
+ // that fitting size is what just fits inside the eyeball, so the default (1)
+ // sits in the white; raising it may push the shape out past the white, which is
+ // allowed. `maxHalf` is Infinity when the white is hidden, so nothing is scaled
+ // back there.
+ const k = (unitHalf) => shapeScale * (maxHalf === Infinity ? 1 : Math.min(1, maxHalf / unitHalf));
if (spec.closed === 'chevron') {
// The chevron points *at* the nose, matching the original artwork.
@@ -1394,9 +1399,10 @@ export function drawEyes(ctx, p) {
ctx.restore();
};
- if (open <= 0.02) {
+ 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.
+ // 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);
drawShutEye(ctx, eye, spec, lineStyle, shapeScale, shapeHalfLimit);
// The glasses sit over the shut-eye artwork too, but under the tear below.
@@ -1431,9 +1437,10 @@ export function drawEyes(ctx, p) {
// 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.
- // When the white is shown, it is capped to the eyeball instead.
- let heartRadius = Math.min(irisRadius * heartScale * shapeScale, EYE_LAYOUT.radius * 2.1);
- if (showWhite) heartRadius = Math.min(heartRadius, shapeHalfLimit);
+ // 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);
} else {
fillCircle(ctx, irisX, irisY, irisRadius, line ? lineColor : irisColor);
@@ -1472,9 +1479,9 @@ export function drawEyes(ctx, p) {
// The tilt is mirrored between the eyes, so a positive value reads the same
// way on both (an inward, angry-looking squint).
const eyeTilt = lidTilt * eye.towardNose;
- // The upper lid line has to sit where the clip put it: it travels from the
- // top of the eye down to the lower lid's resting line.
- const upperAmt = lowerLid + open * (1 - lowerLid);
+ // The upper lid line has to sit where the clip put it - and that is set by
+ // `open` alone, so it stays put when the lower lid moves.
+ const upperAmt = clamp(open, 0, 1);
if (lidFade > 0.01 || lowerLid > 0.01) {
ctx.save();
if (eyeTilt) {
diff --git a/public/bluebey-studio/src/gacha.js b/public/bluebey-studio/src/gacha.js
index b1df7a4..d689fa5 100644
--- a/public/bluebey-studio/src/gacha.js
+++ b/public/bluebey-studio/src/gacha.js
@@ -167,7 +167,7 @@ const MOODS = [
corners: [0, 0.1], cornerAngle: [-4, 4], tilt: [-5, 5], offsetY: [6, 16],
tongue: [0, 0.1],
},
- lowerLid: [0.3, 0.5],
+ lowerLid: [0.16, 0.28],
tear: [0.6, 1.2],
tearChance: 1,
},
@@ -182,7 +182,7 @@ const MOODS = [
corners: [0, 0.05], cornerAngle: [-6, 6], tilt: [-6, 6], offsetY: [8, 18],
tongue: [0, 0.05],
},
- lowerLid: [0.15, 0.35],
+ lowerLid: [0.08, 0.2],
},
{
id: 'love',
@@ -208,7 +208,7 @@ const MOODS = [
corners: [0.5, 0.9], cornerAngle: [-4, 4], tilt: [-7, -1], offsetY: [4, 13],
tongue: [0.3, 0.8],
},
- lowerLid: [0.2, 0.45],
+ lowerLid: [0.1, 0.24],
tear: [0.2, 0.5],
tearChance: 0.5,
shutBoth: { chance: 0.4, closed: 'line', closedLines: 1 },
diff --git a/public/bluebey-studio/src/panel.js b/public/bluebey-studio/src/panel.js
index 9913982..2809269 100644
--- a/public/bluebey-studio/src/panel.js
+++ b/public/bluebey-studio/src/panel.js
@@ -531,6 +531,21 @@ 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(); },
+ });
const shape = segmented({
label: '目の形',
options: [
@@ -618,6 +633,8 @@ 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(closedLines);
@@ -637,6 +654,8 @@ 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);
closedLines.set(String(e.closedLines ?? 1));
@@ -667,6 +686,8 @@ export function buildPanel(app, root) {
faceSection.add(bothControls.group);
faceSection.add(leftControls.group);
faceSection.add(rightControls.group);
+ faceSection.add(hint('「まぶたの形」を「直線(平ら)」にすると、まぶたが平らに下りてきます。'
+ + '「まぶたの傾き」で、細めた目を左右反転で傾けられます。'));
// 横位置 (per eye) rarely changes, so it lives in こまかい設定.
const eyeAdvancedWidgets = {};
@@ -680,58 +701,6 @@ export function buildPanel(app, root) {
tearWidgets.push(eyeX.el);
}
- // 目を細める (and its undo) grouped with the lid shape and tilt that shape the
- // squint. It sits right under the lid-height sliders it drives.
- const squintGroup = eyeGroup('目を細める');
- squintGroup.add(buttons({
- items: [
- {
- id: 'squint',
- label: '目を細める',
- onClick: () => {
- // The upper lid comes partway down and the lower lid rises to meet it.
- // Both lids are per eye, so both eyes are set here.
- state.face.eyes.left.open = 0.5;
- state.face.eyes.right.open = 0.5;
- state.face.eyes.left.lowerLid = 0.5;
- state.face.eyes.right.lowerLid = 0.5;
- app.actions.refresh('face');
- sync();
- },
- },
- {
- id: 'unsquint',
- label: '目を細めない',
- onClick: () => {
- state.face.eyes.left.open = 1;
- state.face.eyes.right.open = 1;
- state.face.eyes.left.lowerLid = 0;
- state.face.eyes.right.lowerLid = 0;
- app.actions.refresh('face');
- sync();
- },
- },
- ],
- }));
- 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 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'); },
- });
- squintGroup.add(lidShapeSegment.el);
- squintGroup.add(lidTiltSlider.el);
- squintGroup.add(hint('「まぶたの形」を「直線」にすると、まぶたが平らに下りてきます。'
- + '「まぶたの傾き」で、細めた目を左右反転で傾けられます。'));
- faceSection.add(squintGroup);
-
// One button to put every eye setting back to the artwork's defaults. The
// artwork *source* is kept: resetting the numbers should not throw away a
// hand-drawn image the user loaded.
@@ -748,7 +717,7 @@ export function buildPanel(app, root) {
}],
}));
- faceSection.add(hint('「左右を連動させる」を入れると「両目」の設定が1つだけ出て、上まぶた・下まぶた・形・白目・光彩・目線が両目でそろいます。'
+ faceSection.add(hint('「左右を連動させる」を入れると「両目」の設定が1つだけ出て、上まぶた・下まぶた・まぶたの形・形・白目・光彩・目線が両目でそろいます。'
+ 'はずすと「左の目」「右の目」の設定がそれぞれに出て、目線のパッドも2つになります。'));
const eyeAdvanced = details(faceSection.body, 'こまかい設定(瞳・色・まぶたの太さ・左右べつの設定・手描き)');
@@ -768,7 +737,7 @@ export function buildPanel(app, root) {
eyeAdvanced.add(irisScaleSlider.el);
eyeAdvanced.add(heartColorField.el);
eyeAdvanced.add(lidWidthSlider.el);
- eyeAdvanced.add(hint('「まぶたの形」「まぶたの傾き」「目を細める」「白目」「光彩」は、この上の「目」にあります。'
+ eyeAdvanced.add(hint('「まぶたの形」「まぶたの傾き」「白目」「光彩」は、この上の「目」にあります。'
+ 'ハートの大きさは「瞳の大きさ」で変わります。'));
const eyeColors = {
@@ -2801,8 +2770,6 @@ export function buildPanel(app, root) {
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 66f4fab..ca8f048 100644
--- a/public/bluebey-studio/src/presets.js
+++ b/public/bluebey-studio/src/presets.js
@@ -495,8 +495,10 @@ export const FACE_PRESETS = [
face: {
eyes: {
// Flat, narrowed lids tilted inwards - a sideways, unimpressed look.
- left: { open: 0.55, lookX: 0.4, lookY: -0.12, closed: 'line' },
- right: { open: 0.55, lookX: 0.4, lookY: -0.12, closed: 'line' },
+ // 上まぶたの高さ is now independent of the lower lid, so `open` carries the
+ // whole slit (it used to be measured from the raised lower lid).
+ left: { open: 0.74, lookX: 0.4, lookY: -0.12, closed: 'line' },
+ right: { open: 0.74, lookX: 0.4, lookY: -0.12, closed: 'line' },
lowerLid: 0.42,
lidShape: 'flat',
lidTilt: 12,
@@ -542,8 +544,8 @@ export const FACE_PRESETS = [
// Big tears that hang low and lean outwards - the classic "crying hard"
// look. The sideways position and the tilt are mirrored between the eyes,
// so the pair stays symmetric.
- left: { open: 0.72, lookX: 0, lookY: -0.3, closed: 'line', tear: 1.35, tearOn: true, tearY: 26, tearTilt: -20 },
- right: { open: 0.72, lookX: 0, lookY: -0.3, closed: 'line', tear: 1.35, tearOn: true, tearY: 26, tearTilt: 20 },
+ left: { open: 0.84, lookX: 0, lookY: -0.3, closed: 'line', tear: 1.35, tearOn: true, tearY: 26, tearTilt: -20 },
+ right: { open: 0.84, lookX: 0, lookY: -0.3, closed: 'line', tear: 1.35, tearOn: true, tearY: 26, tearTilt: 20 },
irisScale: 1.02,
lowerLid: 0.42,
highlight: true,