aboutsummaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
authorYasutake Yohei <61961825+yasutakeyohei@users.noreply.github.com>2026-10-05 14:55:10 +0900
committerYasutake Yohei <61961825+yasutakeyohei@users.noreply.github.com>2026-10-05 14:55:10 +0900
commit27220fe6aa19f8e63be3984be1bc3288002252f7 (patch)
treef5ccd3846f22727ee031286493ec1fde8136e856
parent257516e2bde90e65439d01f19434cd2d9d5a03f7 (diff)
bluebey-studio: まつげの角度と位置を調整できるように
まつげの角度(跳ねる向き)と位置(まぶたに沿った位置)を左右の目ごとにスライダーで設定できるようにした。
-rw-r--r--public/bluebey-studio/src/faceArt.js20
-rw-r--r--public/bluebey-studio/src/main.js2
-rw-r--r--public/bluebey-studio/src/panel.js17
-rw-r--r--public/bluebey-studio/src/presets.js6
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.