Files
Flipdot-display/src/esp32/logic/page_render.cpp
T

212 lines
6.7 KiB
C++
Raw Normal View History

#include "logic/page_render.h"
2026-10-16 16:23:16 +02:00
#include <math.h>
#include <stdio.h>
#include <string.h>
#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: 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);
}
}
// 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;
2026-10-16 16:23:16 +02:00
// 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
2026-10-16 16:23:16 +02:00
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
{
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;
}