feat: metadata overlay tweaks — location badge, rounded corners, translucent option

- Location gets its own badge in the lower-left corner (separate from
  the main date/time/people/camera badge at the configured position)
- All overlay badges now use rounded corners (radius 4px) via
  fillRoundRect instead of fillRect
- New setting: 'Translucent background?' checkbox in web UI
  When enabled, badge backgrounds use a checkerboard pattern (every
  other pixel black) instead of solid black, creating a 50% opacity
  effect that lets the underlying image show through
- Refactored all badge drawing through a single drawOverlayBadge()
  helper (metadata, sleep indicator, battery indicator)

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-08-03 20:50:01 -04:00
parent 5b7c0042f9
commit bb270e0546
7 changed files with 153 additions and 63 deletions

View File

@@ -130,7 +130,7 @@ void DisplayManager::showMetadataInRegion(const AssetInfo& info, uint8_t metaFla
const String& dateFmt, const String& timeFmt) {
if (metaFlags == 0) return;
// Build metadata string
// Build metadata string (excluding location — it gets its own badge)
String metaText = "";
// Parse ISO datetime (YYYY-MM-DDTHH:MM:SS) into struct tm for strftime
@@ -158,10 +158,6 @@ void DisplayManager::showMetadataInRegion(const AssetInfo& info, uint8_t metaFla
// Fallback if datetime is too short for full parse
metaText += info.dateTime.substring(0, 10);
}
if ((metaFlags & META_LOCATION) && info.city.length() > 0) {
if (metaText.length() > 0) metaText += " | ";
metaText += info.city;
}
if ((metaFlags & META_PEOPLE) && !info.people.empty()) {
if (metaText.length() > 0) metaText += " | ";
for (size_t i = 0; i < info.people.size(); i++) {
@@ -177,71 +173,151 @@ void DisplayManager::showMetadataInRegion(const AssetInfo& info, uint8_t metaFla
metaText += info.camera;
}
if (metaText.length() == 0) return;
// Location gets its own badge in lower-left
String locationText = "";
if ((metaFlags & META_LOCATION) && info.city.length() > 0) {
locationText = info.city;
}
if (metaText.length() == 0 && locationText.length() == 0) return;
static constexpr uint16_t MARGIN = 8;
static constexpr uint16_t PAD_X = 8;
static constexpr uint16_t PAD_Y = 5;
static constexpr uint16_t TEXT_H = 20;
_display->setTextSize(1.5f);
_display->setTextColor(TFT_WHITE);
switch (pos) {
case MetaPosition::CaptionBottom:
case MetaPosition::CaptionTop: {
// Bar spanning the region width, positioned at region top or bottom
uint16_t barH = 36;
uint16_t barY = (pos == MetaPosition::CaptionTop)
? regionY
: regionY + regionH - barH;
_display->fillRect(regionX, barY, regionW, barH, TFT_BLACK);
_display->setTextDatum(middle_center);
_display->drawString(metaText.c_str(), regionX + regionW / 2, barY + barH / 2);
break;
}
case MetaPosition::OverlayTopLeft:
case MetaPosition::OverlayTopRight:
case MetaPosition::OverlayBottomLeft:
case MetaPosition::OverlayBottomRight: {
static constexpr uint16_t MARGIN = 8;
static constexpr uint16_t PAD_X = 8;
static constexpr uint16_t PAD_Y = 5;
static constexpr uint16_t TEXT_H = 20;
// Track where the main badge ends up (for location badge offset)
uint16_t mainBadgeBottomY = 0;
uint16_t textW = _display->textWidth(metaText.c_str());
uint16_t badgeW = min((uint16_t)(textW + PAD_X * 2), regionW);
uint16_t badgeH = TEXT_H + PAD_Y * 2;
uint16_t bx, by;
switch (pos) {
case MetaPosition::OverlayTopLeft:
bx = regionX + MARGIN;
by = regionY + MARGIN;
break;
case MetaPosition::OverlayTopRight:
bx = regionX + regionW - badgeW - MARGIN;
by = regionY + MARGIN;
break;
case MetaPosition::OverlayBottomLeft:
bx = regionX + MARGIN;
by = regionY + regionH - badgeH - MARGIN;
break;
case MetaPosition::OverlayBottomRight:
bx = regionX + regionW - badgeW - MARGIN;
by = regionY + regionH - badgeH - MARGIN;
break;
default:
bx = regionX + MARGIN;
by = regionY + MARGIN;
break;
// Draw main metadata badge (date/time/people/camera) at configured position
if (metaText.length() > 0) {
switch (pos) {
case MetaPosition::CaptionBottom:
case MetaPosition::CaptionTop: {
uint16_t barH = 36;
uint16_t barY = (pos == MetaPosition::CaptionTop)
? regionY
: regionY + regionH - barH;
_display->fillRect(regionX, barY, regionW, barH, TFT_BLACK);
_display->setTextDatum(middle_center);
_display->drawString(metaText.c_str(), regionX + regionW / 2, barY + barH / 2);
break;
}
case MetaPosition::OverlayTopLeft:
case MetaPosition::OverlayTopRight:
case MetaPosition::OverlayBottomLeft:
case MetaPosition::OverlayBottomRight: {
uint16_t textW = _display->textWidth(metaText.c_str());
uint16_t badgeW = min((uint16_t)(textW + PAD_X * 2), regionW);
uint16_t badgeH = TEXT_H + PAD_Y * 2;
_display->fillRect(bx, by, badgeW, badgeH, TFT_BLACK);
_display->setTextDatum(middle_left);
_display->drawString(metaText.c_str(), bx + PAD_X, by + badgeH / 2);
break;
uint16_t bx, by;
switch (pos) {
case MetaPosition::OverlayTopLeft:
bx = regionX + MARGIN;
by = regionY + MARGIN;
break;
case MetaPosition::OverlayTopRight:
bx = regionX + regionW - badgeW - MARGIN;
by = regionY + MARGIN;
break;
case MetaPosition::OverlayBottomLeft:
bx = regionX + MARGIN;
by = regionY + regionH - badgeH - MARGIN;
break;
case MetaPosition::OverlayBottomRight:
bx = regionX + regionW - badgeW - MARGIN;
by = regionY + regionH - badgeH - MARGIN;
break;
default:
bx = regionX + MARGIN;
by = regionY + MARGIN;
break;
}
drawOverlayBadge(bx, by, badgeW, badgeH);
_display->setTextDatum(middle_left);
_display->drawString(metaText.c_str(), bx + PAD_X, by + badgeH / 2);
mainBadgeBottomY = by + badgeH;
break;
}
}
}
// Draw location badge in lower-left corner (separate from main badge)
if (locationText.length() > 0) {
uint16_t locTextW = _display->textWidth(locationText.c_str());
uint16_t locBadgeW = min((uint16_t)(locTextW + PAD_X * 2), regionW);
uint16_t locBadgeH = TEXT_H + PAD_Y * 2;
uint16_t lx = regionX + MARGIN;
uint16_t ly = regionY + regionH - locBadgeH - MARGIN;
// If main badge is also in bottom-left, stack location above it
if (pos == MetaPosition::OverlayBottomLeft && metaText.length() > 0) {
ly = ly - locBadgeH - 4;
}
drawOverlayBadge(lx, ly, locBadgeW, locBadgeH);
_display->setTextColor(TFT_WHITE);
_display->setTextDatum(middle_left);
_display->drawString(locationText.c_str(), lx + PAD_X, ly + locBadgeH / 2);
}
}
void DisplayManager::drawBadge(uint16_t x, uint16_t y, uint16_t w, uint16_t h) {
_display->fillRect(x, y, w, h, TFT_BLACK);
void DisplayManager::drawOverlayBadge(uint16_t x, uint16_t y, uint16_t w, uint16_t h) {
static constexpr int16_t R = 4;
if (!_translucent) {
_display->fillRoundRect(x, y, w, h, R, TFT_BLACK);
return;
}
// Checkerboard pattern within rounded-rect bounds for 50% opacity effect.
// Use fillRoundRect to create a clip mask in black, then punch holes by
// restoring every-other pixel. Simpler: just iterate and test corner radii.
for (uint16_t py = y; py < y + h; py++) {
for (uint16_t px = x; px < x + w; px++) {
if ((px + py) % 2 != 0) continue;
// Check if pixel is inside the rounded rect (corner exclusion)
int16_t lx = px - x;
int16_t ly = py - y;
bool inside = true;
// Top-left corner
if (lx < R && ly < R) {
int16_t dx = R - 1 - lx;
int16_t dy = R - 1 - ly;
inside = (dx * dx + dy * dy) <= (R * R);
}
// Top-right corner
else if (lx >= w - R && ly < R) {
int16_t dx = lx - (w - R);
int16_t dy = R - 1 - ly;
inside = (dx * dx + dy * dy) <= (R * R);
}
// Bottom-left corner
else if (lx < R && ly >= h - R) {
int16_t dx = R - 1 - lx;
int16_t dy = ly - (h - R);
inside = (dx * dx + dy * dy) <= (R * R);
}
// Bottom-right corner
else if (lx >= w - R && ly >= h - R) {
int16_t dx = lx - (w - R);
int16_t dy = ly - (h - R);
inside = (dx * dx + dy * dy) <= (R * R);
}
if (inside) {
_display->writePixel(px, py, TFT_BLACK);
}
}
}
}
void DisplayManager::showSleepIndicator(MetaPosition metaPos) {
@@ -281,7 +357,7 @@ void DisplayManager::showSleepIndicator(MetaPosition metaPos) {
break;
}
drawBadge(bx, by, badgeW, badgeH);
drawOverlayBadge(bx, by, badgeW, badgeH);
_display->setTextColor(TFT_WHITE);
_display->setTextDatum(middle_center);
_display->drawString("SLP", bx + badgeW / 2, by + badgeH / 2);
@@ -324,7 +400,7 @@ void DisplayManager::showBatteryIndicator(uint8_t percent, MetaPosition metaPos)
}
// Draw badge background
drawBadge(bx, by, badgeW, badgeH);
drawOverlayBadge(bx, by, badgeW, badgeH);
// Draw battery outline (white rect with 1px border)
uint16_t iconX = bx + PAD_X;