diff --git a/workspace/TS100/Core/Inc/OLED.hpp b/workspace/TS100/Core/Inc/OLED.hpp index f65216c0..5994e9f6 100644 --- a/workspace/TS100/Core/Inc/OLED.hpp +++ b/workspace/TS100/Core/Inc/OLED.hpp @@ -79,7 +79,7 @@ public: // Draws a number at the current cursor location // Clears the buffer static void clearScreen() { - memset(&screenBuffer[FRAMEBUFFER_START], 0, OLED_WIDTH * 2); + memset(firstStripPtr, 0, OLED_WIDTH * 2); } // Draws the battery level symbol static void drawBattery(uint8_t state) { @@ -101,8 +101,11 @@ public: bool clear); static void drawHeatSymbol(uint8_t state); static void drawScrollIndicator(uint8_t p, uint8_t h); // Draws a scrolling position indicator + static void transitionSecondaryFramebuffer(bool forwardNavigation); + static void useSecondaryFramebuffer(bool useSecondary); private: static void drawChar(char c); // Draw a character to a specific location + static void setFramebuffer(uint8_t *buffer); static const uint8_t* currentFont;// Pointer to the current font used for rendering to the buffer static uint8_t* firstStripPtr; // Pointers to the strips to allow for buffer having extra content static uint8_t* secondStripPtr; //Pointers to the strips @@ -112,6 +115,7 @@ private: static int16_t cursor_x, cursor_y; static uint8_t displayOffset; static uint8_t screenBuffer[16 + (OLED_WIDTH * 2) + 10]; // The data buffer + static uint8_t secondFrameBuffer[OLED_WIDTH * 2]; }; #endif /* OLED_HPP_ */ diff --git a/workspace/TS100/Core/Src/OLED.cpp b/workspace/TS100/Core/Src/OLED.cpp index 31c5f38b..3949b17a 100644 --- a/workspace/TS100/Core/Src/OLED.cpp +++ b/workspace/TS100/Core/Src/OLED.cpp @@ -24,6 +24,7 @@ uint8_t OLED::fontWidth, OLED::fontHeight; int16_t OLED::cursor_x, OLED::cursor_y; uint8_t OLED::displayOffset; uint8_t OLED::screenBuffer[16 + (OLED_WIDTH * 2) + 10]; // The data buffer +uint8_t OLED::secondFrameBuffer[OLED_WIDTH * 2]; /*Setup params for the OLED screen*/ /*http://www.displayfuture.com/Display/datasheet/controller/SSD1307.pdf*/ @@ -62,6 +63,27 @@ uint8_t OLED_Setup_Array[] = { const uint8_t REFRESH_COMMANDS[17] = { 0x80, 0xAF, 0x80, 0x21, 0x80, 0x20, 0x80, 0x7F, 0x80, 0xC0, 0x80, 0x22, 0x80, 0x00, 0x80, 0x01, 0x40 }; + +/* + * Animation timing function that follows a bezier curve. + * @param t A given percentage value [0..<100] + * Returns a new percentage value with ease in and ease out. + * Original floating point formula: t * t * (3.0f - 2.0f * t); + */ +static uint8_t easeInOutTiming(uint8_t t) { + return t * t * (300 - 2 * t) / 10000; +} + +/* + * Returns the value between a and b, using a percentage value t. + * @param a The value associated with 0% + * @param b The value associated with 100% + * @param t The percentage [0..<100] + */ +static uint8_t lerp(uint8_t a, uint8_t b, uint8_t t) { + return a + t * (b - a) / 100; +} + void OLED::initialize() { cursor_x = cursor_y = 0; currentFont = USER_FONT_12; @@ -85,6 +107,17 @@ void OLED::initialize() { sizeof(OLED_Setup_Array)); } +void OLED::setFramebuffer(uint8_t *buffer) { + if (buffer == NULL) { + firstStripPtr = &screenBuffer[FRAMEBUFFER_START]; + secondStripPtr = &screenBuffer[FRAMEBUFFER_START + OLED_WIDTH]; + return; + } + + firstStripPtr = &buffer[0]; + secondStripPtr = &buffer[OLED_WIDTH]; +} + /* * Prints a char to the screen. * UTF font handling is done using the two input chars. @@ -125,6 +158,62 @@ void OLED::drawScrollIndicator(uint8_t y, uint8_t height) { fillArea(OLED_WIDTH - 1, 8, 1, 8, column.strips[1]); } +/** + * Plays a transition animation between two framebuffers. + * @param forwardNavigation Direction of the navigation animation. + * + * If forward is true, this displays a forward navigation to the second framebuffer contents. + * Otherwise a rewinding navigation animation is shown to the second framebuffer contents. + */ +void OLED::transitionSecondaryFramebuffer(bool forwardNavigation) { + uint8_t *firstBackStripPtr = &secondFrameBuffer[0]; + uint8_t *secondBackStripPtr = &secondFrameBuffer[OLED_WIDTH]; + + uint32_t totalDuration = 50; // 500ms + uint32_t duration = 0; + uint32_t start = xTaskGetTickCount(); + uint8_t offset = 0; + + while (duration <= totalDuration) { + duration = xTaskGetTickCount() - start; + uint8_t progress = duration * 100 / totalDuration; + progress = easeInOutTiming(progress); + progress = lerp(0, OLED_WIDTH, progress); + if (progress > OLED_WIDTH) { + progress = OLED_WIDTH; + } + + // When forward, current contents move to the left out. + // Otherwise the contents move to the right out. + uint8_t oldStart = forwardNavigation ? 0 : progress; + uint8_t oldPrevious = forwardNavigation ? progress - offset : offset; + + // Content from the second framebuffer moves in from the right (forward) + // or from the left (not forward). + uint8_t newStart = forwardNavigation ? OLED_WIDTH - progress : 0; + uint8_t newEnd = forwardNavigation ? 0 : OLED_WIDTH - progress; + + offset = progress; + + memmove(&firstStripPtr[oldStart], &firstStripPtr[oldPrevious], OLED_WIDTH - progress); + memmove(&secondStripPtr[oldStart], &secondStripPtr[oldPrevious], OLED_WIDTH - progress); + + memmove(&firstStripPtr[newStart], &firstBackStripPtr[newEnd], progress); + memmove(&secondStripPtr[newStart], &secondBackStripPtr[newEnd], progress); + + refresh(); + osDelay(40); + } +} + +void OLED::useSecondaryFramebuffer(bool useSecondary) { + if (useSecondary) { + setFramebuffer(secondFrameBuffer); + } else { + setFramebuffer(NULL); + } +} + void OLED::setRotation(bool leftHanded) { #ifdef MODEL_TS80 leftHanded = !leftHanded; diff --git a/workspace/TS100/Core/Src/gui.cpp b/workspace/TS100/Core/Src/gui.cpp index afa063d2..a38aae4d 100644 --- a/workspace/TS100/Core/Src/gui.cpp +++ b/workspace/TS100/Core/Src/gui.cpp @@ -826,12 +826,29 @@ void gui_Menu(const menuitem *menu) { int16_t lastOffset = -1; bool lcdRefresh = true; ButtonState lastButtonState = BUTTON_NONE; + static bool enterGUIMenu = true; + enterGUIMenu = true; uint8_t scrollContentSize = 0; - + for (uint8_t i = 0; menu[i].draw.func != NULL; i++) { scrollContentSize += 1; } + // Animated menu opening. + if (menu[currentScreen].draw.func != NULL) { + // This menu is drawn in a secondary framebuffer. + // Then we play a transition from the current primary + // framebuffer to the new buffer. + // The extra buffer is discarded at the end of the transition. + OLED::useSecondaryFramebuffer(true); + OLED::setFont(0); + OLED::setCursor(0, 0); + OLED::clearScreen(); + menu[currentScreen].draw.func(); + OLED::useSecondaryFramebuffer(false); + OLED::transitionSecondaryFramebuffer(true); + } + while ((menu[currentScreen].draw.func != NULL) && earlyExit == false) { OLED::setFont(0); OLED::setCursor(0, 0); @@ -882,10 +899,23 @@ void gui_Menu(const menuitem *menu) { case BUTTON_F_SHORT: // increment if (descriptionStart == 0) { - if (menu[currentScreen].incrementHandler.func != NULL) + if (menu[currentScreen].incrementHandler.func != NULL) { + enterGUIMenu = false; menu[currentScreen].incrementHandler.func(); - else + + if (enterGUIMenu) { + OLED::useSecondaryFramebuffer(true); + OLED::setFont(0); + OLED::setCursor(0, 0); + OLED::clearScreen(); + menu[currentScreen].draw.func(); + OLED::useSecondaryFramebuffer(false); + OLED::transitionSecondaryFramebuffer(false); + } + enterGUIMenu = true; + } else { earlyExit = true; + } } else descriptionStart = 0; break; @@ -931,7 +961,7 @@ void gui_Menu(const menuitem *menu) { osDelay(40); lcdRefresh = false; } - if ((xTaskGetTickCount() - lastButtonTime) > (1000 * 30)) { + if ((xTaskGetTickCount() - lastButtonTime) > (100 * 30)) { // If user has not pressed any buttons in 30 seconds, exit back a menu layer // This will trickle the user back to the main screen eventually earlyExit = true;