diff options
| author | Yasutake Yohei <61961825+yasutakeyohei@users.noreply.github.com> | 2026-10-05 13:48:38 +0900 |
|---|---|---|
| committer | Yasutake Yohei <61961825+yasutakeyohei@users.noreply.github.com> | 2026-10-05 13:48:38 +0900 |
| commit | 4fac4bfdfa339c0ee103d7fbef529f4d49e8ac51 (patch) | |
| tree | d26cc0418057e8deb4d09ab64e5c8367d1399f28 /public/bluebey-studio/src | |
| parent | cdc88d268019ee9cb335ac14e0dbc8ebabb5b801 (diff) | |
bluebey-studio: 上まぶたと下まぶたを独立させ、まぶた設定を目メニューに移設
上まぶたの高さが下まぶたの高さに引きずられる挙動を解消。目を細めるボタンを廃止し、まぶたの形・傾きを目の形の上へ移動。白目を出すときの形のサイズ上限を撤廃し、既定で白目に収まるようにした。
Diffstat (limited to 'public/bluebey-studio/src')
| -rw-r--r-- | public/bluebey-studio/src/faceArt.js | 37 | ||||
| -rw-r--r-- | public/bluebey-studio/src/gacha.js | 6 | ||||
| -rw-r--r-- | public/bluebey-studio/src/panel.js | 79 | ||||
| -rw-r--r-- | public/bluebey-studio/src/presets.js | 10 |
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, |
