From db38f557a1af4ffb676bbe2f23b6a50eb4a05964 Mon Sep 17 00:00:00 2001 From: Sebastian Nieto Date: Mon, 21 Sep 2026 23:29:31 -0300 Subject: [PATCH] feat(ui): implementar doble buffer (TFT_eSprite) para transicion 100% libre de parpadeo --- sketch/obd2_gauge/meter.h | 235 ++++++++++++++++++++++++-------------- 1 file changed, 152 insertions(+), 83 deletions(-) diff --git a/sketch/obd2_gauge/meter.h b/sketch/obd2_gauge/meter.h index 37446a8..58fde35 100644 --- a/sketch/obd2_gauge/meter.h +++ b/sketch/obd2_gauge/meter.h @@ -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) -inline void drawSlidePage0(int x_offset) { - tft.fillRect(x_offset, 0, 320, 240, TFT_BLACK); - +// Render Page 0 (4 Quadrants) into double-buffered Sprite slice +inline void renderPage0ToSprite(TFT_eSprite &s, int x_offset, int y_slice, int slice_h) { uint16_t bgCol = getThemeBgColor(); uint16_t divCol = getThemeDivColor(); uint16_t priCol = getThemePrimaryColor(); - // 4 Quadrants for (uint8_t c = 0; c < 4; c++) { 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 ry = (c & 2) ? 121 : 0; + int ry = ((c & 2) ? 121 : 0) - y_slice; - // Fast hardware fill for quadrant background - tft.fillRect(rx, ry, 159, 119, bgCol); + if (qx + 160 <= 0 || qx >= 320 || ry + 119 <= 0 || ry >= slice_h) continue; + + s.fillRect(rx, ry, 159, 119, bgCol); uint8_t pidIdx = pidInCell[c]; - - // Fast header label in font 2 - tft.setTextColor(TFT_WHITE, bgCol); - tft.drawString(pidConfig[pidIdx][0].c_str(), qx + 8, qy + 6, 2); + s.setTextColor(TFT_WHITE, bgCol); + s.drawString(pidConfig[pidIdx][0].c_str(), qx + 8, qy + 6, 2); int warn = warningValue[pidIdx].toInt(); int min = pidConfig[pidIdx][4].toInt(); @@ -1266,9 +1262,8 @@ inline void drawSlidePage0(int x_offset) { bool digit = pidConfig[pidIdx][7].toInt(); float val = last_valid_data[c]; - // Horizontal gauge bar 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) { uint16_t col = getMeterColor(pidIdx, val, warn); float ratio = (val - (float)min) / (float)(max - min); @@ -1277,88 +1272,162 @@ inline void drawSlidePage0(int x_offset) { int fillW = (int)(ratio * 144.0f); if (fillW < 4) fillW = 4; 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); - tft.loadFont(DSEG7_Modern_BoldItalic_34); - tft.setTextColor(priCol, bgCol); - tft.setTextPadding(114); - tft.drawRightString(numStr.c_str(), qx + 152, qy + 70, 1); - tft.setTextPadding(0); - tft.unloadFont(); + s.loadFont(DSEG7_Modern_BoldItalic_34); + s.setTextColor(priCol, bgCol); + s.setTextPadding(114); + s.drawRightString(numStr.c_str(), qx + 152, qy + 70, 1); + s.setTextPadding(0); + s.unloadFont(); } // Crosshair dividers - tft.fillRect(159 + x_offset, 0, 2, 240, divCol); - tft.fillRect(x_offset, 119, 320, 2, divCol); + s.fillRect(159 + x_offset, 0, 2, slice_h, 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) { 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) { - // SWIPE LEFT: Page 0 slides out to the left, Page 1 enters from the right - for (uint8_t f = 0; f < FRAMES; f++) { - int16_t shift = step_shifts[f]; - int16_t splitX = 320 - shift; - - if (splitX > 0) { - 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_eSprite spr(&tft); + int slice_h = 240; + // Try 320x240 full double-buffer first; if heap is tight, allocate two 320x120 slices + if (spr.createSprite(320, 240) == nullptr) { + slice_h = 120; + if (spr.createSprite(320, 120) == nullptr) { + return; // Fallback: switch instantly without crashing if RAM is fully depleted } } - 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(); } /*-------------------*/