From 27220fe6aa19f8e63be3984be1bc3288002252f7 Mon Sep 17 00:00:00 2001 From: Yasutake Yohei <61961825+yasutakeyohei@users.noreply.github.com> Date: Mon, 5 Oct 2026 14:55:10 +0900 Subject: bluebey-studio: まつげの角度と位置を調整できるように MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit まつげの角度(跳ねる向き)と位置(まぶたに沿った位置)を左右の目ごとにスライダーで設定できるようにした。 --- public/bluebey-studio/src/faceArt.js | 20 +++++++++++++++----- public/bluebey-studio/src/main.js | 2 ++ public/bluebey-studio/src/panel.js | 17 +++++++++++++++++ public/bluebey-studio/src/presets.js | 6 ++++-- 4 files changed, 38 insertions(+), 7 deletions(-) diff --git a/public/bluebey-studio/src/faceArt.js b/public/bluebey-studio/src/faceArt.js index adecbb9..829defc 100644 --- a/public/bluebey-studio/src/faceArt.js +++ b/public/bluebey-studio/src/faceArt.js @@ -1562,9 +1562,15 @@ export function drawEyes(ctx, p) { // the eyeball, so they are drawn after the lid block and are not clipped. if (spec.lashes ?? style.lashes) { // The lashes ride the upper lid: they drop as the lid lowers (2r per unit of - // open) and tilt with まぶたの傾き, so they stay on the lid edge. + // open) and tilt with まぶたの傾き, so they stay on the lid edge. まつげの位置 + // slides them along the lid, まつげの角度 tilts the strokes themselves. const outDir = -eye.towardNose; const lidDrop = 2 * eye.r * (1 - open); + const lashPos = clamp(spec.lashPos ?? style.lashPos ?? 0, -80, 80); + const lashAngle = clamp(spec.lashAngle ?? style.lashAngle ?? 0, -80, 80) * outDir; + const a = (lashAngle * Math.PI) / 180; + const ca = Math.cos(a); + const sa = Math.sin(a); ctx.save(); if (eyeTilt) { ctx.translate(eye.cx, eye.cy); @@ -1572,14 +1578,18 @@ export function drawEyes(ctx, p) { ctx.translate(-eye.cx, -eye.cy); } for (const deg of EYE_LAYOUT.lash.angles) { - const t = (deg * Math.PI) / 180; + const t = ((deg + lashPos) * Math.PI) / 180; const ux = outDir * Math.cos(t); const uy = -Math.sin(t); + // The stroke points along the radial direction, tilted by まつげの角度. + const dx = ux * ca - uy * sa; + const dy = ux * sa + uy * ca; const r0 = eye.r * 0.96; - const r1 = r0 + EYE_LAYOUT.lash.length; + const bx = eye.cx + ux * r0; + const by = eye.cy + uy * r0 + lidDrop; strokePolyline(ctx, [ - { x: eye.cx + ux * r0, y: eye.cy + uy * r0 + lidDrop }, - { x: eye.cx + ux * r1, y: eye.cy + uy * r1 + lidDrop }, + { x: bx, y: by }, + { x: bx + dx * EYE_LAYOUT.lash.length, y: by + dy * EYE_LAYOUT.lash.length }, ], { color: lineColor, width: EYE_LAYOUT.lash.width }); } ctx.restore(); diff --git a/public/bluebey-studio/src/main.js b/public/bluebey-studio/src/main.js index 322a0e6..b2437b3 100644 --- a/public/bluebey-studio/src/main.js +++ b/public/bluebey-studio/src/main.js @@ -1736,6 +1736,8 @@ async function init() { lidWidth: own.lidWidth ?? null, lidTilt: own.lidTilt ?? null, lashes: own.lashes ?? null, + lashAngle: own.lashAngle ?? null, + lashPos: own.lashPos ?? null, }; }; diff --git a/public/bluebey-studio/src/panel.js b/public/bluebey-studio/src/panel.js index 093386f..9e7a9ac 100644 --- a/public/bluebey-studio/src/panel.js +++ b/public/bluebey-studio/src/panel.js @@ -463,6 +463,8 @@ export function buildPanel(app, root) { state.face.eyes.right.lidWidth = state.face.eyes.left.lidWidth; state.face.eyes.right.lidTilt = state.face.eyes.left.lidTilt; state.face.eyes.right.lashes = state.face.eyes.left.lashes; + state.face.eyes.right.lashAngle = state.face.eyes.left.lashAngle; + state.face.eyes.right.lashPos = state.face.eyes.left.lashPos; 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; @@ -604,12 +606,22 @@ export function buildPanel(app, root) { value: false, onChange: (v) => { write((e) => { e.lashes = v; }); touched(); }, }); + const lashAngle = slider({ + label: 'まつげの角度', min: -80, max: 80, step: 1, value: 0, format: (v) => `${Math.round(v)}°`, + onInput: (v) => { write((e) => { e.lashAngle = v; }); touched(); }, + }); + const lashPos = slider({ + label: 'まつげの位置', min: -80, max: 80, step: 1, value: 0, format: (v) => `${Math.round(v)}`, + onInput: (v) => { write((e) => { e.lashPos = v; }); touched(); }, + }); lidSub.add(lidShape); lidSub.add(open); lidSub.add(lowerLid); lidSub.add(lidWidth); lidSub.add(lidTilt); lidSub.add(controlRow(null, lashes.el, { wide: true })); + lidSub.add(lashAngle); + lidSub.add(lashPos); group.add(lidSub); const openInput = open.el.querySelector('input'); const lowerLidInput = lowerLid.el.querySelector('input'); @@ -729,6 +741,11 @@ export function buildPanel(app, root) { lidWidth.set(e.lidWidth ?? state.face.eyes.lidWidth ?? 14); lidTilt.set(e.lidTilt ?? state.face.eyes.lidTilt ?? 0); lashes.set((e.lashes ?? state.face.eyes.lashes) === true); + const lashesOn = (e.lashes ?? state.face.eyes.lashes) === true; + lashAngle.set(e.lashAngle ?? state.face.eyes.lashAngle ?? 0); + lashPos.set(e.lashPos ?? state.face.eyes.lashPos ?? 0); + lashAngle.el.style.display = lashesOn ? '' : 'none'; + lashPos.el.style.display = lashesOn ? '' : 'none'; shape.set(shapeValue); heartColor.set(state.face.eyes.heartColor ?? '#e0344f'); heartColor.el.style.display = shapeValue === 'heart' ? '' : 'none'; diff --git a/public/bluebey-studio/src/presets.js b/public/bluebey-studio/src/presets.js index 8644215..9baa3a4 100644 --- a/public/bluebey-studio/src/presets.js +++ b/public/bluebey-studio/src/presets.js @@ -200,14 +200,16 @@ export function defaultState() { eyes: { source: 'parametric', // parametric | opened | closed | ... (hand-drawn) linked: true, // move both eyes together - 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, lidShape: null, lidWidth: null, lidTilt: null, lashes: 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, lidShape: null, lidWidth: null, lidTilt: null, lashes: 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, lidShape: null, lidWidth: null, lidTilt: null, lashes: null, lashAngle: null, lashPos: 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, lidShape: null, lidWidth: null, lidTilt: null, lashes: null, lashAngle: null, lashPos: null }, irisScale: 1, lookMax: 1.2, highlight: true, lidWidth: 14, lowerLid: 0, lashes: false, + lashAngle: 0, + lashPos: 0, // How the closing eyelid is drawn: `curve` is the soft, rounded lid; `flat` // is a straight lid edge (the hard, narrowed look). `lidTilt` rotates the // narrowed eye. See faceArt.js. -- cgit v1.3.1