obd2_gauge/sketch/obd2_gauge/gps_clock.h
Sebastian Nieto d614ea633a feat(ui): pantalla de instrumental deportivo con tacometro, velocidad, refrigerante y bateria
- Agrega cluster_screen.h con diseno estilo cockpit (Pantalla 4).
- Tacometro horizontal deportivo (RPM) con 12 segmentos de color dinamico y lectura digital.
- Velocimetro digital gigante en tipografia DSEG7 44pt con etiqueta de origen GPS/OBD y riel luminico.
- Relojes inferiores dobles para Temperatura de Refrigerante (°C) y Voltaje de Bateria (V).
- Expansion del sistema de navegacion y paginacion a 5 pantallas (0 a 4) con indicadores sincronizados.
- Integracion completa con transiciones suaves y soporte para los 9 temas de color.
2026-10-14 07:36:41 -03:00

608 lines
21 KiB
C++

#pragma once
#include "DSEG7_Modern_BoldItalic_34.h"
#include "DSEG7_Modern_BoldItalic_80.h"
#include "Latin_Sans_Bold_26.h"
#include "rainbow_text.h"
#include "gps_module.h"
#include <Arduino.h>
#include <TFT_eSPI.h>
extern TFT_eSPI tft;
extern uint16_t getThemePrimaryColor();
extern uint16_t getThemeAccentColor();
extern uint16_t getThemeBgColor();
extern bool isRainbowTheme();
extern int8_t currentScreenPage;
extern int8_t fullScreenCell;
// Spanish 3-letter Month abbreviations
static const char *const SPANISH_MONTHS_SHORT[12] = {
"ENE", "FEB", "MAR", "ABR", "MAY", "JUN",
"JUL", "AGO", "SEP", "OCT", "NOV", "DIC"};
// Previous values for flicker-free redrawing
static uint8_t oldClockHour = 255;
static uint8_t oldClockMin = 255;
static uint8_t oldClockSec = 255;
static uint8_t oldClockDay = 255;
static uint8_t oldClockMonth = 255;
static uint16_t oldClockYear = 0;
// Smooth pre-rendered title sprites for Day of Week and Year (100% anti-aliased Latin_Sans_Bold_26 matching Page 1)
static TFT_eSprite sprDayTitle(&tft);
static uint8_t sprDayWday = 255;
inline void invalidateGpsClockTitles() {
sprDayWday = 255;
}
inline void updateDayTitleSprite(uint8_t wday) {
if (sprDayWday == wday && sprDayTitle.created()) return;
if (!sprDayTitle.created()) {
sprDayTitle.createSprite(160, 26);
}
if (sprDayTitle.created()) {
uint16_t cardBg = 0x10A2;
sprDayTitle.loadFont(Latin_Sans_Bold_26);
sprDayTitle.setTextPadding(0);
sprDayTitle.fillSprite(cardBg);
if (isYapaTheme()) {
int curX = 0;
char buf[2] = {0, 0};
String dName = SPANISH_DAYS[wday];
for (size_t i = 0; i < dName.length(); i++) {
buf[0] = dName[i];
if (buf[0] == ' ') { curX += sprDayTitle.textWidth(buf); continue; }
sprDayTitle.setTextColor(getYapaColor(i), cardBg);
sprDayTitle.drawString(buf, curX, 0);
curX += sprDayTitle.textWidth(buf);
}
} else {
sprDayTitle.setTextColor(TFT_WHITE, cardBg);
sprDayTitle.drawString(SPANISH_DAYS[wday], 0, 0, 1);
}
sprDayTitle.unloadFont();
if (isRainbowTheme()) {
applyRainbowToSprite(sprDayTitle, 4 + 116, 125 + 22, cardBg);
}
sprDayWday = wday;
}
}
static TFT_eSprite sprYearTitle(&tft);
static uint16_t sprYearVal = 0;
static uint16_t sprYearCol = 0;
inline void updateYearTitleSprite(uint16_t year, uint16_t priCol) {
if (sprYearVal == year && sprYearCol == priCol && sprYearTitle.created()) return;
if (!sprYearTitle.created()) {
sprYearTitle.createSprite(80, 26);
}
if (sprYearTitle.created()) {
uint16_t cardBg = 0x10A2;
sprYearTitle.loadFont(Latin_Sans_Bold_26);
sprYearTitle.setTextPadding(0);
sprYearTitle.fillSprite(cardBg);
if (isYapaTheme()) {
int curX = 0;
char buf[2] = {0, 0};
String yStr = String(year);
for (size_t i = 0; i < yStr.length(); i++) {
buf[0] = yStr[i];
sprYearTitle.setTextColor(getYapaColor(i + 2), cardBg);
sprYearTitle.drawString(buf, curX, 0);
curX += sprYearTitle.textWidth(buf);
}
} else {
sprYearTitle.setTextColor(priCol, cardBg);
sprYearTitle.drawString(String(year).c_str(), 0, 0, 1);
}
sprYearTitle.unloadFont();
if (isRainbowTheme()) {
applyRainbowToSprite(sprYearTitle, 4 + 116, 125 + 64, cardBg);
}
sprYearVal = year;
sprYearCol = priCol;
}
}
inline void initGpsClockTitles() {
updateDayTitleSprite(gpsDayOfWeek % 7);
updateYearTitleSprite(gpsYear, isRainbowTheme() ? (uint16_t)0xFFE0 : getThemeAccentColor());
}
// Draw Full-Width Date Card (Bottom) - Clean, prominent, automotive badge
// layout
inline void drawDateCardContent(int cardX, int cardY, int cardW, int cardH,
uint16_t cardBg, int x_offset = 0) {
if (cardX + cardW <= 0 || cardX >= 320)
return;
uint16_t accCol = isRainbowTheme() ? (uint16_t)0xFD20 : getThemeAccentColor();
uint16_t yearCol = isRainbowTheme() ? (uint16_t)0xFFE0 : accCol;
uint16_t cardBorder = 0x2945;
uint8_t wday = gpsDayOfWeek % 7;
uint8_t mIdx = (gpsMonth >= 1 && gpsMonth <= 12) ? (gpsMonth - 1) : 0;
String mShort = SPANISH_MONTHS_SHORT[mIdx];
String dayStr = (gpsDay < 10) ? "0" + String(gpsDay) : String(gpsDay);
// Left vertical accent bar
tft.fillRoundRect(cardX + 6, cardY + 8, 4, cardH - 16, 2, accCol);
// --- CALENDAR BADGE (Left side: 80x82 px) ---
int bx = cardX + 20;
int by = cardY + 14;
int bw = 80;
int bh = 82;
// Badge body
tft.fillRoundRect(bx, by, bw, bh, 6, 0x18C3);
tft.drawRoundRect(bx, by, bw, bh, 6, cardBorder);
// Badge top header (Month abbreviated in Accent Color)
if (isYapaTheme()) {
tft.fillRoundRect(bx, by, bw, 24, 6, 0x3A53);
tft.fillRect(bx, by + 18, bw, 6, 0x3A53);
int totalW = tft.textWidth(mShort.c_str(), 2);
int startX = bx + (bw - totalW) / 2;
char buf[2] = {0, 0};
for (size_t i = 0; i < mShort.length(); i++) {
buf[0] = mShort[i];
tft.setTextColor(getYapaColor(i), 0x3A53);
tft.drawString(buf, startX, by + 4, 2);
startX += tft.textWidth(buf, 2);
}
} else {
tft.fillRoundRect(bx, by, bw, 24, 6, accCol);
tft.fillRect(bx, by + 18, bw, 6, accCol); // flatten lower header corners
tft.setTextColor(TFT_BLACK, accCol);
tft.drawCentreString(mShort.c_str(), bx + (bw / 2), by + 4, 2);
}
// Badge body: Big Day Number in DSEG7 34pt
tft.loadFont(DSEG7_Modern_BoldItalic_34);
tft.setTextPadding(0);
if (isYapaTheme()) {
int totalW = tft.textWidth(dayStr.c_str());
int startX = bx + (bw - totalW) / 2;
char buf[2] = {0, 0};
for (size_t i = 0; i < dayStr.length(); i++) {
buf[0] = dayStr[i];
tft.setTextColor(getYapaColor(i), 0x18C3);
tft.drawString(buf, startX, by + 34);
startX += tft.textWidth(buf);
}
} else {
tft.setTextColor(TFT_WHITE, 0x18C3);
tft.drawCentreString(dayStr.c_str(), bx + (bw / 2), by + 34, 1);
}
tft.unloadFont();
// --- DATE DETAILS (Right side) ---
int tx = cardX + 116;
// Day of week in Latin Sans Bold 26 (Smooth antialiased, White, uppercase matching Page 1)
if (isYapaTheme()) {
tft.loadFont(Latin_Sans_Bold_26);
int curX = tx;
char buf[2] = {0, 0};
String dName = SPANISH_DAYS[wday];
for (size_t i = 0; i < dName.length(); i++) {
buf[0] = dName[i];
if (buf[0] == ' ') { curX += tft.textWidth(buf); continue; }
tft.setTextColor(getYapaColor(i), cardBg);
tft.drawString(buf, curX, cardY + 22);
curX += tft.textWidth(buf);
}
tft.unloadFont();
} else {
updateDayTitleSprite(wday);
if (sprDayTitle.created()) {
sprDayTitle.pushSprite(tx, cardY + 22);
} else {
tft.loadFont(Latin_Sans_Bold_26);
tft.setTextPadding(0);
tft.setTextColor(TFT_WHITE, cardBg);
tft.drawString(SPANISH_DAYS[wday], tx, cardY + 22, 1);
tft.unloadFont();
}
}
// Elegant two-tone horizontal dividing line
tft.drawFastHLine(tx, cardY + 54, 180, cardBorder);
tft.drawFastHLine(tx, cardY + 54, 50, isYapaTheme() ? getYapaColor(0) : accCol);
// Year in Latin Sans Bold 26 (Smooth antialiased, Accent color)
if (isYapaTheme()) {
tft.loadFont(Latin_Sans_Bold_26);
int curX = tx;
char buf[2] = {0, 0};
String yStr = String(gpsYear);
for (size_t i = 0; i < yStr.length(); i++) {
buf[0] = yStr[i];
tft.setTextColor(getYapaColor(i + 2), cardBg);
tft.drawString(buf, curX, cardY + 64);
curX += tft.textWidth(buf);
}
tft.unloadFont();
} else {
updateYearTitleSprite(gpsYear, yearCol);
if (sprYearTitle.created()) {
sprYearTitle.pushSprite(tx, cardY + 64);
} else {
tft.loadFont(Latin_Sans_Bold_26);
tft.setTextPadding(0);
tft.setTextColor(yearCol, cardBg);
tft.drawString(String(gpsYear).c_str(), tx, cardY + 64, 1);
tft.unloadFont();
}
}
}
// Authentic DSEG7 antialiased dot alpha matrix (from
// DSEG7_Modern_BoldItalic_80)
static const uint8_t DSEG7_DOT_ALPHA[11][13] PROGMEM = {
{0x21, 0xEB, 0xEE, 0xED, 0xEE, 0xEE, 0xEE, 0xEE, 0xEE, 0xEC, 0xEE, 0xD7,
0x0A},
{0x35, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xDC,
0x00},
{0x47, 0xFE, 0xFB, 0xFE, 0xFE, 0xFE, 0xFE, 0xFE, 0xFE, 0xFB, 0xFE, 0xC5,
0x00},
{0x5C, 0xFF, 0xFB, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFC, 0xFF, 0xB2,
0x00},
{0x71, 0xFF, 0xFB, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFB, 0xFF, 0x9E,
0x00},
{0x86, 0xFF, 0xFB, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFB, 0xFF, 0x88,
0x00},
{0x9C, 0xFF, 0xFB, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFB, 0xFF, 0x73,
0x00},
{0xB1, 0xFF, 0xFC, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFB, 0xFF, 0x5E,
0x00},
{0xC5, 0xFF, 0xFC, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFD, 0xFF, 0x49,
0x00},
{0xD7, 0xFF, 0xFD, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFE, 0xFF, 0x35,
0x00},
{0xE6, 0xFF, 0xFD, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFE, 0xFF, 0xFC, 0x24,
0x00},
};
// Helper: Draw authentic 7-segment italic colon dots with true anti-aliasing
// matching DSEG7 typography and optional rainbow gradient
template <typename T>
inline void draw7SegDot(T &dst, int x0, int y0, uint16_t color,
uint16_t bgCol, int clip_h = 240, bool rainbow = false) {
dst.startWrite();
for (int dy = 0; dy < 11; dy++) {
int curY = y0 + dy;
if (curY < 0 || curY >= clip_h)
continue;
int fl = 0;
int fxs = 0;
for (int dx = 0; dx < 13; dx++) {
uint8_t a = pgm_read_byte(&(DSEG7_DOT_ALPHA[dy][dx]));
int curX = x0 + dx;
if (a == 0) {
if (fl) {
dst.drawFastHLine(fxs, curY, fl, color);
fl = 0;
}
dst.drawPixel(curX, curY, bgCol);
} else if (a >= 235) {
if (fl == 0)
fxs = curX;
fl++;
} else {
if (fl) {
dst.drawFastHLine(fxs, curY, fl, color);
fl = 0;
}
uint16_t fg = rainbow ? getRainbowScreenColor(curX, curY) : color;
dst.drawPixel(curX, curY, dst.alphaBlend(a, fg, bgCol));
}
}
if (fl) {
if (rainbow) {
for (int i = 0; i < fl; i++) {
dst.drawPixel(fxs + i, curY, getRainbowScreenColor(fxs + i, curY));
}
} else {
dst.drawFastHLine(fxs, curY, fl, color);
}
}
}
dst.endWrite();
}
template <typename T>
inline void draw7SegColon(T &dst, int clkX, int clkY, uint16_t color,
uint16_t bgCol = 0x10A2, int clip_h = 240, bool rainbow = false) {
// Top dot: centered at clkX + 151, font row 19..29 (y = clkY + 30)
draw7SegDot(dst, clkX + 151, clkY + 30, color, bgCol, clip_h, rainbow);
// Bottom dot: centered at clkX + 148, font row 53..63 (y = clkY + 64)
draw7SegDot(dst, clkX + 148, clkY + 64, color, bgCol, clip_h, rainbow);
}
// Synchronize tracking variables without touching TFT display (zero flicker)
inline void syncGpsClockTrackers() {
oldClockHour = gpsHour;
oldClockMin = gpsMinute;
oldClockSec = gpsSecond;
oldClockDay = gpsDay;
oldClockMonth = gpsMonth;
oldClockYear = gpsYear;
}
// Draw full Page 2 (GPS Clock Screen)
inline void drawGpsClockScreen(int x_offset = 0) {
if (x_offset == 0) {
tft.fillScreen(TFT_BLACK);
} else {
tft.fillRect(0, 0, x_offset, 240, TFT_BLACK);
}
uint16_t priCol = getThemePrimaryColor();
uint16_t clkAccCol = isRainbowTheme() ? (uint16_t)0x07FF : priCol;
uint16_t cardBg = 0x10A2; // Dark graphite navy
uint16_t cardBorder = 0x2945; // Subtle edge border
bool synced = isGpsSynced();
// Center: 5 Pagination Indicator Dots [○ ○ ● ○ ○] (Page 2 of 5 active)
tft.fillCircle(132 + x_offset, 2, 2, 0x5AEB); // Page 0 inactive
tft.fillCircle(146 + x_offset, 2, 2, 0x5AEB); // Page 1 inactive
tft.fillCircle(160 + x_offset, 2, 2, priCol); // Page 2 active
tft.fillCircle(174 + x_offset, 2, 2, 0x5AEB); // Page 3 inactive
tft.fillCircle(188 + x_offset, 2, 2, 0x5AEB); // Page 4 inactive
// 2. MAIN CLOCK CARD (y = 19 to 121, height = 102, width = 312, x = 4)
int clkX = 4 + x_offset;
int clkY = 19;
int clkW = 312;
int clkH = 102;
tft.fillRoundRect(clkX, clkY, clkW, clkH, 6, cardBg);
tft.drawRoundRect(clkX, clkY, clkW, clkH, 6, cardBorder);
// Left vertical accent bar
tft.fillRoundRect(clkX + 6, clkY + 8, 4, clkH - 16, 2, clkAccCol);
// Digits: Hours HH & Minutes MM with smooth rainbow gradient
uint8_t h10 = (gpsHour / 10) % 10;
uint8_t h1 = gpsHour % 10;
uint8_t m10 = (gpsMinute / 10) % 10;
uint8_t m1 = gpsMinute % 10;
bool rbClk = isRainbowTheme();
uint16_t c_h10 = isYapaTheme() ? getYapaColor(0) : clkAccCol;
uint16_t c_h1 = isYapaTheme() ? getYapaColor(1) : clkAccCol;
uint16_t c_m10 = isYapaTheme() ? getYapaColor(2) : clkAccCol;
uint16_t c_m1 = isYapaTheme() ? getYapaColor(3) : clkAccCol;
drawDseg7Digit80(tft, clkX + 12, clkY + 11, h10, c_h10, cardBg, 240, rbClk);
drawDseg7Digit80(tft, clkX + 77, clkY + 11, h1, c_h1, cardBg, 240, rbClk);
drawDseg7Digit80(tft, clkX + 170, clkY + 11, m10, c_m10, cardBg, 240, rbClk);
drawDseg7Digit80(tft, clkX + 235, clkY + 11, m1, c_m1, cardBg, 240, rbClk);
// Blinking Colon Dots between HH and MM (italic 7-segment style with
// anti-aliasing)
bool colOn = (gpsSecond % 2 == 0);
uint16_t colonCol = colOn ? clkAccCol : 0x18C3;
draw7SegColon(tft, clkX, clkY, colonCol, cardBg, 240, colOn && rbClk);
// 3. FULL-WIDTH DATE CARD (y = 125 to 235, height = 110, width = 312, x = 4)
int dateX = 4 + x_offset;
int dateY = 125;
int dateW = 312;
int dateH = 110;
tft.fillRoundRect(dateX, dateY, dateW, dateH, 6, cardBg);
tft.drawRoundRect(dateX, dateY, dateW, dateH, 6, cardBorder);
drawDateCardContent(dateX, dateY, dateW, dateH, cardBg, x_offset);
// Sync state trackers
syncGpsClockTrackers();
}
// 100% Flicker-Free Live Refresh for resting Page 2
inline void updateGpsClockScreen(bool forceRedraw = false) {
if (currentScreenPage != 2 || fullScreenCell != -1)
return;
uint16_t priCol = getThemePrimaryColor();
uint16_t clkAccCol = isRainbowTheme() ? (uint16_t)0x07FF : priCol;
uint16_t cardBg = 0x10A2;
int clkX = 4;
int clkY = 19;
// 1. Seconds change: update colon dots only (no seconds text)
if (forceRedraw || gpsSecond != oldClockSec) {
oldClockSec = gpsSecond;
bool colOn = (gpsSecond % 2 == 0);
uint16_t colonCol = colOn ? clkAccCol : 0x18C3;
draw7SegColon(tft, clkX, clkY, colonCol, cardBg, 240, colOn && isRainbowTheme());
}
// 2. Minutes change: update MM digits
if (forceRedraw || gpsMinute != oldClockMin) {
oldClockMin = gpsMinute;
tft.fillRect(clkX + 166, clkY + 10, 142, 82, cardBg);
uint8_t m10 = (gpsMinute / 10) % 10;
uint8_t m1 = gpsMinute % 10;
uint16_t c_m10 = isYapaTheme() ? getYapaColor(2) : clkAccCol;
uint16_t c_m1 = isYapaTheme() ? getYapaColor(3) : clkAccCol;
drawDseg7Digit80(tft, clkX + 170, clkY + 11, m10, c_m10, cardBg, 240, isRainbowTheme());
drawDseg7Digit80(tft, clkX + 235, clkY + 11, m1, c_m1, cardBg, 240, isRainbowTheme());
}
// 3. Hours change: update HH digits
if (forceRedraw || gpsHour != oldClockHour) {
oldClockHour = gpsHour;
tft.fillRect(clkX + 12, clkY + 10, 136, 82, cardBg);
uint8_t h10 = (gpsHour / 10) % 10;
uint8_t h1 = gpsHour % 10;
uint16_t c_h10 = isYapaTheme() ? getYapaColor(0) : clkAccCol;
uint16_t c_h1 = isYapaTheme() ? getYapaColor(1) : clkAccCol;
drawDseg7Digit80(tft, clkX + 12, clkY + 11, h10, c_h10, cardBg, 240, isRainbowTheme());
drawDseg7Digit80(tft, clkX + 77, clkY + 11, h1, c_h1, cardBg, 240, isRainbowTheme());
}
// 4. Date changed (midnight rollover): redraw full date card
if (forceRedraw || gpsDay != oldClockDay || gpsMonth != oldClockMonth ||
gpsYear != oldClockYear) {
oldClockDay = gpsDay;
oldClockMonth = gpsMonth;
oldClockYear = gpsYear;
int dateX = 4;
int dateY = 125;
int dateW = 312;
int dateH = 110;
tft.fillRoundRect(dateX, dateY, dateW, dateH, 6, cardBg);
tft.drawRoundRect(dateX, dateY, dateW, dateH, 6, 0x2945);
drawDateCardContent(dateX, dateY, dateW, dateH, cardBg, 0);
}
}
// Render Page 2 into double-buffered sprite slice for slide / fade transitions
inline void renderPage2ToSprite(TFT_eSprite &s, int x_offset, int y_slice,
int slice_h) {
uint16_t priCol = getThemePrimaryColor();
uint16_t accCol = getThemeAccentColor();
uint16_t clkAccCol = isRainbowTheme() ? (uint16_t)0x07FF : priCol;
uint16_t cardBg = 0x10A2;
uint16_t cardBorder = 0x2945;
// 1. TOP BAR in top slice (pagination dots rendered by caller or top bar)
// 2. MAIN CLOCK CARD (y = 19 to 121)
int clkX = 4 + x_offset;
int clkY = 19 - y_slice;
int clkW = 312;
int clkH = 102;
if (clkX + clkW > 0 && clkX < 320 && clkY + clkH > 0 && clkY < slice_h) {
s.fillRoundRect(clkX, clkY, clkW, clkH, 6, cardBg);
s.drawRoundRect(clkX, clkY, clkW, clkH, 6, cardBorder);
s.fillRoundRect(clkX + 6, clkY + 8, 4, clkH - 16, 2, clkAccCol);
// Colon dots during transition (italic 7-segment style with anti-aliasing)
draw7SegColon(s, clkX, clkY, clkAccCol, cardBg, slice_h, isRainbowTheme());
// Digits: Hours HH (Right aligned to clkX + 142) & Minutes MM (Left aligned
// to clkX + 170)
int digitY = clkY + 11;
if (digitY + 80 > 0 && digitY < slice_h) {
uint8_t h10 = (gpsHour / 10) % 10;
uint8_t h1 = gpsHour % 10;
uint8_t m10 = (gpsMinute / 10) % 10;
uint8_t m1 = gpsMinute % 10;
uint16_t c_h10 = isYapaTheme() ? getYapaColor(0) : clkAccCol;
uint16_t c_h1 = isYapaTheme() ? getYapaColor(1) : clkAccCol;
uint16_t c_m10 = isYapaTheme() ? getYapaColor(2) : clkAccCol;
uint16_t c_m1 = isYapaTheme() ? getYapaColor(3) : clkAccCol;
drawDseg7Digit80(s, clkX + 12, digitY, h10, c_h10, cardBg, slice_h, isRainbowTheme());
drawDseg7Digit80(s, clkX + 77, digitY, h1, c_h1, cardBg, slice_h, isRainbowTheme());
drawDseg7Digit80(s, clkX + 170, digitY, m10, c_m10, cardBg, slice_h, isRainbowTheme());
drawDseg7Digit80(s, clkX + 235, digitY, m1, c_m1, cardBg, slice_h, isRainbowTheme());
}
}
// 3. FULL-WIDTH DATE CARD (y = 125 to 235) - Calendar Badge + Details
int dateX = 4 + x_offset;
int dateY = 125 - y_slice;
int dateW = 312;
int dateH = 110;
if (dateX + dateW > 0 && dateX < 320 && dateY + dateH > 0 &&
dateY < slice_h) {
uint16_t dateAccCol = isRainbowTheme() ? (uint16_t)0xFD20 : accCol;
uint16_t yearCol = isRainbowTheme() ? (uint16_t)0xFFE0 : accCol;
s.fillRoundRect(dateX, dateY, dateW, dateH, 6, cardBg);
s.drawRoundRect(dateX, dateY, dateW, dateH, 6, cardBorder);
s.fillRoundRect(dateX + 6, dateY + 8, 4, dateH - 16, 2, dateAccCol);
// Badge container
int bx = dateX + 20;
int by = dateY + 14;
int bw = 80;
int bh = 82;
if (by + bh > 0 && by < slice_h) {
s.fillRoundRect(bx, by, bw, bh, 6, 0x18C3);
s.drawRoundRect(bx, by, bw, bh, 6, cardBorder);
if (isYapaTheme()) {
s.fillRoundRect(bx, by, bw, 24, 6, 0x3A53);
s.fillRect(bx, by + 18, bw, 6, 0x3A53);
uint8_t mIdx = (gpsMonth >= 1 && gpsMonth <= 12) ? (gpsMonth - 1) : 0;
String mShort = SPANISH_MONTHS_SHORT[mIdx];
int totalW = s.textWidth(mShort.c_str(), 2);
int startX = bx + (bw - totalW) / 2;
char buf[2] = {0, 0};
for (size_t i = 0; i < mShort.length(); i++) {
buf[0] = mShort[i];
s.setTextColor(getYapaColor(i), 0x3A53);
s.drawString(buf, startX, by + 4, 2);
startX += s.textWidth(buf, 2);
}
// Badge day number in Yapa
String dayStr = (gpsDay < 10) ? "0" + String(gpsDay) : String(gpsDay);
s.loadFont(DSEG7_Modern_BoldItalic_34);
s.setTextPadding(0);
int dayW = s.textWidth(dayStr.c_str());
int dStartX = bx + (bw - dayW) / 2;
for (size_t i = 0; i < dayStr.length(); i++) {
buf[0] = dayStr[i];
s.setTextColor(getYapaColor(i), 0x18C3);
s.drawString(buf, dStartX, by + 34);
dStartX += s.textWidth(buf);
}
s.unloadFont();
} else {
s.fillRoundRect(bx, by, bw, 24, 6, dateAccCol);
s.fillRect(bx, by + 18, bw, 6, dateAccCol);
uint8_t mIdx = (gpsMonth >= 1 && gpsMonth <= 12) ? (gpsMonth - 1) : 0;
s.setTextColor(TFT_BLACK, dateAccCol);
s.drawCentreString(SPANISH_MONTHS_SHORT[mIdx], bx + (bw / 2), by + 4, 2);
// Badge day number
String dayStr = (gpsDay < 10) ? "0" + String(gpsDay) : String(gpsDay);
s.fontLoaded = true;
s.setTextColor(TFT_WHITE, 0x18C3);
s.setTextPadding(0);
s.drawCentreString(dayStr.c_str(), bx + (bw / 2), by + 34, 1);
s.fontLoaded = false;
}
}
int tx = dateX + 116;
uint8_t wday = gpsDayOfWeek % 7;
int dwY = dateY + 22;
if (dwY + 26 > 0 && dwY < slice_h) {
updateDayTitleSprite(wday);
if (sprDayTitle.created()) {
sprDayTitle.pushToSprite(&s, tx, dwY);
} else {
s.setTextColor(TFT_WHITE, cardBg);
s.drawString(SPANISH_DAYS[wday], tx, dwY, 4);
}
}
int divY = dateY + 54;
if (divY > 0 && divY < slice_h) {
s.drawFastHLine(tx, divY, 180, cardBorder);
s.drawFastHLine(tx, divY, 50, dateAccCol);
}
int yrY = dateY + 64;
if (yrY + 26 > 0 && yrY < slice_h) {
updateYearTitleSprite(gpsYear, yearCol);
if (sprYearTitle.created()) {
sprYearTitle.pushToSprite(&s, tx, yrY);
} else {
s.setTextColor(yearCol, cardBg);
s.drawString(String(gpsYear).c_str(), tx, yrY, 4);
}
}
}
}