feat(ui): implementar doble buffer (TFT_eSprite) para transicion 100% libre de parpadeo

This commit is contained in:
Sebastián Nieto 2026-09-21 23:29:31 -03:00
parent 7eb50cd389
commit db38f557a1

View file

@ -1236,29 +1236,25 @@ void plotNoData(uint8_t cell) {
} }
} }
// Fast slide-frame rendering of Page 0 (4 Quadrants) shifted by x_offset (optimized for 60 FPS) // Render Page 0 (4 Quadrants) into double-buffered Sprite slice
inline void drawSlidePage0(int x_offset) { inline void renderPage0ToSprite(TFT_eSprite &s, int x_offset, int y_slice, int slice_h) {
tft.fillRect(x_offset, 0, 320, 240, TFT_BLACK);
uint16_t bgCol = getThemeBgColor(); uint16_t bgCol = getThemeBgColor();
uint16_t divCol = getThemeDivColor(); uint16_t divCol = getThemeDivColor();
uint16_t priCol = getThemePrimaryColor(); uint16_t priCol = getThemePrimaryColor();
// 4 Quadrants
for (uint8_t c = 0; c < 4; c++) { for (uint8_t c = 0; c < 4; c++) {
int qx = cell4_x[c] + x_offset; int qx = cell4_x[c] + x_offset;
int qy = cell4_y[c]; int qy = cell4_y[c] - y_slice;
int rx = (c & 1) ? (161 + x_offset) : (0 + x_offset); int rx = (c & 1) ? (161 + x_offset) : (0 + x_offset);
int ry = (c & 2) ? 121 : 0; int ry = ((c & 2) ? 121 : 0) - y_slice;
// Fast hardware fill for quadrant background if (qx + 160 <= 0 || qx >= 320 || ry + 119 <= 0 || ry >= slice_h) continue;
tft.fillRect(rx, ry, 159, 119, bgCol);
s.fillRect(rx, ry, 159, 119, bgCol);
uint8_t pidIdx = pidInCell[c]; uint8_t pidIdx = pidInCell[c];
s.setTextColor(TFT_WHITE, bgCol);
// Fast header label in font 2 s.drawString(pidConfig[pidIdx][0].c_str(), qx + 8, qy + 6, 2);
tft.setTextColor(TFT_WHITE, bgCol);
tft.drawString(pidConfig[pidIdx][0].c_str(), qx + 8, qy + 6, 2);
int warn = warningValue[pidIdx].toInt(); int warn = warningValue[pidIdx].toInt();
int min = pidConfig[pidIdx][4].toInt(); int min = pidConfig[pidIdx][4].toInt();
@ -1266,9 +1262,8 @@ inline void drawSlidePage0(int x_offset) {
bool digit = pidConfig[pidIdx][7].toInt(); bool digit = pidConfig[pidIdx][7].toInt();
float val = last_valid_data[c]; float val = last_valid_data[c];
// Horizontal gauge bar
int barY = qy + 28; int barY = qy + 28;
tft.fillRoundRect(qx + 8, barY, 144, 6, 2, METER_INACTIVE_COLOR); s.fillRoundRect(qx + 8, barY, 144, 6, 2, METER_INACTIVE_COLOR);
if (val > -900.0f) { if (val > -900.0f) {
uint16_t col = getMeterColor(pidIdx, val, warn); uint16_t col = getMeterColor(pidIdx, val, warn);
float ratio = (val - (float)min) / (float)(max - min); float ratio = (val - (float)min) / (float)(max - min);
@ -1277,88 +1272,162 @@ inline void drawSlidePage0(int x_offset) {
int fillW = (int)(ratio * 144.0f); int fillW = (int)(ratio * 144.0f);
if (fillW < 4) fillW = 4; if (fillW < 4) fillW = 4;
if (fillW > 144) fillW = 144; if (fillW > 144) fillW = 144;
tft.fillRoundRect(qx + 8, barY, fillW, 6, 2, col); s.fillRoundRect(qx + 8, barY, fillW, 6, 2, col);
} }
// Digital value using DSEG7 Modern 34pt
String numStr = (val <= -900.0f) ? "---" : String(val, digit); String numStr = (val <= -900.0f) ? "---" : String(val, digit);
tft.loadFont(DSEG7_Modern_BoldItalic_34); s.loadFont(DSEG7_Modern_BoldItalic_34);
tft.setTextColor(priCol, bgCol); s.setTextColor(priCol, bgCol);
tft.setTextPadding(114); s.setTextPadding(114);
tft.drawRightString(numStr.c_str(), qx + 152, qy + 70, 1); s.drawRightString(numStr.c_str(), qx + 152, qy + 70, 1);
tft.setTextPadding(0); s.setTextPadding(0);
tft.unloadFont(); s.unloadFont();
} }
// Crosshair dividers // Crosshair dividers
tft.fillRect(159 + x_offset, 0, 2, 240, divCol); s.fillRect(159 + x_offset, 0, 2, slice_h, divCol);
tft.fillRect(x_offset, 119, 320, 2, divCol); int hLineY = 119 - y_slice;
if (hLineY >= 0 && hLineY < slice_h) {
s.fillRect(x_offset, hLineY, 320, 2, divCol);
}
} }
// Butter-smooth mobile slide transition with 14-frame ease-out deceleration // Render Page 1 (3 Temperature Cards) into double-buffered Sprite slice
inline void renderPage1ToSprite(TFT_eSprite &s, int x_offset, int y_slice, int slice_h) {
for (uint8_t cardIdx = 0; cardIdx < 3; cardIdx++) {
int cardY = (6 + cardIdx * 77) - y_slice;
int cardH = 73;
int cardX = 4 + x_offset;
int cardW = 312;
if (cardX + cardW <= 0 || cardX >= 320 || cardY + cardH <= 0 || cardY >= slice_h) continue;
uint16_t cardBg = 0x10A2;
uint16_t cardBorder = 0x2945;
s.fillRoundRect(cardX, cardY, cardW, cardH, 6, cardBg);
s.drawRoundRect(cardX, cardY, cardW, cardH, 6, cardBorder);
uint16_t accentCol = getThemePrimaryColor();
const char* titleStr = "";
const char* subStr = "";
float val = 0.0f;
float minRange = 0.0f, maxRange = 100.0f;
bool isInt = false;
if (cardIdx == 0) {
titleStr = "MOTOR / REFRIGERANTE";
subStr = ignitionOn ? "ECU OBD-II (PID 0105)" : "ECU OBD-II (STANDBY)";
val = tempEngineCoolant;
minRange = 60.0f; maxRange = 120.0f; isInt = true;
if (val >= 110.0f) accentCol = TFT_RED;
else if (val >= 100.0f) accentCol = TFT_YELLOW;
} else if (cardIdx == 1) {
titleStr = "HABITACULO / INTERIOR";
subStr = (dsSensorCount > 0) ? "DS18B20 SENSOR 1" : "DS18B20 SENSOR 1 (DEMO)";
val = tempInterior;
minRange = 10.0f; maxRange = 40.0f;
accentCol = 0x07F3;
} else {
titleStr = "AMBIENTE / EXTERIOR";
subStr = (dsSensorCount > 1) ? "DS18B20 SENSOR 2" : "DS18B20 SENSOR 2 (DEMO)";
val = tempExterior;
minRange = -10.0f; maxRange = 50.0f;
accentCol = 0x3D7F;
}
s.fillRoundRect(cardX + 6, cardY + 8, 4, cardH - 16, 2, accentCol);
s.setTextColor(TFT_WHITE, cardBg);
s.drawString(titleStr, cardX + 16, cardY + 8, 2);
s.setTextColor(0x8410, cardBg);
s.drawString(subStr, cardX + 16, cardY + 28, 1);
// Progress bar
int barX = cardX + 16;
int barY = cardY + 50;
int barW = 120;
s.fillRoundRect(barX, barY, barW, 8, 3, 0x18C3);
float ratio = (val - minRange) / (maxRange - minRange);
if (ratio < 0.0f) ratio = 0.0f;
if (ratio > 1.0f) ratio = 1.0f;
int fillW = (int)(barW * ratio);
if (fillW < 4) fillW = 4;
s.fillRoundRect(barX, barY, fillW, 8, 3, accentCol);
// Value text
String numStr = (val <= -900.0f) ? "---" : (isInt ? String((int)val) : String(val, 1));
s.loadFont(DSEG7_Modern_BoldItalic_34);
s.setTextColor(accentCol, cardBg);
s.setTextPadding(100);
s.drawRightString(numStr.c_str(), cardX + 262, cardY + 22, 1);
s.setTextPadding(0);
s.unloadFont();
s.setTextColor(0xC618, cardBg);
s.drawString("`C", cardX + 268, cardY + 32, 2);
}
}
// 100% Flicker-Free Double-Buffered Mobile Slide Transition
inline void animateSlideTransition(bool swipeLeft) { inline void animateSlideTransition(bool swipeLeft) {
touchBeep(); touchBeep();
const uint8_t FRAMES = 14;
// Cubic ease-out deceleration curve: fast initial departure, silk-smooth docking
const int16_t step_shifts[FRAMES] = { 38, 74, 108, 140, 170, 198, 224, 248, 268, 285, 298, 308, 316, 320 };
if (swipeLeft) { TFT_eSprite spr(&tft);
// SWIPE LEFT: Page 0 slides out to the left, Page 1 enters from the right int slice_h = 240;
for (uint8_t f = 0; f < FRAMES; f++) { // Try 320x240 full double-buffer first; if heap is tight, allocate two 320x120 slices
int16_t shift = step_shifts[f]; if (spr.createSprite(320, 240) == nullptr) {
int16_t splitX = 320 - shift; slice_h = 120;
if (spr.createSprite(320, 120) == nullptr) {
if (splitX > 0) { return; // Fallback: switch instantly without crashing if RAM is fully depleted
tft.setViewport(0, 0, splitX, 240, false);
drawSlidePage0(-shift);
}
if (splitX < 320) {
tft.setViewport(splitX, 0, 320 - splitX, 240, false);
drawSlidePage1(splitX);
}
tft.resetViewport();
if (splitX > 0 && splitX < 320) {
tft.drawFastVLine(splitX, 0, 240, getThemePrimaryColor());
tft.drawFastVLine(splitX + 1, 0, 240, TFT_WHITE);
}
tft.fillCircle(153, 2, 2, (splitX > 160) ? getThemePrimaryColor() : 0x5AEB);
tft.fillCircle(167, 2, 2, (splitX <= 160) ? getThemePrimaryColor() : 0x5AEB);
delay(6);
}
} else {
// SWIPE RIGHT: Page 1 slides out to the right, Page 0 enters from the left
for (uint8_t f = 0; f < FRAMES; f++) {
int16_t shift = step_shifts[f];
int16_t splitX = shift;
if (splitX > 0) {
tft.setViewport(0, 0, splitX, 240, false);
drawSlidePage0(splitX - 320);
}
if (splitX < 320) {
tft.setViewport(splitX, 0, 320 - splitX, 240, false);
drawSlidePage1(splitX);
}
tft.resetViewport();
if (splitX > 0 && splitX < 320) {
tft.drawFastVLine(splitX, 0, 240, getThemePrimaryColor());
tft.drawFastVLine(splitX - 1, 0, 240, TFT_WHITE);
}
tft.fillCircle(153, 2, 2, (splitX >= 160) ? getThemePrimaryColor() : 0x5AEB);
tft.fillCircle(167, 2, 2, (splitX < 160) ? getThemePrimaryColor() : 0x5AEB);
delay(6);
} }
} }
tft.resetViewport(); const uint8_t FRAMES = 10;
// Smooth cubic ease-out deceleration curve
const int16_t step_shifts[FRAMES] = { 45, 92, 140, 186, 228, 264, 290, 308, 317, 320 };
uint8_t numSlices = (slice_h == 240) ? 1 : 2;
for (uint8_t f = 0; f < FRAMES; f++) {
int16_t shift = step_shifts[f];
int16_t splitX = swipeLeft ? (320 - shift) : shift;
int16_t x0 = swipeLeft ? (-shift) : (shift - 320);
int16_t x1 = swipeLeft ? (320 - shift) : shift;
for (uint8_t s = 0; s < numSlices; s++) {
int y_slice = s * slice_h;
spr.fillSprite(TFT_BLACK);
if (splitX > 0) {
spr.setViewport(0, 0, splitX, slice_h, false);
renderPage0ToSprite(spr, x0, y_slice, slice_h);
}
if (splitX < 320) {
spr.setViewport(splitX, 0, 320 - splitX, slice_h, false);
renderPage1ToSprite(spr, x1, y_slice, slice_h);
}
spr.resetViewport();
// Dividing neon seam line
if (splitX > 0 && splitX < 320) {
spr.drawFastVLine(splitX, 0, slice_h, getThemePrimaryColor());
spr.drawFastVLine(splitX + (swipeLeft ? 1 : -1), 0, slice_h, TFT_WHITE);
}
// Pagination indicator dots on top slice
if (y_slice == 0) {
spr.fillCircle(153, 2, 2, (splitX > 160) ? getThemePrimaryColor() : 0x5AEB);
spr.fillCircle(167, 2, 2, (splitX <= 160) ? getThemePrimaryColor() : 0x5AEB);
}
// Direct single SPI push: ZERO visual flicker!
spr.pushSprite(0, y_slice);
}
delay(8);
}
spr.deleteSprite();
} }
/*-------------------*/ /*-------------------*/