#include "logic/page_render.h" #include #include #include #include "logic/bitmap_format.h" #include "logic/builtin_fonts.h" static Font usableFont(PageResources &resources, const char *name) { Font font = resources.font(name); if (validFont(font.data, font.size) && font.scale >= 1) return font; return builtinFont(*findBuiltinFont(FONT_NORMAL_NAME)); } static void clearBox(uint8_t *bitmap, int x, int y, int width, int height) { for (int yy = y; yy < y + height; yy++) for (int xx = x; xx < x + width; xx++) setBitmapPixel(bitmap, xx, yy, false); } // The earlier of two "next change" times (0 = none) static uint32_t sooner(uint32_t a, uint32_t b) { if (a == 0) return b; if (b == 0) return a; return a < b ? a : b; } // Image: a bitmap drawn with its top left corner at x, y. With an entity the name is the // element's text followed by the state, or followed by "default" when there is no bitmap // for the state. static void drawImage(const PageElement &e, PageResources &resources, uint8_t *bitmap, ElementBox &box) { uint8_t image[BITMAP_BYTES]; bool found; if (e.entity[0]) { char name[64]; const char *state = resources.haValue(e); snprintf(name, sizeof(name), "%s%s", e.text, state ? state : ""); found = state && resources.background(name, image); if (!found) { snprintf(name, sizeof(name), "%sdefault", e.text); found = resources.background(name, image); } } else found = resources.background(e.text, image); box = ElementBox(); box.x = e.x; box.y = e.y; if (!found) return; // The box of its dots int left = BITMAP_WIDTH, right = -1, top = BITMAP_HEIGHT, bottom = -1; for (int y = 0; y < BITMAP_HEIGHT; y++) for (int x = 0; x < BITMAP_WIDTH; x++) if (bitmapPixel(image, x, y)) { left = x < left ? x : left; right = x > right ? x : right; top = y < top ? y : top; bottom = y > bottom ? y : bottom; } if (right < 0) return; box.x = e.x + left; box.y = e.y + top; box.width = right - left + 1; box.height = bottom - top + 1; if (e.overlay == Overlay::Box) clearBox(bitmap, box.x - 1, box.y - 1, box.width + 2, box.height + 2); for (int y = top; y <= bottom; y++) for (int x = left; x <= right; x++) if (bitmapPixel(image, x, y)) { int bx = e.x + x, by = e.y + y; bool on = e.overlay == Overlay::Invert ? !bitmapPixel(bitmap, bx, by) : true; setBitmapPixel(bitmap, bx, by, on); } } // Bar with BAR_PERCENT: the percentage in the middle of the area inside the frame, its // dots flip what is below them (so it shows on the filled and the empty part). Centred on // the dots the text really has (fonts have empty rows); left out when it doesn't fit. static void drawBarPercent(const PageElement &e, PageResources &resources, uint8_t *bitmap, int x, int y, int w, int h) { char text[8]; barPercent(e, resources.haValue(e), text, sizeof(text)); Font font = usableFont(resources, e.font); uint8_t dots[BITMAP_BYTES]; memset(dots, 0, BITMAP_BYTES); drawText(dots, font, 0, 0, text, DrawMode::Set); int left = BITMAP_WIDTH, right = -1, top = BITMAP_HEIGHT, bottom = -1; for (int yy = 0; yy < BITMAP_HEIGHT; yy++) for (int xx = 0; xx < BITMAP_WIDTH; xx++) if (bitmapPixel(dots, xx, yy)) { left = xx < left ? xx : left; right = xx > right ? xx : right; top = yy < top ? yy : top; bottom = yy > bottom ? yy : bottom; } int tw = right - left + 1, th = bottom - top + 1; if (right < 0 || tw > w || th > h) return; int ox = x + (w - tw) / 2 - left, oy = y + (h - th) / 2 - top; for (int yy = top; yy <= bottom; yy++) for (int xx = left; xx <= right; xx++) if (bitmapPixel(dots, xx, yy)) setBitmapPixel(bitmap, ox + xx, oy + yy, !bitmapPixel(bitmap, ox + xx, oy + yy)); } // Bar: a frame (optional) filled from the left by the Home Assistant value static void drawBar(const PageElement &e, PageResources &resources, uint8_t *bitmap, ElementBox &box) { int w = e.bar_width, h = e.bar_height; int left = e.x; if (e.align == Align::Center) left -= w / 2; else if (e.align == Align::Right) left -= w; box.x = left; box.y = e.y; box.width = w; box.height = h; if (e.overlay == Overlay::Box) clearBox(bitmap, left - 1, e.y - 1, w + 2, h + 2); // With a frame the fill is inside it (a bar of 1 or 2 dots high has no room: no frame) bool frame = e.bar_frame && w >= 3 && h >= 3; int inner = frame ? 1 : 0; int fill = barFill(e, resources.haValue(e), w - 2 * inner); for (int y = 0; y < h; y++) for (int x = 0; x < w; x++) { bool edge = frame && (x == 0 || y == 0 || x == w - 1 || y == h - 1); bool filled = !edge && x - inner < fill; if (!edge && !filled) continue; int bx = left + x, by = e.y + y; setBitmapPixel(bitmap, bx, by, e.overlay == Overlay::Invert ? !bitmapPixel(bitmap, bx, by) : true); } if (e.format == BAR_PERCENT) drawBarPercent(e, resources, bitmap, left + inner, e.y + inner, w - 2 * inner, h - 2 * inner); } // Life without a running board (a preview of a page that is not shown): the start, the // pattern in the middle or a random fill that is always the same, in area coordinates static void lifeStart(const PageElement &e, int w, int h, PageResources &resources, uint8_t *start) { memset(start, 0, BITMAP_BYTES); uint32_t random = 1; uint8_t pattern[BITMAP_BYTES]; int dx, dy; if (e.input_unit == LIFE_START_PATTERN) { if (!resources.background(e.text, pattern) || !lifePlacement(pattern, w, h, false, random, dx, dy)) return; for (int y = 0; y < BITMAP_HEIGHT; y++) for (int x = 0; x < BITMAP_WIDTH; x++) if (bitmapPixel(pattern, x, y)) setBitmapPixel(start, x + dx, y + dy, true); return; } int percent = e.input_unit == LIFE_START_RANDOM ? (int)e.factor : 30; for (int y = 0; y < h; y++) for (int x = 0; x < w; x++) setBitmapPixel(start, x, y, (int)(lifeRandom(random) % 100) < percent); } // Life: the running board of the shown page; returns the ms until its next generation static uint32_t drawLife(const PageElement &e, uint32_t now_ms, PageResources &resources, uint8_t *bitmap, ElementBox &box) { int w = e.bar_width, h = e.bar_height; int left = e.x; if (e.align == Align::Center) left -= w / 2; else if (e.align == Align::Right) left -= w; box.x = left; box.y = e.y; box.width = w; box.height = h; if (e.overlay == Overlay::Box) clearBox(bitmap, left - 1, e.y - 1, w + 2, h + 2); const LifeRunner *runner = resources.life(e); uint8_t start[BITMAP_BYTES]; if (!runner) lifeStart(e, w, h, resources, start); for (int y = 0; y < h; y++) for (int x = 0; x < w; x++) { if (!(runner ? runner->board.cell(x, y) : bitmapPixel(start, x, y))) continue; int bx = left + x, by = e.y + y; setBitmapPixel(bitmap, bx, by, e.overlay == Overlay::Invert ? !bitmapPixel(bitmap, bx, by) : true); } return runner ? runner->msUntilNext(now_ms) : 0; } // A text element; returns the ms until it scrolls on (0: it doesn't scroll) static uint32_t drawTextElement(const PageElement &e, const char *text, uint32_t now_ms, PageResources &resources, uint8_t *bitmap, ElementBox &box) { Font font = usableFont(resources, e.font); int width = textWidth(font, text), height = fontHeight(font); DrawMode mode = e.overlay == Overlay::Invert ? DrawMode::Invert : DrawMode::Set; // Without scrolling the element is as wide as its text, otherwise the window int area = e.scroll_width ? e.scroll_width : width; int left = e.x; if (e.align == Align::Center) left -= area / 2; else if (e.align == Align::Right) left -= area; box.x = left; box.y = e.y; box.width = area; box.height = height; if (e.overlay == Overlay::Box) clearBox(bitmap, left - 1, e.y - 1, area + 2, height + 2); if (width <= area) { // Fits: aligned in the window like the element int x = left; if (e.align == Align::Center) x += (area - width) / 2; else if (e.align == Align::Right) x += area - width; drawText(bitmap, font, x, e.y, text, mode); return 0; } // Scrolls from right to left, the next round follows after SCROLL_GAP dots uint32_t period = width + SCROLL_GAP; // Step n starts at n * 1000 / speed ms (the speed can be below 1 dot per second) double ms_per_step = 1000.0 / e.scroll_speed; uint64_t steps = (uint64_t)(now_ms / ms_per_step); int offset = (int)(steps % period); drawText(bitmap, font, left - offset, e.y, text, mode, left, left + area); drawText(bitmap, font, left - offset + (int)period, e.y, text, mode, left, left + area); // Until the next step uint64_t next_step_ms = (uint64_t)ceil((steps + 1) * ms_per_step); uint32_t wait = next_step_ms > now_ms ? (uint32_t)(next_step_ms - now_ms) : 0; return wait ? wait : 1; } uint32_t renderPage(const DynamicPage &page, const LocalTime &time, uint32_t now_ms, PageResources &resources, uint8_t *bitmap, ElementBox *boxes) { memset(bitmap, 0, BITMAP_BYTES); const PageSlot *slot = activeSlot(page, time.hour * 60 + time.minute); if (slot && slot->bitmap[0] && !resources.background(slot->bitmap, bitmap)) memset(bitmap, 0, BITMAP_BYTES); uint32_t next = 0; bool running = resources.stopwatchRunning(); uint32_t stopwatch_ms = resources.stopwatchMs(); for (int i = 0; i < page.element_count && i < PAGE_ELEMENTS; i++) { const PageElement &e = page.elements[i]; ElementBox box; if (e.type == ElementType::Image) drawImage(e, resources, bitmap, box); else if (e.type == ElementType::Bar) drawBar(e, resources, bitmap, box); else if (e.type == ElementType::Life) next = sooner(next, drawLife(e, now_ms, resources, bitmap, box)); else { ElementValues values; if (e.type == ElementType::HaValue) values.ha_value = resources.haValue(e); values.stopwatch_ms = stopwatch_ms; char text[80]; elementText(e, time, values, text, sizeof(text)); next = sooner(next, drawTextElement(e, text, now_ms, resources, bitmap, box)); if (elementShowsSeconds(e, time, running)) // the stopwatch's seconds count from its start next = sooner(next, e.type == ElementType::Stopwatch ? 1000 - stopwatch_ms % 1000 : 1000 - (time.millisecond % 1000)); else if (e.type == ElementType::Stopwatch && running) // H:MM: the next stopwatch minute next = sooner(next, 60000 - stopwatch_ms % 60000); } if (boxes) boxes[i] = box; } return next; }