aboutsummaryrefslogtreecommitdiffhomepage
path: root/public/bluebey-studio/src/faceArt.js
diff options
context:
space:
mode:
Diffstat (limited to 'public/bluebey-studio/src/faceArt.js')
-rw-r--r--public/bluebey-studio/src/faceArt.js38
1 files changed, 29 insertions, 9 deletions
diff --git a/public/bluebey-studio/src/faceArt.js b/public/bluebey-studio/src/faceArt.js
index 11319c1..716fcab 100644
--- a/public/bluebey-studio/src/faceArt.js
+++ b/public/bluebey-studio/src/faceArt.js
@@ -1207,18 +1207,27 @@ function drawGlasses(ctx, eye, line, style, limits) {
// the full height so even that point cannot reach the canvas border.
const halfV = shades ? ry * Math.max(L.innerTop + L.topSkew, L.outerBottom) : ry;
+ // The face's midline: the two eyes are symmetric about it, so it is where the
+ // two halves of the bridge meet and what `tilt` turns the pair about.
+ const midX = (EYE_LAYOUT.eyes[0].cx + EYE_LAYOUT.eyes[1].cx) / 2;
+ // `lensGap` slides the two lenses apart (positive) or together (negative) along
+ // the face, symmetrically about the midline. It is clamped so the inner edges may
+ // meet at the midline but the two lenses can never cross each other.
+ const inwardRoom = Math.max(0, Math.abs(midX - eye.cx) - rx);
+ const gap = clamp(spec.lensGap ?? 0, -inwardRoom, HUGE);
// Keep the lens (and so everything that hangs off it) inside the artwork. The
// rest of the file clamps against `limits` for the same reason: a mark that
// reaches the canvas border gets the edge row copied across the whole plate.
- const lensCx = clamp(eye.cx, limits.left + EDGE_MARGIN + rx, limits.right - EDGE_MARGIN - rx);
+ const lensCx = clamp(
+ eye.cx + out * gap,
+ limits.left + EDGE_MARGIN + rx,
+ limits.right - EDGE_MARGIN - rx,
+ );
const lensCy = clamp(
eye.cy + L.drop + (spec.offsetY ?? 0),
limits.top + EDGE_MARGIN + halfV,
limits.bottom - EDGE_MARGIN - halfV,
);
- // The face's midline: the two eyes are symmetric about it, so it is where the
- // two halves of the bridge meet and what `tilt` turns the pair about.
- const midX = (EYE_LAYOUT.eyes[0].cx + EYE_LAYOUT.eyes[1].cx) / 2;
// Local (u, v) -> canvas, with u outwards and v downwards.
const at = (u, v) => ({ x: lensCx + out * u, y: lensCy + v });
@@ -1366,6 +1375,12 @@ export function drawEyes(ctx, p) {
const lookMax = style.lookMax ?? 1;
const heartScale = 2;
const heartColor = style.heartColor ?? '#e0344f';
+ // A lens that is effectively opaque hides the eye completely, so a blink would
+ // show only as a flicker of the shut artwork through the lens. While such a
+ // lens is on, the eyes are drawn open and no blink is visible at all. This is a
+ // rendering decision, not an animation one - the blink still runs underneath.
+ const lensOpaque = style.glasses?.enabled === true
+ && clamp(style.glasses.lensOpacity ?? 0, 0, 1) >= 0.99;
// ほっぺ and 頭の模様 belong to the whole face, so they are drawn once here
// rather than inside the per-eye loop below. 鼻ちょうちん comes along too.
@@ -1388,7 +1403,7 @@ export function drawEyes(ctx, p) {
const lineStyle = { line: lineColor, width: lidWidth };
// Which artwork this eye shows. `shape` is the explicit choice; older saved
// states and the おまかせ rolls only set `open`/`closed`, so fall back.
- const shapeKey = eyeShapeKey(spec);
+ const shapeKey = eyeShapeKey(lensOpaque ? { ...spec, open: 1 } : spec);
const explicitShut = typeof spec.shape === 'string'
&& (spec.shape === 'three' || spec.shape === 'arch' || spec.shape.startsWith('line'));
const isHeart = shapeKey === 'heart';
@@ -1408,7 +1423,9 @@ export function drawEyes(ctx, p) {
// "shut"; lift it or the artwork would be clipped away. An explicit shut
// `shape` keeps its own lid height, so the lid can be lowered onto it.
const rawOpen = spec.open ?? 1;
- const open = clamp(explicitShut ? rawOpen : (rawOpen <= 0.02 ? 1 : rawOpen), 0, 1);
+ const open = lensOpaque
+ ? 1
+ : clamp(explicitShut ? rawOpen : (rawOpen <= 0.02 ? 1 : rawOpen), 0, 1);
const lowerLid = clamp(spec.lowerLid ?? style.lowerLid ?? 0, 0, 1);
// 大きさ: one knob for the iris, the heart and the shut artwork.
const shapeScale = clamp(style.irisScale ?? 1, 0.4, 2);
@@ -1480,10 +1497,13 @@ export function drawEyes(ctx, p) {
// --- the eye's content, clipped by the lids -------------------------
// Every shape is drawn the same way now: a white backing, then the artwork
// (iris, heart or the shut line/3/arch), trimmed by the upper and lower lids.
- // The shut artwork reaches past the eyeball, so it is only lid-clipped - but
- // once the white is shown it is clipped to the eyeball too, so it stays in.
+ // The shut artwork and a heart both reach past the eyeball, so they are only
+ // lid-clipped - but once the white is shown they are clipped to the eyeball
+ // too, so they stay inside it. Clipping the heart by its *lids* rather than by
+ // the eyeball is also what lets a raised lower lid cover it from below: the
+ // heart replaces the eyeball, but it must still sit behind both lids.
ctx.save();
- if (isShutShape && !showWhite) {
+ if ((isShutShape || isHeart) && !showWhite) {
clipLids(ctx, eye, open, lowerLid, lidFlat, lidTilt * eye.towardNose);
} else if (!isHeart || open < 0.999 || showWhite) {
clipEye(ctx, eye, open, lowerLid, lidFlat, lidTilt * eye.towardNose);