Files
Flipdot-display/src/esp32/logic/page_render.cpp
T
mangavalkandClaude Opus 5.5 a91d2550d7 Game of Life page element and built-in page
A new page element runs Conway's Game of Life in an area (the whole display
by default), one per page. Start: random dots, one pattern, or the icons
whose names start with a text in turn or in random order. Edges: wrap
around, dead, or a hidden margin where gliders fly out of view (leftovers
that stay put in the margin are cleared, so a glider gun runs cleanly).
It restarts when the board is still or repeats within 16 generations
and/or after a number of generations, after a hold time (an empty board
at once).

logic/life holds the board and timing (tested), display/life_player runs
the board of the shown page, ticked before each frame; frames are paced
like scrolling text. The element reuses existing fields, so the page
format stays the same. The Icons tab has a library of 17 LifeWiki
patterns (checked with a simulation), and the built-in page "Game of
Life" shuffles through the life_ icons. About 12 KB flash, 8 KB RAM.

Version 2.17.0, tested on the display over OTA.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-20 17:52:25 +02:00

305 lines
10 KiB
C++

#include "logic/page_render.h"
#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 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;
}