aboutsummaryrefslogtreecommitdiffhomepage
path: root/public/bluebey-studio/src/faceArt.js
diff options
context:
space:
mode:
authorYasutake Yohei <61961825+yasutakeyohei@users.noreply.github.com>2026-10-04 19:20:51 +0900
committerYasutake Yohei <61961825+yasutakeyohei@users.noreply.github.com>2026-10-04 19:20:51 +0900
commit33557794b50815ff43204a81dfce71d3d95fa1c3 (patch)
tree2b0dd3a1dd5915c458d00dbf33d12463694392b8 /public/bluebey-studio/src/faceArt.js
parent291408cc4939e68aac60fb7a4cd37d0fea85c57a (diff)
bluebey-studio: まぶたの形と目の傾きを追加し、細目時のまばたきを修正
「直線(平ら)」と「目の傾き」を「目を細める」の隣に移動・追加(左右で反転)。上まぶたが下まぶたの位置まで下りて閉じるようにし、細目でもまばたきで最後まで閉じるようにした。
Diffstat (limited to 'public/bluebey-studio/src/faceArt.js')
-rw-r--r--public/bluebey-studio/src/faceArt.js54
1 files changed, 41 insertions, 13 deletions
diff --git a/public/bluebey-studio/src/faceArt.js b/public/bluebey-studio/src/faceArt.js
index c5eca6e..6a870fa 100644
--- a/public/bluebey-studio/src/faceArt.js
+++ b/public/bluebey-studio/src/faceArt.js
@@ -485,11 +485,24 @@ 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, lower, flat = false) {
+function clipEye(ctx, eye, open, lowerLid, flat = false, tiltDeg = 0) {
circlePath(ctx, eye.cx, eye.cy, eye.r);
ctx.clip();
- const upper = 1 - clamp(open, 0, 1);
+ const tilt = (clamp(tiltDeg, -45, 45) * Math.PI) / 180;
+ const rotated = Math.abs(tilt) > 1e-4;
+ const spin = () => {
+ ctx.translate(eye.cx, eye.cy);
+ ctx.rotate(tilt);
+ ctx.translate(-eye.cx, -eye.cy);
+ };
+ 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));
if (upper > 0.0005) {
const rl = eye.r * EYE_LAYOUT.lidRadiusFactor;
const lowest = eye.cy - eye.r + 2 * eye.r * upper;
@@ -510,7 +523,7 @@ function clipEye(ctx, eye, open, lower, flat = false) {
ctx.clip();
}
- const bottom = clamp(lower, 0, 1);
+ const bottom = lower;
if (bottom > 0.0005) {
const rl = eye.r * EYE_LAYOUT.lidRadiusFactor;
const highest = eye.cy + eye.r - 2 * eye.r * bottom;
@@ -529,6 +542,13 @@ function clipEye(ctx, eye, open, lower, flat = false) {
ctx.closePath();
ctx.clip();
}
+
+ if (rotated) {
+ // Undo the rotation for the caller, but keep the clip we just set.
+ ctx.translate(eye.cx, eye.cy);
+ ctx.rotate(-tilt);
+ ctx.translate(-eye.cx, -eye.cy);
+ }
}
function lidPath(ctx, eye, amount, lower, flat = false) {
@@ -1337,6 +1357,8 @@ export function drawEyes(ctx, p) {
const heartTravel = EYE_LAYOUT.radius * 0.9;
const lowerLid = clamp(style.lowerLid ?? 0, 0, 1);
const lidFlat = (style.lidShape ?? 'curve') === 'flat';
+ // How much the whole narrowed eye is tilted (e.g. an angry or gentle squint).
+ const lidTilt = clamp(style.lidTilt ?? 0, -45, 45);
// ほっぺ and 頭の模様 belong to the whole face, so they are drawn once here
// rather than inside the per-eye loop below. 鼻ちょうちん comes along too.
@@ -1403,7 +1425,7 @@ export function drawEyes(ctx, p) {
// 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, lowerLid * (1 - open), lidFlat);
+ if (!isHeart || open < 0.999) clipEye(ctx, eye, open, lowerLid, lidFlat, lidTilt * eye.towardNose);
const eyeTravel = isHeart ? heartTravel * lookMax : travel * lookMax;
let dx = clamp(spec.lookX ?? 0, -1, 1) * eyeTravel;
@@ -1461,25 +1483,31 @@ export function drawEyes(ctx, p) {
// Faded in as the lid starts to cover the eye, so opening the eye all the
// way leaves the clean original artwork with no extra line.
const lidFade = clamp((0.95 - open) / 0.1, 0, 1);
+ // 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);
if (lidFade > 0.01 || lowerLid > 0.01) {
ctx.save();
+ if (eyeTilt) {
+ ctx.translate(eye.cx, eye.cy);
+ ctx.rotate((eyeTilt * Math.PI) / 180);
+ ctx.translate(-eye.cx, -eye.cy);
+ }
circlePath(ctx, eye.cx, eye.cy, eye.r);
ctx.clip();
ctx.globalAlpha = line ? 1 : lidFade;
ctx.strokeStyle = lineColor;
ctx.lineWidth = lidWidth;
if (lidFade > 0.01) {
- lidPath(ctx, eye, open, false, lidFlat);
+ lidPath(ctx, eye, upperAmt, false, lidFlat);
ctx.stroke();
}
- // The lower lid fades out as the eye closes (the upper lid does the work),
- // and the *line* has to use the same amount as the clip above - using the
- // raw `lowerLid` left the line floating inside the eyeball, which read as a
- // second (doubled) lid.
- const lower = lowerLid * (1 - open);
- if (lower > 0.01) {
- ctx.globalAlpha = line ? 1 : lower;
- lidPath(ctx, eye, lower, true, lidFlat);
+ if (lowerLid > 0.01) {
+ ctx.globalAlpha = line ? 1 : lowerLid;
+ lidPath(ctx, eye, lowerLid, true, lidFlat);
ctx.stroke();
}
ctx.restore();