Files
Flipdot-display/src/esp32/logic/page_render.cpp
T
mangavalkandClaude Opus 5.5 fc6fa1a180 Add scrolling, countdown, stopwatch, images, seconds, multiplier and degree sign
- Text elements can scroll in a window at a set speed; clock, countdown and
  stopwatch can show seconds. Moving pages are sent as instant frames (new
  Nano command D1) and paced by the estimated flip time.
- Countdown to a date and time (smart, days, days + time, with seconds),
  stopwatch (web UI, HA switch and reset button), image element (fixed or
  by Home Assistant state), multiplier for HA values, degree sign (char 127)
  in all fonts and the font converter.
- Page format version 2; version 1 pages are converted.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-15 21:01:14 +02:00

176 lines
5.4 KiB
C++

#include "logic/page_render.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);
}
}
// 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;
uint64_t steps = (uint64_t)now_ms * e.scroll_speed / 1000;
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 = ((steps + 1) * 1000 + e.scroll_speed - 1) / e.scroll_speed;
uint32_t wait = (uint32_t)(next_step_ms - now_ms);
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
{
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;
}