diff --git a/Marlin/Configuration_adv.h b/Marlin/Configuration_adv.h index f0066338c177..81e8c4058e45 100644 --- a/Marlin/Configuration_adv.h +++ b/Marlin/Configuration_adv.h @@ -2015,17 +2015,17 @@ //#define STATUS_HEAT_PERCENT // Show heating in a progress bar //#define STATUS_HEAT_POWER // Show heater output power as a vertical bar - // Frivolous Game Options - //#define MARLIN_BRICKOUT - //#define MARLIN_INVADERS - //#define MARLIN_SNAKE - //#define GAMES_EASTER_EGG // Add extra blank lines above the "Games" sub-menu - #endif // HAS_MARLINUI_U8GLIB #if HAS_MARLINUI_U8GLIB || IS_DWIN_MARLINUI #define MENU_HOLLOW_FRAME // Enable to save many cycles by drawing a hollow frame on Menu Screens //#define OVERLAY_GFX_REVERSE // Swap the CW/CCW indicators in the graphics overlay + + // Frivolous Game Options + //#define MARLIN_BRICKOUT + //#define MARLIN_INVADERS + //#define MARLIN_SNAKE + //#define GAMES_EASTER_EGG // Add extra blank lines above the "Games" sub-menu #endif // diff --git a/Marlin/src/lcd/dogm/game.cpp b/Marlin/src/lcd/dogm/game.cpp new file mode 100644 index 000000000000..73c98aec52f4 --- /dev/null +++ b/Marlin/src/lcd/dogm/game.cpp @@ -0,0 +1,61 @@ +#include "../../inc/MarlinConfigPre.h" + +#if HAS_MARLINUI_U8GLIB && HAS_GAMES + +#include "../menu/game/types.h" // includes dogm/game.h + +void MarlinGame::frame_start() {} + +void MarlinGame::frame_end() {} + +void MarlinGame::set_color(const color color) { + switch(color) + { + case color::BLACK: + u8g.setColorIndex(0); + break; + case color::WHITE: + default: + u8g.setColorIndex(1); + break; + } +} + +void MarlinGame::draw_hline(const game_dim_t x, const game_dim_t y, const game_dim_t w) { + u8g.drawHLine(x, y, w); +} + +void MarlinGame::draw_vline(const game_dim_t x, const game_dim_t y, const game_dim_t h) { + u8g.drawVLine(x, y, h); +} + +void MarlinGame::draw_frame(const game_dim_t x, const game_dim_t y, const game_dim_t w, const game_dim_t h) { + u8g.drawFrame(x, y, w, h); +} + +void MarlinGame::draw_box(const game_dim_t x, const game_dim_t y, const game_dim_t w, const game_dim_t h) { + u8g.drawBox(x, y, w, h); +} + +void MarlinGame::draw_pixel(const game_dim_t x, const game_dim_t y) { + u8g.drawPixel(x, y); +} + +void MarlinGame::draw_bitmap(const game_dim_t x, const game_dim_t y, const game_dim_t bytes_per_row, const game_dim_t rows, const pgm_bitmap_t bitmap) { + u8g.drawBitmapP(x, y, bytes_per_row, rows, bitmap); +} + +int MarlinGame::draw_string(const game_dim_t x, const game_dim_t y, const char* str) { + lcd_moveto(x, y); + return lcd_put_u8str_P(str); +} + +int MarlinGame::draw_string(const game_dim_t x, const game_dim_t y, FSTR_P const fstr) { + lcd_moveto(x, y); + return lcd_put_u8str(fstr); +} + +void MarlinGame::draw_int(const game_dim_t x, const game_dim_t y, const int value) { + lcd_put_int(x, y, value); +} +#endif // HAS_MARLINUI_U8GLIB && HAS_GAMES diff --git a/Marlin/src/lcd/dogm/game.h b/Marlin/src/lcd/dogm/game.h new file mode 100644 index 000000000000..6e16e8e0579c --- /dev/null +++ b/Marlin/src/lcd/dogm/game.h @@ -0,0 +1,11 @@ +#pragma once +#include "marlinui_DOGM.h" +#include "../lcdprint.h" + +typedef uint8_t game_dim_t; +typedef const u8g_pgm_uint8_t* pgm_bitmap_t; + +constexpr game_dim_t GAME_WIDTH = LCD_PIXEL_WIDTH; +constexpr game_dim_t GAME_HEIGHT = LCD_PIXEL_HEIGHT; +constexpr game_dim_t GAME_FONT_WIDTH = MENU_FONT_WIDTH; +constexpr game_dim_t GAME_FONT_ASCENT = MENU_FONT_ASCENT; diff --git a/Marlin/src/lcd/e3v2/common/dwin_api.cpp b/Marlin/src/lcd/e3v2/common/dwin_api.cpp index 1688b2423053..a064505807a3 100644 --- a/Marlin/src/lcd/e3v2/common/dwin_api.cpp +++ b/Marlin/src/lcd/e3v2/common/dwin_api.cpp @@ -169,6 +169,69 @@ void dwinFrameClear(const uint16_t color) { dwinWord(i, y); dwinSend(i); } + + // Draw a map of multiple points using minimal amount of point drawing commands + // color: point color + // point_width: point width 0x01-0x0F + // point_height: point height 0x01-0x0F + // x,y: upper left point + // map_columns: columns in theh point map. each column is a byte in the map and contains 8 points + // map_rows: rows in the point map + // map: point bitmap. 2D array of points, 1 bit per point + // Note: somewhat similar to U8G's drawBitmap() function, see https://github.com/olikraus/u8glib/wiki/userreference#drawbitmap + void dwinDrawPointMap( + const uint16_t color, + const uint8_t point_width, + const uint8_t point_height, + const uint16_t x, + const uint16_t y, + const uint16_t map_columns, + const uint16_t map_rows, + const uint8_t *map_data) { + // At how many bytes should we flush the send buffer? + // One byte is used (hidden) for F_HONE, and we need 4 bytes when appending a point. + // So we should flush the send buffer when we have less than 5 bytes left. + constexpr size_t flush_send_buffer_at = (COUNT(dwinSendBuf) - 1 - 4); + + // How long is the header of each draw command? + // 1B CMD, 2B COLOR, 1B WIDTH, 1B HEIGHT + constexpr size_t command_header_size = 5; + + // Draw the point map + size_t i = 0; + for (uint16_t row = 0; row < map_rows; row++) { + for (uint16_t col = 0; col < map_columns; col++) { + const uint8_t map_byte = map_data[(row * map_columns) + col]; + for (uint8_t bit = 0; bit < 8; bit++) { + // Draw the bit of the byte if it's set + if (TEST(map_byte, bit)) { + // Flush the send buffer and prepare next draw if either + // a) The buffer reached the 'should flush' state, or + // b) This is the first point to draw + if (i >= flush_send_buffer_at || i == 0) + { + // Dispatch the current draw command + if (i > command_header_size) dwinSend(i); + + // Prepare the next draw command + i = 0; + dwinByte(i, 0x02); // cmd: draw point(s) + dwinWord(i, color); + dwinByte(i, point_width); + dwinByte(i, point_height); + } + + // Append point coordinates to draw command + dwinWord(i, x + (point_width * ((8 * col) + (7 - bit)))); // x + dwinWord(i, y + (point_height * (row))); // y + } + } + } + } + + // Dispatch final draw command if the buffer contains any points + if (i > command_header_size) dwinSend(i); + } #endif // Draw a line diff --git a/Marlin/src/lcd/e3v2/common/dwin_api.h b/Marlin/src/lcd/e3v2/common/dwin_api.h index 48785150328f..64145b69c4c1 100644 --- a/Marlin/src/lcd/e3v2/common/dwin_api.h +++ b/Marlin/src/lcd/e3v2/common/dwin_api.h @@ -164,6 +164,26 @@ inline void dwinDrawBox(uint8_t mode, uint16_t color, uint16_t xStart, uint16_t void dwinDrawPoint(uint16_t color, uint8_t width, uint8_t height, uint16_t x, uint16_t y); #endif +// Draw a map of multiple points using minimal amount of point drawing commands +// color: point color +// point_width: point width 0x01-0x0F +// point_height: point height 0x01-0x0F +// x,y: upper left point +// map_columns: columns in theh point map. each column is a byte in the map and contains 8 points +// map_rows: rows in the point map +// map: point bitmap. 2D array of points, 1 bit per point +#if DISABLED(TJC_DISPLAY) + void dwinDrawPointMap( + const uint16_t color, + const uint8_t point_width, + const uint8_t point_height, + const uint16_t x, + const uint16_t y, + const uint16_t map_columns, + const uint16_t map_rows, + const uint8_t *map_data); +#endif + // Move a screen area // mode: 0, circle shift; 1, translation // dir: 0=left, 1=right, 2=up, 3=down diff --git a/Marlin/src/lcd/e3v2/marlinui/game.cpp b/Marlin/src/lcd/e3v2/marlinui/game.cpp new file mode 100644 index 000000000000..1c2acf4792a5 --- /dev/null +++ b/Marlin/src/lcd/e3v2/marlinui/game.cpp @@ -0,0 +1,227 @@ +#include "../../../inc/MarlinConfigPre.h" + +#if IS_DWIN_MARLINUI && HAS_GAMES + +// Show performance counters on the screen (frame timing and draw call count) +#define PERFORMANCE_COUNTERS 0 + +// Compound calls are calls that are made up of multiple subcalls (e.g. draw_hline, which is made up of a draw_box call) +#define INCLUDE_COMPOUNT_CALLS 0 + +#include "../../menu/game/types.h" // includes e3v2/marlinui/game.h +#include "../../lcdprint.h" +#include "lcdprint_dwin.h" +#include "marlinui_dwin.h" + +#if ENABLED(PERFORMANCE_COUNTERS) + static uint32_t draw_call_cnt = 0; // Total number of draw calls in the current frame + static millis_t frame_draw_millis = 0, // Time spent drawing the frame + frame_wait_millis = 0; // Time spent waiting for the next frame + + #define COUNT_DRAW_CALL(compound_call_count) TERN(INCLUDE_COMPOUNT_CALLS, draw_call_cnt++, draw_call_cnt = draw_call_cnt + 1 - compound_call_count) +#else + #define COUNT_DRAW_CALL(compound_call_count) +#endif + +void MarlinGame::frame_start() { + // Clear the screen before each frame + //dwinFrameClear(CLEAR_COLOR); + + // Filling the play area is faster than clearing the whole screen + const uint16_t fg = dwin_font.fg; + dwin_font.fg = COLOR_BG_BLACK; + draw_box(0, 0, GAME_WIDTH, GAME_HEIGHT); + dwin_font.fg = fg; + + // Ensure the correct font is selected + dwin_font.index = DWIN_FONT_MENU; + + // Reset the performance counters + #if ENABLED(PERFORMANCE_COUNTERS) + draw_call_cnt = 0; + frame_draw_millis = millis(); + frame_wait_millis = frame_draw_millis - frame_wait_millis; + #endif +} + +void MarlinGame::frame_end() { + #if ENABLED(PERFORMANCE_COUNTERS) + const millis_t frame_wait = frame_wait_millis; + frame_wait_millis = millis(); + frame_draw_millis = frame_wait_millis - frame_draw_millis; + + // Format the performance counters as a string + char perf_str[64]; + sprintf_P( + perf_str, + PSTR("d%04lu w%04lu c%04lu "), + frame_draw_millis, + frame_wait, + draw_call_cnt + ); + + // Draw the performance counters at the (physical) origin of the screen + const uint16_t fg = dwin_font.fg; + const bool solid = dwin_font.solid; + set_color(color::YELLOW); + dwin_font.solid = true; + + lcd_moveto_xy(0, 0); + lcd_put_u8str(perf_str); + + dwin_font.fg = fg; + dwin_font.solid = solid; + #endif +} + +void MarlinGame::set_color(const color color) { + switch(color) + { + case color::BLACK: + dwin_font.fg = COLOR_BG_BLACK; + break; + case color::WHITE: + default: + dwin_font.fg = COLOR_WHITE; + break; + + // https://rgbcolorpicker.com/565/table + case color::RED: + dwin_font.fg = RGB(0x1F, 0x00, 0x00); + break; + case color::GREEN: + dwin_font.fg = RGB(0x00, 0x3F, 0x00); + break; + case color::BLUE: + dwin_font.fg = RGB(0x00, 0x00, 0x1F); + break; + case color::YELLOW: + dwin_font.fg = RGB(0x1F, 0x3F, 0x00); + break; + case color::CYAN: + dwin_font.fg = RGB(0x00, 0x3F, 0x1F); + break; + case color::MAGENTA: + dwin_font.fg = RGB(0x1F, 0x00, 0x1F); + break; + } +} + +void MarlinGame::draw_hline(const game_dim_t x, const game_dim_t y, const game_dim_t w) { + // Draw lines as boxes, since DWIN lines are always 1px wide but we want to scale them + draw_box(x, y, w, 1); + + COUNT_DRAW_CALL(1); +} + +void MarlinGame::draw_vline(const game_dim_t x, const game_dim_t y, const game_dim_t h) { + // Draw lines as boxes, since DWIN lines are always 1px wide but we want to scale them + draw_box(x, y, 1, h); + + COUNT_DRAW_CALL(1); +} + +void MarlinGame::draw_frame(const game_dim_t x, const game_dim_t y, const game_dim_t w, const game_dim_t h) { + dwinDrawBox( + 0, // mode = frame + dwin_font.fg, // color + dwin_game::game_to_screen(x) + dwin_game::x_offset, + dwin_game::game_to_screen(y) + dwin_game::y_offset, + dwin_game::game_to_screen(w), + dwin_game::game_to_screen(h) + ); + + COUNT_DRAW_CALL(0); +} + +void MarlinGame::draw_box(const game_dim_t x, const game_dim_t y, const game_dim_t w, const game_dim_t h) { + dwinDrawBox( + 1, // mode = fill + dwin_font.fg, // color + dwin_game::game_to_screen(x) + dwin_game::x_offset, + dwin_game::game_to_screen(y) + dwin_game::y_offset, + dwin_game::game_to_screen(w), + dwin_game::game_to_screen(h) + ); + + COUNT_DRAW_CALL(0); +} + +void MarlinGame::draw_pixel(const game_dim_t x, const game_dim_t y) { + // Draw pixels using boxes. + // While DWIN protocol supports drawing points with different sizes, the + // 0x02 'draw point' command is slower per pixel than 0x05 'fill rectangle' + // (0.4 us vs 0.14 us per pixel) + draw_box(x, y, 1, 1); + + COUNT_DRAW_CALL(1); +} + +void MarlinGame::draw_bitmap(const game_dim_t x, const game_dim_t y, const game_dim_t bytes_per_row, const game_dim_t rows, const pgm_bitmap_t bitmap) { + // DWIN theorethically supports bitmaps since kernel 2.1, but most screens don't support it + // (either because they use an older kernel version, or because they just (badly) emulate the DWIN protocol). + // So instead, we have to fall back to drawing points manually. + + #if DISABLED(TJC_DISPLAY) + // DWIN T5UI actually supports drawing multiple points in one go using the 0x02 'draw point' command, ever since kernel 1.2. + // So we use that to draw the bitmap as a series of points, which is faster than drawing rectangles using draw_pixel. + dwinDrawPointMap( + dwin_font.fg, + dwin_game::game_to_screen(1), + dwin_game::game_to_screen(1), + dwin_game::game_to_screen(x) + dwin_game::x_offset, + dwin_game::game_to_screen(y) + dwin_game::y_offset, + bytes_per_row, + rows, + bitmap + ); + + COUNT_DRAW_CALL(0); + #else + // TJC displays don't seem to support the 0x02 'draw point' command, so instead we have to draw the bitmap + // as a series of rectangles using draw_pixel. + // This will absolutely suck for performance, but it's the best we can do on these screens. + for (game_dim_t row = 0; row < rows; row++) { + for (game_dim_t col = 0; col < bytes_per_row; col++) { + const uint8_t byte = bitmap[(row * bytes_per_row) + col]; + for (uint8_t bit = 0; bit < 8; bit++) { + // Assuming that the drawing area was cleared before drawing + if (byte & (1 << bit)) { + draw_pixel(x + (col * 8) + (7 - bit + 1), y + row); + COUNT_DRAW_CALL(1); + } + } + } + } + #endif +} + +int MarlinGame::draw_string(const game_dim_t x, const game_dim_t y, const char* str) { + COUNT_DRAW_CALL(0); + + lcd_moveto_xy( + dwin_game::game_to_screen(x) + dwin_game::x_offset, + dwin_game::game_to_screen(y) + dwin_game::y_offset + ); + + return lcd_put_u8str_max_P( + str, + PIXEL_LEN_NOLIMIT + ); +} + +int MarlinGame::draw_string(const game_dim_t x, const game_dim_t y, FSTR_P const str) { + return draw_string(x, y, FTOP(str)); +} + +void MarlinGame::draw_int(const game_dim_t x, const game_dim_t y, const int value) { + COUNT_DRAW_CALL(0); + + lcd_moveto_xy( + dwin_game::game_to_screen(x) + dwin_game::x_offset, + dwin_game::game_to_screen(y) + dwin_game::y_offset + ); + + lcd_put_int(value); +} +#endif // IS_DWIN_MARLINUI && HAS_GAMES diff --git a/Marlin/src/lcd/e3v2/marlinui/game.h b/Marlin/src/lcd/e3v2/marlinui/game.h new file mode 100644 index 000000000000..de988f9a4908 --- /dev/null +++ b/Marlin/src/lcd/e3v2/marlinui/game.h @@ -0,0 +1,65 @@ +#pragma once +#include +#include "../marlinui/marlinui_dwin.h" + +typedef uint8_t game_dim_t; +typedef uint16_t screen_dim_t; +typedef const uint8_t* pgm_bitmap_t; + +namespace dwin_game { + /** + * @brief Target the renderer at 128x64 pixels to match UG8 screens + */ + constexpr screen_dim_t TARGET_WIDTH = 128; + constexpr screen_dim_t TARGET_HEIGHT = 64; + + constexpr int calculate_scale() + { + // Use whichever is smaller: the width or height scaling factor + float scaling_factor = _MIN( + static_cast(DWIN_WIDTH) / static_cast(TARGET_WIDTH), + static_cast(DWIN_HEIGHT) / static_cast(TARGET_HEIGHT) + ); + + // Round DOWN to closest integer + return static_cast(scaling_factor); + } + + /** + * @brief Game render scale. + */ + constexpr int scale = calculate_scale(); + + /** + * @brief Scale a game dimension to screen dimensions + */ + constexpr game_dim_t screen_to_game(const screen_dim_t x) { + return x / scale; + } + + /** + * @brief Scale a screen dimension to game dimensions + */ + constexpr screen_dim_t game_to_screen(const game_dim_t x) { + return x * scale; + } + + /** + * @brief Offset of the game window on the screen. Applied after scaling. + */ + constexpr screen_dim_t x_offset = (DWIN_WIDTH - game_to_screen(TARGET_WIDTH)) / 2; + constexpr screen_dim_t y_offset = (DWIN_HEIGHT - game_to_screen(TARGET_HEIGHT)) / 2; + + static_assert(game_to_screen(TARGET_WIDTH) + (x_offset * 2) <= DWIN_WIDTH, "DWIN game renderer failed to auto-scale, is too wide"); + static_assert(game_to_screen(TARGET_HEIGHT) + (y_offset * 2) <= DWIN_HEIGHT, "DWIN game renderer failed to auto-scale, is too high"); +} // namespace dwin_game + +constexpr game_dim_t GAME_WIDTH = dwin_game::screen_to_game(DWIN_WIDTH - (dwin_game::x_offset * 2)); +constexpr game_dim_t GAME_HEIGHT = dwin_game::screen_to_game(DWIN_HEIGHT - (dwin_game::y_offset * 2)); +constexpr game_dim_t GAME_FONT_WIDTH = dwin_game::screen_to_game(MENU_FONT_WIDTH); +constexpr game_dim_t GAME_FONT_ASCENT = dwin_game::screen_to_game(MENU_FONT_ASCENT); + +// DWIN screens don't page, so these macros are always true +#define PAGE_OVER(ya) true +#define PAGE_UNDER(yb) true +#define PAGE_CONTAINS(ya, yb) true diff --git a/Marlin/src/lcd/e3v2/marlinui/lcdprint_dwin.cpp b/Marlin/src/lcd/e3v2/marlinui/lcdprint_dwin.cpp index f689a6ff698d..9fa75a700bc6 100644 --- a/Marlin/src/lcd/e3v2/marlinui/lcdprint_dwin.cpp +++ b/Marlin/src/lcd/e3v2/marlinui/lcdprint_dwin.cpp @@ -52,7 +52,9 @@ void lcd_moveto(const lcd_uint_t col, const lcd_uint_t row) { inline void lcd_advance_cursor(const uint8_t len=1) { cursor.x += len * dwin_font.width; } void lcd_put_int(const int i) { - // TODO: Draw an int at the cursor position, advance the cursor + char buf[12]; // 10 digits + sign + null + itoa(i, buf, 10); + lcd_put_u8str_max(buf, PIXEL_LEN_NOLIMIT); } int lcd_put_dwin_string() { diff --git a/Marlin/src/lcd/menu/game/brickout.cpp b/Marlin/src/lcd/menu/game/brickout.cpp index 078cbbcceee2..f4f9837ba445 100644 --- a/Marlin/src/lcd/menu/game/brickout.cpp +++ b/Marlin/src/lcd/menu/game/brickout.cpp @@ -27,14 +27,14 @@ #include "game.h" #define BRICK_H 5 -#define BRICK_TOP MENU_FONT_ASCENT +#define BRICK_TOP GAME_FONT_ASCENT #define PADDLE_H 2 #define PADDLE_VEL 3 -#define PADDLE_W ((LCD_PIXEL_WIDTH) / 8) -#define PADDLE_Y (LCD_PIXEL_HEIGHT - 1 - PADDLE_H) +#define PADDLE_W ((GAME_WIDTH) / 8) +#define PADDLE_Y (GAME_HEIGHT - 1 - PADDLE_H) -#define BRICK_W ((LCD_PIXEL_WIDTH) / (BRICK_COLS)) +#define BRICK_W ((GAME_WIDTH) / (BRICK_COLS)) #define BRICK_BOT (BRICK_TOP + BRICK_H * BRICK_ROWS - 1) #define BRICK_COL(X) ((X) / (BRICK_W)) @@ -53,7 +53,7 @@ void reset_ball() { bdat.ballv = FTOF(1.3f); bdat.ballh = -FTOF(1.25f); uint8_t bx = bdat.paddle_x + (PADDLE_W) / 2 + ball_dist; - if (bx >= LCD_PIXEL_WIDTH - 10) { bx -= ball_dist * 2; bdat.ballh = -bdat.ballh; } + if (bx >= GAME_WIDTH - 10) { bx -= ball_dist * 2; bdat.ballh = -bdat.ballh; } bdat.ballx = BTOF(bx); bdat.hit_dir = -1; } @@ -61,7 +61,7 @@ void reset_ball() { void BrickoutGame::game_screen() { if (game_frame()) { // Run logic twice for finer resolution // Update Paddle Position - bdat.paddle_x = constrain(int8_t(ui.encoderPosition), 0, (LCD_PIXEL_WIDTH - (PADDLE_W)) / (PADDLE_VEL)); + bdat.paddle_x = constrain(int8_t(ui.encoderPosition), 0, (GAME_WIDTH - (PADDLE_W)) / (PADDLE_VEL)); ui.encoderPosition = bdat.paddle_x; bdat.paddle_x *= (PADDLE_VEL); @@ -70,7 +70,7 @@ void BrickoutGame::game_screen() { // Provisionally update the ball position const fixed_t newx = bdat.ballx + bdat.ballh, newy = bdat.bally + bdat.ballv; // current next position - if (!WITHIN(newx, 0, BTOF(LCD_PIXEL_WIDTH - 1))) { // out in x? + if (!WITHIN(newx, 0, BTOF(GAME_WIDTH - 1))) { // out in x? bdat.ballh = -bdat.ballh; _BUZZ(5, 220); // bounce x } if (newy < 0) { // out in y? @@ -78,7 +78,7 @@ void BrickoutGame::game_screen() { bdat.hit_dir = 1; } // Did the ball go below the bottom? - else if (newy > BTOF(LCD_PIXEL_HEIGHT)) { + else if (newy > BTOF(GAME_HEIGHT)) { _BUZZ(500, 75); if (--bdat.balls_left) reset_ball(); else game_state = 0; break; // done @@ -134,32 +134,40 @@ void BrickoutGame::game_screen() { } while (false); } - u8g.setColorIndex(1); + frame_start(); - // Draw bricks + // Draw bricks, cycling through colors for each brick + const color brick_colors[] = { color::RED, color::CYAN, color::GREEN, color::YELLOW, color::MAGENTA, color::BLUE }; + int color_index = 0; if (PAGE_CONTAINS(BRICK_TOP, BRICK_BOT)) { for (uint8_t y = 0; y < BRICK_ROWS; ++y) { const uint8_t yy = y * BRICK_H + BRICK_TOP; if (PAGE_CONTAINS(yy, yy + BRICK_H - 1)) { for (uint8_t x = 0; x < BRICK_COLS; ++x) { + // Cycle through colors, even if the brick is gone. + // Otherwise, bricks would change color if their neighbor is hit + set_color(brick_colors[color_index++ % COUNT(brick_colors)]); + + // Draw brick if it's still there if (TEST(bdat.bricks[y], x)) { const uint8_t xx = x * BRICK_W; - for (uint8_t v = 0; v < BRICK_H - 1; ++v) - if (PAGE_CONTAINS(yy + v, yy + v)) - u8g.drawHLine(xx, yy + v, BRICK_W - 1); + draw_box(xx, yy, BRICK_W - 1, BRICK_H - 1); } } } } } + // Everything else is white + set_color(color::WHITE); + // Draw paddle if (PAGE_CONTAINS(PADDLE_Y-1, PADDLE_Y)) { - u8g.drawHLine(bdat.paddle_x, PADDLE_Y, PADDLE_W); + draw_hline(bdat.paddle_x, PADDLE_Y, PADDLE_W); #if PADDLE_H > 1 - u8g.drawHLine(bdat.paddle_x, PADDLE_Y-1, PADDLE_W); + draw_hline(bdat.paddle_x, PADDLE_Y-1, PADDLE_W); #if PADDLE_H > 2 - u8g.drawHLine(bdat.paddle_x, PADDLE_Y-2, PADDLE_W); + draw_hline(bdat.paddle_x, PADDLE_Y-2, PADDLE_W); #endif #endif } @@ -168,29 +176,30 @@ void BrickoutGame::game_screen() { if (game_state) { const uint8_t by = FTOB(bdat.bally); if (PAGE_CONTAINS(by, by+1)) - u8g.drawFrame(FTOB(bdat.ballx), by, 2, 2); + draw_frame(FTOB(bdat.ballx), by, 2, 2); } // Or draw GAME OVER else draw_game_over(); - if (PAGE_UNDER(MENU_FONT_ASCENT)) { + if (PAGE_UNDER(GAME_FONT_ASCENT)) { // Score Digits - //const uint8_t sx = (LCD_PIXEL_WIDTH - (score >= 10 ? score >= 100 ? score >= 1000 ? 4 : 3 : 2 : 1) * MENU_FONT_WIDTH) / 2; + //const uint8_t sx = (GAME_WIDTH - (score >= 10 ? score >= 100 ? score >= 1000 ? 4 : 3 : 2 : 1) * GAME_FONT_WIDTH) / 2; constexpr uint8_t sx = 0; - lcd_put_int(sx, MENU_FONT_ASCENT - 1, score); + draw_int(sx, GAME_FONT_ASCENT - 1, score); // Balls Left - lcd_moveto(LCD_PIXEL_WIDTH - MENU_FONT_WIDTH * 3, MENU_FONT_ASCENT - 1); PGM_P const ohs = PSTR("ooo\0\0"); - lcd_put_u8str_P(ohs + 3 - bdat.balls_left); + draw_string(GAME_WIDTH - GAME_FONT_WIDTH * 3, GAME_FONT_ASCENT - 1, ohs + 3 - bdat.balls_left); } + frame_end(); + // A click always exits this game if (ui.use_click()) exit_game(); } -#define SCREEN_M ((LCD_PIXEL_WIDTH) / 2) +#define SCREEN_M ((GAME_WIDTH) / 2) void BrickoutGame::enter_game() { init_game(2, game_screen); // 2 = reset bricks on paddle hit diff --git a/Marlin/src/lcd/menu/game/game.cpp b/Marlin/src/lcd/menu/game/game.cpp index d465b00388cf..933e2e8e66e3 100644 --- a/Marlin/src/lcd/menu/game/game.cpp +++ b/Marlin/src/lcd/menu/game/game.cpp @@ -40,15 +40,15 @@ bool MarlinGame::game_frame() { } void MarlinGame::draw_game_over() { - constexpr int8_t gowide = (MENU_FONT_WIDTH) * 9, - gohigh = MENU_FONT_ASCENT - 3, - lx = (LCD_PIXEL_WIDTH - gowide) / 2, - ly = (LCD_PIXEL_HEIGHT + gohigh) / 2; + constexpr int8_t gowide = (GAME_FONT_WIDTH) * 9, + gohigh = GAME_FONT_ASCENT - 3, + lx = (GAME_WIDTH - gowide) / 2, + ly = (GAME_HEIGHT + gohigh) / 2; if (PAGE_CONTAINS(ly - gohigh - 1, ly + 1)) { - u8g.setColorIndex(0); - u8g.drawBox(lx - 1, ly - gohigh - 1, gowide + 2, gohigh + 2); - u8g.setColorIndex(1); - if (ui.get_blink()) lcd_put_u8str(lx, ly, F("GAME OVER")); + set_color(color::BLACK); + draw_box(lx - 1, ly - gohigh - 1, gowide + 2, gohigh + 2); + set_color(color::WHITE); + if (ui.get_blink()) draw_string(lx, ly, F("GAME OVER")); } } diff --git a/Marlin/src/lcd/menu/game/game.h b/Marlin/src/lcd/menu/game/game.h index ba123cb98bfe..2e9d55b1a5fc 100644 --- a/Marlin/src/lcd/menu/game/game.h +++ b/Marlin/src/lcd/menu/game/game.h @@ -22,9 +22,8 @@ #pragma once #include "../../../inc/MarlinConfigPre.h" -#include "../../dogm/marlinui_DOGM.h" -#include "../../lcdprint.h" #include "../../marlinui.h" +#include "types.h" //#define MUTE_GAMES diff --git a/Marlin/src/lcd/menu/game/invaders.cpp b/Marlin/src/lcd/menu/game/invaders.cpp index 588523854f94..a77137fa614e 100644 --- a/Marlin/src/lcd/menu/game/invaders.cpp +++ b/Marlin/src/lcd/menu/game/invaders.cpp @@ -29,11 +29,11 @@ #define CANNON_W 11 #define CANNON_H 8 #define CANNON_VEL 4 -#define CANNON_Y (LCD_PIXEL_HEIGHT - 1 - CANNON_H) +#define CANNON_Y (GAME_HEIGHT - 1 - CANNON_H) #define INVADER_VEL 3 -#define INVADER_TOP MENU_FONT_ASCENT +#define INVADER_TOP GAME_FONT_ASCENT #define INVADERS_WIDE ((INVADER_COL_W) * (INVADER_COLS)) #define INVADERS_HIGH ((INVADER_ROW_H) * (INVADER_ROWS)) @@ -48,6 +48,16 @@ #define INVADER_RIGHT ((INVADER_COLS) * (INVADER_COL_W)) + +#define INVADER_COLOR { MarlinGame::color::GREEN, MarlinGame::color::CYAN, MarlinGame::color::YELLOW } +#define CANNON_COLOR MarlinGame::color::WHITE +#define LASER_COLOR MarlinGame::color::WHITE // Shot by player +#define BULLET_COLOR LASER_COLOR // Shot by invader +#define LIFE_COLOR CANNON_COLOR +#define UFO_COLOR MarlinGame::color::MAGENTA +#define EXPLOSION_COLOR MarlinGame::color::RED + + // 11x8 const unsigned char invader[3][2][16] PROGMEM = { { { B00000110,B00000000, @@ -175,7 +185,7 @@ inline void update_invader_data() { } idat.leftmost = 0; for (uint8_t i = 0; i < INVADER_COLS; ++i) { if (TEST(inv_mask, i)) break; idat.leftmost -= INVADER_COL_W; } - idat.rightmost = LCD_PIXEL_WIDTH - (INVADERS_WIDE); + idat.rightmost = GAME_WIDTH - (INVADERS_WIDE); for (uint8_t i = INVADER_COLS; i--;) { if (TEST(inv_mask, i)) break; idat.rightmost += INVADER_COL_W; } if (idat.count == 2) idat.dir = idat.dir > 0 ? INVADER_VEL + 1 : -(INVADER_VEL + 1); } @@ -195,7 +205,7 @@ inline void reset_invaders() { inline void spawn_ufo() { idat.ufov = random(0, 2) ? 1 : -1; - idat.ufox = idat.ufov > 0 ? -(UFO_W) : LCD_PIXEL_WIDTH - 1; + idat.ufox = idat.ufov > 0 ? -(UFO_W) : GAME_WIDTH - 1; } inline void reset_player() { @@ -205,7 +215,7 @@ inline void reset_player() { inline void fire_cannon() { idat.laser.x = idat.cannon_x + CANNON_W / 2; - idat.laser.y = LCD_PIXEL_HEIGHT - CANNON_H - (LASER_H); + idat.laser.y = GAME_HEIGHT - CANNON_H - (LASER_H); idat.laser.v = -(LASER_H); } @@ -235,7 +245,7 @@ void InvadersGame::game_screen() { if (ui.first_page) { // Update Cannon Position - int16_t ep = constrain(int16_t(ui.encoderPosition), 0, (LCD_PIXEL_WIDTH - (CANNON_W)) / (CANNON_VEL)); + int16_t ep = constrain(int16_t(ui.encoderPosition), 0, (GAME_WIDTH - (CANNON_W)) / (CANNON_VEL)); ui.encoderPosition = ep; ep *= (CANNON_VEL); @@ -246,7 +256,7 @@ void InvadersGame::game_screen() { if (game_state) do { // Move the UFO, if any - if (idat.ufov) { idat.ufox += idat.ufov; if (!WITHIN(idat.ufox, -(UFO_W), LCD_PIXEL_WIDTH - 1)) idat.ufov = 0; } + if (idat.ufov) { idat.ufox += idat.ufov; if (!WITHIN(idat.ufox, -(UFO_W), GAME_WIDTH - 1)) idat.ufov = 0; } if (game_state > 1) { if (--game_state == 2) { reset_invaders(); } else if (game_state == 100) { game_state = 1; } break; } @@ -326,7 +336,7 @@ void InvadersGame::game_screen() { if (b->v) { // Update alien bullet position b->y += b->v; - if (b->y >= LCD_PIXEL_HEIGHT) + if (b->y >= GAME_HEIGHT) b->v = 0; // Offscreen else if (b->y >= CANNON_Y && WITHIN(b->x, idat.cannon_x, idat.cannon_x + CANNON_W - 1)) kill_cannon(game_state, 120); // Hit the cannon @@ -365,7 +375,7 @@ void InvadersGame::game_screen() { if (!idat.quit_count) exit_game(); - u8g.setColorIndex(1); + frame_start(); // Draw invaders if (PAGE_CONTAINS(idat.pos.y, idat.pos.y + idat.botmost * (INVADER_ROW_H) - 2 - 1)) { @@ -375,8 +385,11 @@ void InvadersGame::game_screen() { if (PAGE_CONTAINS(yy, yy + INVADER_H - 1)) { int8_t xx = idat.pos.x; for (uint8_t x = 0; x < INVADER_COLS; ++x) { - if (TEST(idat.bugs[y], x)) - u8g.drawBitmapP(xx, yy, 2, INVADER_H, invader[type][idat.game_blink]); + if (TEST(idat.bugs[y], x)) { + constexpr color invader_color[] = INVADER_COLOR; + set_color(invader_color[type]); + draw_bitmap(xx, yy, 2, INVADER_H, invader[type][idat.game_blink]); + } xx += INVADER_COL_W; } } @@ -385,44 +398,58 @@ void InvadersGame::game_screen() { } // Draw UFO - if (idat.ufov && PAGE_UNDER(UFO_H + 2)) - u8g.drawBitmapP(idat.ufox, 2, 2, UFO_H, ufo); + if (idat.ufov && PAGE_UNDER(UFO_H + 2)) { + set_color(UFO_COLOR); + draw_bitmap(idat.ufox, 2, 2, UFO_H, ufo); + } // Draw cannon - if (game_state && PAGE_CONTAINS(CANNON_Y, CANNON_Y + CANNON_H - 1) && (game_state < 2 || (game_state & 0x02))) - u8g.drawBitmapP(idat.cannon_x, CANNON_Y, 2, CANNON_H, cannon); + if (game_state && PAGE_CONTAINS(CANNON_Y, CANNON_Y + CANNON_H - 1) && (game_state < 2 || (game_state & 0x02))) { + set_color(CANNON_COLOR); + draw_bitmap(idat.cannon_x, CANNON_Y, 2, CANNON_H, cannon); + } // Draw laser - if (idat.laser.v && PAGE_CONTAINS(idat.laser.y, idat.laser.y + LASER_H - 1)) - u8g.drawVLine(idat.laser.x, idat.laser.y, LASER_H); + if (idat.laser.v && PAGE_CONTAINS(idat.laser.y, idat.laser.y + LASER_H - 1)) { + set_color(LASER_COLOR); + draw_vline(idat.laser.x, idat.laser.y, LASER_H); + } // Draw invader bullets for (uint8_t i = 0; i < COUNT(idat.bullet); ++i) { - if (idat.bullet[i].v && PAGE_CONTAINS(idat.bullet[i].y - (SHOT_H - 1), idat.bullet[i].y)) - u8g.drawVLine(idat.bullet[i].x, idat.bullet[i].y - (SHOT_H - 1), SHOT_H); + if (idat.bullet[i].v && PAGE_CONTAINS(idat.bullet[i].y - (SHOT_H - 1), idat.bullet[i].y)) { + set_color(BULLET_COLOR); + draw_vline(idat.bullet[i].x, idat.bullet[i].y - (SHOT_H - 1), SHOT_H); + } } // Draw explosion if (idat.explod.v && PAGE_CONTAINS(idat.explod.y, idat.explod.y + 7 - 1)) { - u8g.drawBitmapP(idat.explod.x, idat.explod.y, 2, 7, explosion); + set_color(EXPLOSION_COLOR); + draw_bitmap(idat.explod.x, idat.explod.y, 2, 7, explosion); --idat.explod.v; } + set_color(color::WHITE); + // Blink GAME OVER when game is over if (!game_state) draw_game_over(); - if (PAGE_UNDER(MENU_FONT_ASCENT - 1)) { - // Draw Score - //const uint8_t sx = (LCD_PIXEL_WIDTH - (score >= 10 ? score >= 100 ? score >= 1000 ? 4 : 3 : 2 : 1) * MENU_FONT_WIDTH) / 2; - constexpr uint8_t sx = 0; - lcd_put_int(sx, MENU_FONT_ASCENT - 1, score); - + if (PAGE_UNDER(GAME_FONT_ASCENT - 1)) { // Draw lives if (idat.cannons_left) - for (uint8_t i = 1; i <= idat.cannons_left; ++i) - u8g.drawBitmapP(LCD_PIXEL_WIDTH - i * (LIFE_W), 6 - (LIFE_H), 1, LIFE_H, life); + for (uint8_t i = 1; i <= idat.cannons_left; ++i) { + set_color(LIFE_COLOR); + draw_bitmap(GAME_WIDTH - i * (LIFE_W), 6 - (LIFE_H), 1, LIFE_H, life); + } + + // Draw Score + //const uint8_t sx = (GAME_WIDTH - (score >= 10 ? score >= 100 ? score >= 1000 ? 4 : 3 : 2 : 1) * GAME_FONT_WIDTH) / 2; + constexpr uint8_t sx = 0; + draw_int(sx, GAME_FONT_ASCENT - 1, score); } + frame_end(); } void InvadersGame::enter_game() { diff --git a/Marlin/src/lcd/menu/game/snake.cpp b/Marlin/src/lcd/menu/game/snake.cpp index 2a78c089cfbe..1dfbda829de0 100644 --- a/Marlin/src/lcd/menu/game/snake.cpp +++ b/Marlin/src/lcd/menu/game/snake.cpp @@ -28,13 +28,13 @@ #define SNAKE_BOX 4 -#define HEADER_H (MENU_FONT_ASCENT - 2) +#define HEADER_H (GAME_FONT_ASCENT - 2) #define SNAKE_WH (SNAKE_BOX + 1) #define IDEAL_L 2 -#define IDEAL_R (LCD_PIXEL_WIDTH - 1 - 2) +#define IDEAL_R (GAME_WIDTH - 1 - 2) #define IDEAL_T (HEADER_H + 2) -#define IDEAL_B (LCD_PIXEL_HEIGHT - 1 - 2) +#define IDEAL_B (GAME_HEIGHT - 1 - 2) #define IDEAL_W (IDEAL_R - (IDEAL_L) + 1) #define IDEAL_H (IDEAL_B - (IDEAL_T) + 1) @@ -43,9 +43,9 @@ #define BOARD_W ((SNAKE_WH) * (GAME_W) + 1) #define BOARD_H ((SNAKE_WH) * (GAME_H) + 1) -#define BOARD_L ((LCD_PIXEL_WIDTH - (BOARD_W) + 1) / 2) +#define BOARD_L ((GAME_WIDTH - (BOARD_W) + 1) / 2) #define BOARD_R (BOARD_L + BOARD_W - 1) -#define BOARD_T (((LCD_PIXEL_HEIGHT + IDEAL_T) - (BOARD_H)) / 2) +#define BOARD_T (((GAME_HEIGHT + IDEAL_T) - (BOARD_H)) / 2) #define BOARD_B (BOARD_T + BOARD_H - 1) #define GAMEX(X) (BOARD_L + ((X) * (SNAKE_WH))) @@ -228,15 +228,10 @@ void SnakeGame::game_screen() { } while(0); - u8g.setColorIndex(1); + frame_start(); - // Draw Score - if (PAGE_UNDER(HEADER_H)) lcd_put_int(0, HEADER_H - 1, score); - - // DRAW THE PLAYFIELD BORDER - u8g.drawFrame(BOARD_L - 2, BOARD_T - 2, BOARD_R - BOARD_L + 4, BOARD_B - BOARD_T + 4); - - // Draw the snake (tail) + // Draw the snake (tail) in green + set_color(color::GREEN); #if SNAKE_WH < 2 // At this scale just draw a line @@ -245,11 +240,11 @@ void SnakeGame::game_screen() { if (p.x == q.x) { const int8_t y1 = GAMEY(_MIN(p.y, q.y)), y2 = GAMEY(_MAX(p.y, q.y)); if (PAGE_CONTAINS(y1, y2)) - u8g.drawVLine(GAMEX(p.x), y1, y2 - y1 + 1); + draw_vline(GAMEX(p.x), y1, y2 - y1 + 1); } else if (PAGE_CONTAINS(GAMEY(p.y), GAMEY(p.y))) { const int8_t x1 = GAMEX(_MIN(p.x, q.x)), x2 = GAMEX(_MAX(p.x, q.x)); - u8g.drawHLine(x1, GAMEY(p.y), x2 - x1 + 1); + draw_hline(x1, GAMEY(p.y), x2 - x1 + 1); } } @@ -261,13 +256,13 @@ void SnakeGame::game_screen() { if (p.x == q.x) { const int8_t y1 = GAMEY(_MIN(p.y, q.y)), y2 = GAMEY(_MAX(p.y, q.y)); if (PAGE_CONTAINS(y1, y2 + 1)) - u8g.drawFrame(GAMEX(p.x), y1, 2, y2 - y1 + 1 + 1); + draw_frame(GAMEX(p.x), y1, 2, y2 - y1 + 1 + 1); } else { const int8_t py = GAMEY(p.y); if (PAGE_CONTAINS(py, py + 1)) { const int8_t x1 = GAMEX(_MIN(p.x, q.x)), x2 = GAMEX(_MAX(p.x, q.x)); - u8g.drawFrame(x1, py, x2 - x1 + 1 + 1, 2); + draw_frame(x1, py, x2 - x1 + 1 + 1, 2); } } } @@ -283,7 +278,7 @@ void SnakeGame::game_screen() { for (int8_t i = y1; i <= y2; ++i) { const int8_t y = GAMEY(i); if (PAGE_CONTAINS(y, y + SNAKE_SIZ - 1)) - u8g.drawBox(GAMEX(p.x), y, SNAKE_SIZ, SNAKE_SIZ); + draw_box(GAMEX(p.x), y, SNAKE_SIZ, SNAKE_SIZ); } } } @@ -292,26 +287,36 @@ void SnakeGame::game_screen() { if (PAGE_CONTAINS(py, py + SNAKE_SIZ - 1)) { const int8_t x1 = _MIN(p.x, q.x), x2 = _MAX(p.x, q.x); for (int8_t i = x1; i <= x2; ++i) - u8g.drawBox(GAMEX(i), py, SNAKE_SIZ, SNAKE_SIZ); + draw_box(GAMEX(i), py, SNAKE_SIZ, SNAKE_SIZ); } } } #endif - // Draw food + // Draw food in red + set_color(color::RED); const int8_t fy = GAMEY(sdat.foody); if (PAGE_CONTAINS(fy, fy + FOOD_WH - 1)) { const int8_t fx = GAMEX(sdat.foodx); - u8g.drawFrame(fx, fy, FOOD_WH, FOOD_WH); - if (FOOD_WH == 5) u8g.drawPixel(fx + 2, fy + 2); + draw_frame(fx, fy, FOOD_WH, FOOD_WH); + if (FOOD_WH == 5) draw_pixel(fx + 2, fy + 2); } + // Draw the playfield border + set_color(color::WHITE); + draw_frame(BOARD_L - 2, BOARD_T - 2, BOARD_R - BOARD_L + 4, BOARD_B - BOARD_T + 4); + + // Draw Score + if (PAGE_UNDER(HEADER_H)) draw_int(0, HEADER_H - 1, score); + // Draw GAME OVER if (!game_state) draw_game_over(); // A click always exits this game if (ui.use_click()) exit_game(); + + frame_end(); } void SnakeGame::enter_game() { diff --git a/Marlin/src/lcd/menu/game/types.h b/Marlin/src/lcd/menu/game/types.h index 6e0a2051d74e..9c6c4776e4d4 100644 --- a/Marlin/src/lcd/menu/game/types.h +++ b/Marlin/src/lcd/menu/game/types.h @@ -22,6 +22,14 @@ #pragma once #include +#include "../../../inc/MarlinConfigPre.h" +#include "../../marlinui.h" + +#if HAS_MARLINUI_U8GLIB + #include "../../dogm/game.h" +#elif IS_DWIN_MARLINUI + #include "../../e3v2/marlinui/game.h" +#endif typedef struct { int8_t x, y; } pos_t; @@ -41,6 +49,129 @@ class MarlinGame { static bool game_frame(); static void draw_game_over(); static void exit_game(); + public: static void init_game(const uint8_t init_state, const screenFunc_t screen); + + // + // Render API, based on U8GLib. + // draw_* functions are implemented by the screen-specific renderer + // +public: + /** + * @brief The colors available for drawing games. + * @note If a screen doesn't support (a) color, it is expected to map to the closest + * available color OR white if the closest available color is (near) black. + */ + enum class color { + BLACK, + WHITE, + RED, + GREEN, + BLUE, + YELLOW, + CYAN, + MAGENTA, + }; + +protected: + /** + * @brief Called before any draw calls in the current frame. + */ + static void frame_start(); + + /** + * @brief Called after all draw calls in the current frame. + */ + static void frame_end(); + + /** + * @brief Set the color for subsequent draw calls. + * @param color The color to use for subsequent draw calls. + */ + static void set_color(const color color); + + /** + * @brief Draw a horizontal line. + * @param x The x-coordinate of the start of the line. + * @param y The y-coordinate of the line. + * @param l The length of the line. + * @see https://github.com/olikraus/u8glib/wiki/userreference#drawhline + */ + static void draw_hline(const game_dim_t x, const game_dim_t y, const game_dim_t l); + + /** + * @brief Draw a vertical line. + * @param x The x-coordinate of the line. + * @param y The y-coordinate of the start of the line. + * @param l The length of the line. + * @see https://github.com/olikraus/u8glib/wiki/userreference#drawvline + */ + static void draw_vline(const game_dim_t x, const game_dim_t y, const game_dim_t l); + + /** + * @brief Draw a outlined rectangle (frame). + * @param x The x-coordinate of the top-left corner of the frame. + * @param y The y-coordinate of the top-left corner of the frame. + * @param w The width of the frame. + * @param h The height of the frame. + * @see https://github.com/olikraus/u8glib/wiki/userreference#drawframe + */ + static void draw_frame(const game_dim_t x, const game_dim_t y, const game_dim_t w, const game_dim_t h); + + /** + * @brief Draw a filled rectangle (box). + * @param x The x-coordinate of the top-left corner of the box. + * @param y The y-coordinate of the top-left corner of the box. + * @param w The width of the box. + * @param h The height of the box. + * @see https://github.com/olikraus/u8glib/wiki/userreference#drawbox + */ + static void draw_box(const game_dim_t x, const game_dim_t y, const game_dim_t w, const game_dim_t h); + + /** + * @brief Draw a pixel. + * @param x The x-coordinate of the pixel. + * @param y The y-coordinate of the pixel. + * @see https://github.com/olikraus/u8glib/wiki/userreference#drawpixel + */ + static void draw_pixel(const game_dim_t x, const game_dim_t y); + + /** + * @brief Draw a bitmap. + * @param x The x-coordinate of the top-left corner of the bitmap. + * @param y The y-coordinate of the top-left corner of the bitmap. + * @param bytes_per_row The number of bytes per row in the bitmap (Width = bytes_per_row * 8). + * @param rows The number of rows in the bitmap (= Height). + * @param bitmap The bitmap to draw. + * @see https://github.com/olikraus/u8glib/wiki/userreference#drawbitmap + */ + static void draw_bitmap(const game_dim_t x, const game_dim_t y, const game_dim_t bytes_per_row, const game_dim_t rows, const pgm_bitmap_t bitmap); + + /** + * @brief Draw a string. + * @param x The x-coordinate of the string. + * @param y The y-coordinate of the string. + * @param str The string to draw. + * @see lcd_moveto + lcd_put_u8str + * @note The font size is available using the GAME_FONT_WIDTH and GAME_FONT_ASCENT constants. + * + * @note On the DWIN renderer, strings may flush the screen, which may cause flickering. + * Consider drawing strings after all other elements have been drawn. + */ + static int draw_string(const game_dim_t x, const game_dim_t y, const char *str); + static int draw_string(const game_dim_t x, const game_dim_t y, FSTR_P const str); + + /** + * @brief Draw an integer. + * @param x The x-coordinate of the integer. + * @param y The y-coordinate of the integer. + * @param value The integer to draw. + * @see lcd_put_int + * @note The font size is available using the GAME_FONT_WIDTH and GAME_FONT_ASCENT constants. + * + * @note On the DWIN renderer, strings may flush the screen, which may cause flickering. + * Consider drawing strings after all other elements have been drawn. + */ + static void draw_int(const game_dim_t x, const game_dim_t y, const int value); };