Files
FacePlant/src/ui/FaceRenderer.cpp

282 lines
7.1 KiB
C++
Raw Normal View History

2026-02-09 11:41:12 -05:00
#include "FaceRenderer.h"
static int clampInt(int v, int lo, int hi) {
if (v < lo) return lo;
if (v > hi) return hi;
return v;
}
unsigned long FaceRenderer::randRange(unsigned long lo, unsigned long hi) {
if (hi <= lo) return lo;
return lo + (unsigned long)random((long)(hi - lo + 1UL));
}
int8_t FaceRenderer::randRangeI8(int8_t lo, int8_t hi) {
if (hi <= lo) return lo;
return (int8_t)(lo + random((int)(hi - lo + 1)));
}
void FaceRenderer::begin(Display& display, Settings& settings) {
_display = &display;
_settings = &settings;
randomSeed((uint32_t)esp_random());
unsigned long now = millis();
_nextFrameMs = now;
_nextBlinkMs = now + randRange(3000, 9000);
_nextSillyMs = now + randRange(20000, 60000);
_nextGazeMs = now + randRange(800, 2000);
}
void FaceRenderer::loop(const MoistureSensor& moisture, const BatterySensor& battery) {
2026-02-09 11:41:12 -05:00
unsigned long now = millis();
if ((long)(now - _nextFrameMs) < 0) return;
_nextFrameMs = now + FRAME_MS;
updateMood(moisture.percent());
updateDeathMode(now);
if (_deadMode) {
renderDead(now, battery);
2026-02-09 11:41:12 -05:00
return;
}
if (_mood == HAPPY) updateHappy(now);
else if (_mood == DRY) updateDry(now);
else updateTooWet(now);
renderNormal(now, battery);
2026-02-09 11:41:12 -05:00
}
void FaceRenderer::updateMood(int moisturePct) {
PlantThresholds t = _settings->thresholds();
if (moisturePct >= t.tooWetPct) _mood = TOO_WET;
else if (moisturePct <= t.dryPct) _mood = DRY;
else _mood = HAPPY;
}
void FaceRenderer::updateDeathMode(unsigned long now) {
if (_settings->kidsMode()) {
_deadMode = false;
_dryStartMs = 0;
return;
}
if (_mood == DRY) {
if (_dryStartMs == 0) _dryStartMs = now;
if ((now - _dryStartMs) > DRY_DEADLINE_MS) {
_deadMode = true;
_nextDeadToggleMs = now + 3000;
}
} else {
_dryStartMs = 0;
_deadMode = false;
}
}
void FaceRenderer::updateHappy(unsigned long now) {
if ((long)(now - _nextGazeMs) >= 0) {
_targetX = randRangeI8(-3, 3);
_targetY = randRangeI8(-2, 2);
_nextGazeMs = now + randRange(800, 2200);
}
_gazeX += clampInt(_targetX - _gazeX, -1, 1);
_gazeY += clampInt(_targetY - _gazeY, -1, 1);
bool sillyDue = (long)(now - _nextSillyMs) >= 0;
if (!_silly && sillyDue) {
_silly = true;
_sillyVariant = random(0, 3);
_sillyUntilMs = now + (_settings->kidsMode() ? 4000 : 2000);
_nextSillyMs = now + (_settings->kidsMode() ? randRange(15000, 35000)
: randRange(60000, 180000));
}
if (_silly && (long)(now - _sillyUntilMs) >= 0) _silly = false;
if ((now - _lastBlinkMs) > 60000 || (long)(now - _nextBlinkMs) >= 0) {
_blinking = true;
_blinkUntilMs = now + 90;
_lastBlinkMs = now;
_nextBlinkMs = now + randRange(5000, 15000);
}
if (_blinking && (long)(now - _blinkUntilMs) >= 0) _blinking = false;
}
void FaceRenderer::updateDry(unsigned long now) {
if ((now - _lastBlinkMs) > 60000) {
_blinking = true;
_blinkUntilMs = now + 120;
_lastBlinkMs = now;
}
if (_blinking && (long)(now - _blinkUntilMs) >= 0) _blinking = false;
}
void FaceRenderer::updateTooWet(unsigned long now) {
if ((now - _lastBlinkMs) > 60000) {
_blinking = true;
_blinkUntilMs = now + 90;
_lastBlinkMs = now;
}
if (_blinking && (long)(now - _blinkUntilMs) >= 0) _blinking = false;
}
void FaceRenderer::renderDead(unsigned long now, const BatterySensor& battery) {
2026-02-09 11:41:12 -05:00
auto &d = _display->oled();
d.clearDisplay();
if ((long)(now - _nextDeadToggleMs) >= 0) {
_deadShowTombstone = !_deadShowTombstone;
_nextDeadToggleMs = now + 3000;
}
if (_deadShowTombstone) {
d.drawRoundRect(38, 12, 52, 40, 8, 1);
d.setCursor(52, 28);
d.setTextSize(2);
d.print("RIP");
} else {
drawXEye(40, 24);
drawXEye(88, 24);
drawMouthFlat();
}
// Draw battery icon in top-right corner
_display->drawBatteryIcon(110, 2, battery.percent(), battery.shouldBlink());
2026-02-09 11:41:12 -05:00
d.display();
}
void FaceRenderer::renderNormal(unsigned long now, const BatterySensor& battery) {
2026-02-09 11:41:12 -05:00
auto &d = _display->oled();
d.clearDisplay();
if (_mood == HAPPY) {
if (_silly) drawSilly(_sillyVariant);
else if (_blinking) drawEyesClosed();
else drawEyesOpen(_gazeX, _gazeY);
drawMouthHappy();
drawSparkles(now);
}
else if (_mood == DRY) {
if (_blinking) drawEyesClosed();
else drawEyesSmallPupils(6, 0, 0);
drawBrowsVerySad();
drawMouthFrown();
drawBigWaterText(now);
}
else {
if (_blinking) drawEyesClosed();
else drawEyesOpen(0, 0);
drawMouthFlat();
drawBubbles((now / 100) % 12 - 6);
}
// Draw battery icon in top-right corner
_display->drawBatteryIcon(110, 2, battery.percent(), battery.shouldBlink());
2026-02-09 11:41:12 -05:00
d.display();
}
/* --- Drawing helpers --- */
void FaceRenderer::drawEyesOpen(int dx, int dy) {
auto &d = _display->oled();
d.fillCircle(40, 24, 12, 1);
d.fillCircle(88, 24, 12, 1);
d.fillCircle(40 + dx, 24 + dy, 4, 0);
d.fillCircle(88 + dx, 24 + dy, 4, 0);
}
void FaceRenderer::drawEyesClosed() {
auto &d = _display->oled();
d.drawLine(28, 24, 52, 24, 1);
d.drawLine(76, 24, 100, 24, 1);
}
void FaceRenderer::drawEyesSmallPupils(int dy, int sx, int sy) {
auto &d = _display->oled();
d.fillCircle(40, 24, 12, 1);
d.fillCircle(88, 24, 12, 1);
d.fillCircle(40 + sx, 24 + dy, 2, 0);
d.fillCircle(88 + sx, 24 + dy, 2, 0);
}
void FaceRenderer::drawBrowsVerySad() {
auto &d = _display->oled();
d.drawLine(26, 12, 48, 18, 1);
d.drawLine(80, 18, 102, 12, 1);
}
void FaceRenderer::drawMouthHappy() {
auto &d = _display->oled();
for (int i = 0; i < 5; i++)
d.drawLine(54 + i, 46 + i / 2, 74 - i, 46 + i / 2, 1);
}
void FaceRenderer::drawMouthFrown() {
auto &d = _display->oled();
for (int i = 0; i < 5; i++)
d.drawLine(54 - i, 48 + i / 2, 74 + i, 48 + i / 2, 1);
}
void FaceRenderer::drawMouthFlat() {
auto &d = _display->oled();
d.drawLine(52, 48, 76, 48, 1);
}
void FaceRenderer::drawBubbles(int off) {
auto &d = _display->oled();
d.drawCircle(10 + off, 12, 4, 1);
d.drawCircle(20 + off, 20, 3, 1);
d.drawCircle(30 + off, 14, 5, 1);
}
void FaceRenderer::drawSparkles(unsigned long now) {
auto &d = _display->oled();
if ((now / 250) % 2 == 0) {
d.drawPixel(10, 10, 1);
d.drawPixel(118, 12, 1);
}
}
void FaceRenderer::drawSilly(uint8_t v) {
auto &d = _display->oled();
if (v == 0) {
drawEyesOpen(2, 2);
d.drawCircle(64, 48, 6, 1);
} else if (v == 1) {
drawEyesClosed();
d.drawRect(58, 44, 12, 8, 1);
} else {
drawEyesOpen(-2, 1);
d.drawLine(52, 48, 76, 48, 1);
d.drawPixel(64, 54, 1);
}
}
void FaceRenderer::drawXEye(int cx, int cy) {
auto &d = _display->oled();
d.drawLine(cx - 6, cy - 6, cx + 6, cy + 6, 1);
d.drawLine(cx - 6, cy + 6, cx + 6, cy - 6, 1);
}
void FaceRenderer::drawBigWaterText(unsigned long now) {
auto &d = _display->oled();
const char* msg = ((now / 2200UL) % 2UL == 0UL) ? "Need...water..." : "So...thirsty...";
d.setTextSize(1);
d.setTextColor(1);
int16_t x1 = 0, y1 = 0;
uint16_t w = 0, h = 0;
d.getTextBounds(msg, 0, 0, &x1, &y1, &w, &h);
int16_t x = (int16_t)((128 - (int)w) / 2);
d.setCursor(x < 0 ? 0 : x, 56);
d.print(msg);
}