fix: flicker-free delta redraws for pupils/lids; visible smile curve

Full-eye repaints on every pupil step and blink frame flickered badly on
the 8-bit bus. Pupils now erase/redraw only themselves; lids paint
incrementally (BG rect closing, sclera chords opening). Mouth curve
deepened 20->32px and mood tiers raised so mood ~60 reads as an actual
smile instead of a 6px bend.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Mortdecai
2026-07-11 23:30:38 -04:00
parent 416bca9168
commit 6dfa1d7fb4
+55 -11
View File
@@ -87,16 +87,59 @@ void drawEyes(int8_t lid, int8_t pdx, int8_t pdy) {
cur.lid = lid; cur.pdx = pdx; cur.pdy = pdy;
}
// Delta redraw: erase old pupil with sclera, draw new. No full-eye repaint,
// no flicker. Pupil range + radius never reaches the sclera edge, so the
// erase circle can't bleed outside the eye.
void movePupils(int8_t pdx, int8_t pdy) {
const int cxs[2] = {EYE_LX, EYE_RX};
for (uint8_t i = 0; i < 2; i++) {
int cx = cxs[i];
tft.fillCircle(cx + cur.pdx, EYE_Y + cur.pdy, PUPIL_R + 2, SCLERA);
tft.fillCircle(cx + pdx, EYE_Y + pdy, PUPIL_R, PUPIL);
tft.fillCircle(cx + pdx - 8, EYE_Y + pdy - 8, 7, SCLERA);
if (cur.lid > 0) {
int h = (int32_t)cur.lid * 2 * EYE_R / 100;
tft.fillRect(cx - EYE_R, EYE_Y - EYE_R, 2 * EYE_R + 1, h, BG);
}
}
cur.pdx = pdx; cur.pdy = pdy;
}
// Incremental lids: closing = paint one BG rect over the top of the eye;
// opening = reveal the exposed band with sclera chords, then restore pupil.
void setLids(int8_t pct) {
int newH = (int32_t)pct * 2 * EYE_R / 100;
int oldH = (int32_t)cur.lid * 2 * EYE_R / 100;
const int cxs[2] = {EYE_LX, EYE_RX};
for (uint8_t i = 0; i < 2; i++) {
int cx = cxs[i];
if (newH > oldH) {
tft.fillRect(cx - EYE_R, EYE_Y - EYE_R + oldH, 2 * EYE_R + 1, newH - oldH, BG);
} else if (newH < oldH) {
for (int y = EYE_Y - EYE_R + newH; y < EYE_Y - EYE_R + oldH; y++) {
int dy = y - EYE_Y;
int w = (int)sqrt((float)(EYE_R * EYE_R - dy * dy));
tft.drawFastHLine(cx - w, y, 2 * w + 1, SCLERA);
}
tft.fillCircle(cx + cur.pdx, EYE_Y + cur.pdy, PUPIL_R, PUPIL);
tft.fillCircle(cx + cur.pdx - 8, EYE_Y + cur.pdy - 8, 7, SCLERA);
if (newH > 0)
tft.fillRect(cx - EYE_R, EYE_Y - EYE_R, 2 * EYE_R + 1, newH, BG);
}
}
cur.lid = pct;
}
void drawMouth(int8_t curve, int8_t openR) {
tft.fillRect(MOUTH_X - MOUTH_HW - 8, MOUTH_Y - 30, 2 * (MOUTH_HW + 8), 64, BG);
tft.fillRect(MOUTH_X - MOUTH_HW - 8, MOUTH_Y - 38, 2 * (MOUTH_HW + 8), 80, BG);
if (openR > 0) {
tft.fillCircle(MOUTH_X, MOUTH_Y, openR, PUPIL);
tft.drawCircle(MOUTH_X, MOUTH_Y, openR, SCLERA);
tft.drawCircle(MOUTH_X, MOUTH_Y, openR + 1, SCLERA);
} else {
for (int x = -MOUTH_HW; x <= MOUTH_HW; x += 4) {
int y = MOUTH_Y - (int32_t)curve * (20 - (x * x) / 180) / 100;
tft.fillCircle(MOUTH_X + x, y, 4, SCLERA);
int y = MOUTH_Y - (int32_t)curve * (32 - (x * x) / 113) / 100;
tft.fillCircle(MOUTH_X + x, y, 5, SCLERA);
}
}
cur.curve = curve; cur.openR = openR;
@@ -133,7 +176,7 @@ void setFace(int8_t lid, int8_t pdx, int8_t pdy, int8_t curve, int8_t openR, boo
if (blush != cur.blush) drawBlush(blush);
}
int8_t moodCurve() { return pet.mood > 66 ? 80 : (pet.mood > 33 ? 30 : -40); }
int8_t moodCurve() { return pet.mood > 66 ? 100 : (pet.mood > 33 ? 70 : -50); }
int8_t moodLid() { return pet.mood > 66 ? 0 : (pet.mood > 33 ? 10 : 30); }
void clearZs() { tft.fillRect(380, 20, 100, 90, BG); }
@@ -182,9 +225,9 @@ void bumpMood(int8_t d) {
void blinkOnce() {
int8_t base = moodLid();
const int8_t seq[3] = {60, 100, 60};
for (uint8_t i = 0; i < 3; i++) { drawEyes(seq[i], cur.pdx, cur.pdy); delay(35); }
drawEyes(base, cur.pdx, cur.pdy);
setLids(100);
delay(90);
setLids(base);
}
void wakeAnim() {
@@ -214,7 +257,7 @@ void dizzyAnim() {
drawMouth(0, 10);
for (int a = 0; a < 720; a += 40) {
float r = a * 0.01745;
drawEyes(0, (int8_t)(PUPIL_RANGE * cos(r)), (int8_t)(PUPIL_RANGE * sin(r)));
movePupils((int8_t)(PUPIL_RANGE * cos(r)), (int8_t)(PUPIL_RANGE * sin(r)));
delay(28);
}
drawEyes(30, 0, 8); drawMouth(-30, 0); delay(500);
@@ -223,9 +266,10 @@ void dizzyAnim() {
void yawnAnim() {
Serial.println(F("yawn"));
setLids(50);
const int8_t seq[8] = {8, 14, 20, 24, 24, 20, 12, 0};
for (uint8_t i = 0; i < 8; i++) {
drawMouth(0, seq[i]); drawEyes(50, cur.pdx, cur.pdy); delay(140);
drawMouth(0, seq[i]); delay(140);
}
setFace(moodLid(), cur.pdx, cur.pdy, moodCurve(), 0, false);
}
@@ -251,7 +295,7 @@ void flyAnim() {
tft.fillCircle(fx, fy, 3, FLYCLR);
px = fx; py = fy;
int8_t pdx = constrain((fx - MOUTH_X) / 8, -PUPIL_RANGE, PUPIL_RANGE);
if (pdx != cur.pdx) drawEyes(0, pdx, -14);
if (pdx != cur.pdx) movePupils(pdx, -14);
delay(30);
if (touched()) break; // a poke scares the fly off
}
@@ -352,7 +396,7 @@ void loop() {
} else {
int8_t sx = constrain(targPdx - cur.pdx, -6, 6);
int8_t sy = constrain(targPdy - cur.pdy, -6, 6);
drawEyes(cur.lid, cur.pdx + sx, cur.pdy + sy);
movePupils(cur.pdx + sx, cur.pdy + sy);
nextWanderMs = now + 60;
}
}