Public Access
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>
This commit is contained in:
1 parent
ca8d78ea8a
commit
fc6fa1a180
38 files changed
+1370
-277
No files matched your search
+140
-23
@@ -1,5 +1,6 @@
|
||||
#include "logic/page_render.h"
|
||||
|
||||
#include <stdio.h>
|
||||
#include <string.h>
|
||||
|
||||
#include "logic/bitmap_format.h"
|
||||
@@ -20,39 +21,155 @@ static void clearBox(uint8_t *bitmap, int x, int y, int width, int height)
|
||||
setBitmapPixel(bitmap, xx, yy, false);
|
||||
}
|
||||
|
||||
void renderPage(const DynamicPage &page, const LocalTime &time, PageResources &resources, uint8_t *bitmap,
|
||||
ElementBox *boxes)
|
||||
// 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];
|
||||
const char *ha_value = e.type == ElementType::HaValue ? resources.haValue(e) : nullptr;
|
||||
char text[64];
|
||||
elementText(e, time, ha_value, text, sizeof(text));
|
||||
|
||||
Font font = usableFont(resources, e.font);
|
||||
int width = textWidth(font, text), height = fontHeight(font);
|
||||
int x = e.x;
|
||||
if (e.align == Align::Center)
|
||||
x -= width / 2;
|
||||
else if (e.align == Align::Right)
|
||||
x -= width;
|
||||
|
||||
if (e.overlay == Overlay::Box)
|
||||
clearBox(bitmap, x - 1, e.y - 1, width + 2, height + 2);
|
||||
drawText(bitmap, font, x, e.y, text, e.overlay == Overlay::Invert ? DrawMode::Invert : DrawMode::Set);
|
||||
|
||||
if (boxes)
|
||||
ElementBox box;
|
||||
if (e.type == ElementType::Image)
|
||||
drawImage(e, resources, bitmap, box);
|
||||
else
|
||||
{
|
||||
boxes[i].x = x;
|
||||
boxes[i].y = e.y;
|
||||
boxes[i].width = width;
|
||||
boxes[i].height = height;
|
||||
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;
|
||||
}
|
||||
Reference in new issue
Block a user