Add a PC renderer for the themed pages; fix overlapping layouts

tools/render_pages.py compiles the page code natively with g++ against
ESPHome's own display and font code and the font bitmaps of the last
`esphome compile`, and draws pages to PNG at chosen moments with sample
data (normal, empty, alert, busy), as a contact sheet, or as one example
per page in docs/images/pages (now shown in the README).

Fixes found with it: Cyber HUD zone names ran into the temperatures,
the breach header label hit the tag and the column marker the matrix
label; KonoSuba subtitle, guild weather icon, cabbage text and all-day
deadline, and hills through the party; Steins;Gate D-mail recipient, the
microwave display, the gadget card value and world line labels; LCARS
room rows, readout bars and data cascade, warp core readouts, and the
captain's log weather wording.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
mangavalkandClaude Opus 5.5 committed 2026-10-19 16:46:31 +02:00
1 parent 13a499fd97
commit d2e81a5a36
27 files changed
+627 -38

No files matched your search

+7 -7
View File
@@ -215,7 +215,7 @@ void explosion(EL512Display &it, const Fonts &f, const HomeData &d, ESPTime now)
// Title and clock (hidden while the sky is on fire).
if (!blasting) {
it.print(8, 4, f.jp_big, TextAlign::TOP_LEFT, "爆裂魔法");
it.print(10, 52, f.pixel, TextAlign::TOP_LEFT, "EXPLOSION MAGIC");
it.print(10, 56, f.pixel, TextAlign::TOP_LEFT, "EXPLOSION MAGIC");
if (now.is_valid())
it.strftime(504, 6, f.pixel_l, TextAlign::TOP_RIGHT, "%H:%M", now);
}
@@ -372,7 +372,7 @@ void guild_board(EL512Display &it, const Fonts &f, const HomeData &d, ESPTime no
if (quests.size() == 3)
break;
const std::string day = field(e, 0), time = field(e, 1);
quests.push_back({field(e, 2), upper(day) + " " + (time.empty() ? "ALL DAY" : time), true, day == today});
quests.push_back({field(e, 2), upper(day) + (time.empty() ? "" : " " + time), true, day == today});
}
static const char *const STANDING[][2] = {{"Exterminate five giant toads", "BEFORE DUSK"},
{"Escort a merchant cart to the next town", "THIS WEEK"},
@@ -412,13 +412,13 @@ void guild_board(EL512Display &it, const Fonts &f, const HomeData &d, ESPTime no
// Hazard notice (weather) and the cabbage bounty (solar).
note(it, 386, 48, 116, 94);
it.print(444, 60, f.pixel, TextAlign::TOP_CENTER, "HAZARD");
it.print(416, 102, f.weather, TextAlign::CENTER, weather_icon(d.weather));
it.print(416, 98, f.weather, TextAlign::CENTER, weather_icon(d.weather));
it.print(492, 94, f.text, TextAlign::BASELINE_RIGHT, std::isnan(d.temp) ? "--" : str("%.0f°", d.temp).c_str());
it.print(444, 128, f.pixel, TextAlign::TOP_CENTER, fit(f.pixel, upper(weather_text(d.weather)), 100).c_str());
it.print(444, 131, f.pixel, TextAlign::TOP_CENTER, fit(f.pixel, upper(weather_text(d.weather)), 100).c_str());
note(it, 386, 150, 116, 94);
it.print(444, 162, f.pixel, TextAlign::TOP_CENTER, "BOUNTY");
it.print(444, 176, f.text, TextAlign::TOP_CENTER, "Cabbage harvest");
it.print(444, 176, f.text, TextAlign::TOP_CENTER, "Cabbage crop");
it.print(444, 198, f.text, TextAlign::TOP_CENTER,
std::isnan(d.solar_today) ? "--" : str("%.1f kWh", d.solar_today / 1000.0f).c_str());
it.printf(444, 222, f.pixel, TextAlign::TOP_CENTER, "NOW %s", upper(watts_text(d.solar)).c_str());
@@ -535,8 +535,8 @@ void party_battle(EL512Display &it, const Fonts &f, const HomeData &d) {
// Scenery.
it.print(8, 6, f.jp, TextAlign::TOP_LEFT, "戦闘");
it.printf(56, 12, f.pixel, TextAlign::TOP_LEFT, "ROUND %u", (unsigned) (round + 1));
arc(it, 110, GROUND + 70, 110, -52, 52);
arc(it, 300, GROUND + 90, 120, -40, 40);
// A hill between the party and the toad, clear of both.
arc(it, 270, GROUND + 50, 80, -46, 46);
it.horizontal_line(0, GROUND, W);
for (int i = 0; i < 18; i++) {
const int x = (int) (hash(i, 21) % W);