Public Access
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:
1 parent
13a499fd97
commit
d2e81a5a36
27 files changed
+627
-38
No files matched your search
@@ -404,9 +404,10 @@ void netrunner_hud(EL512Display &it, const Fonts &f, const HomeData &d, ESPTime
|
||||
cut_frame(it, 254, 150, 258, 86);
|
||||
tag(it, f.mono, 254, 150, 14, "ZONES");
|
||||
for (int i = 0; i < 4; i++) {
|
||||
const int x = 264 + (i % 2) * 124, y = 192 + (i / 2) * 34;
|
||||
it.print(x, y, f.label, TextAlign::BASELINE_LEFT, el512::fit(f.label, d.rooms[i].name, 62).c_str());
|
||||
it.print(x + 116, y, f.value, TextAlign::BASELINE_RIGHT, temp_text(d.rooms[i].value).c_str());
|
||||
// Name on top, temperature below it.
|
||||
const int x = 264 + (i % 2) * 124, y = 164 + (i / 2) * 36;
|
||||
it.print(x, y + 11, f.mono, TextAlign::BASELINE_LEFT, el512::fit(f.mono, upper(d.rooms[i].name), 114).c_str());
|
||||
it.print(x, y + 33, f.value, TextAlign::BASELINE_LEFT, temp_text(d.rooms[i].value).c_str());
|
||||
}
|
||||
|
||||
// Bottom: ticker.
|
||||
@@ -432,7 +433,7 @@ void breach_protocol(EL512Display &it, const Fonts &f) {
|
||||
|
||||
// Header and timer.
|
||||
tag(it, f.title, 0, 0, 24, "BREACH PROTOCOL");
|
||||
it.print(420, 18, f.label, TextAlign::BASELINE_RIGHT, "BREACH TIME REMAINING");
|
||||
it.print(420, 18, f.label, TextAlign::BASELINE_RIGHT, "TIME REMAINING");
|
||||
cut_frame(it, 428, 0, 84, 26, 6);
|
||||
const bool low = v.remain < 10.0f && (t / 250) % 2;
|
||||
if (!low)
|
||||
@@ -441,8 +442,8 @@ void breach_protocol(EL512Display &it, const Fonts &f) {
|
||||
|
||||
// Code matrix.
|
||||
cut_frame(it, 0, 40, 296, 216);
|
||||
it.print(10, 54, f.mono, TextAlign::BASELINE_LEFT, "CODE MATRIX");
|
||||
draw_matrix(it, {10, 62, 46, 31, f.code}, p, v);
|
||||
it.print(10, 52, f.mono, TextAlign::BASELINE_LEFT, "CODE MATRIX");
|
||||
draw_matrix(it, {10, 64, 46, 31, f.code}, p, v);
|
||||
|
||||
// Buffer.
|
||||
it.print(304, 54, f.mono, TextAlign::BASELINE_LEFT, "BUFFER");
|
||||
|
||||
@@ -96,6 +96,12 @@ inline std::string upper(std::string s) {
|
||||
return s;
|
||||
}
|
||||
|
||||
inline std::string lower(std::string s) {
|
||||
for (auto &c : s)
|
||||
c = (char) tolower((unsigned char) c);
|
||||
return s;
|
||||
}
|
||||
|
||||
inline int text_width(display::Display &it, display::BaseFont *font, const char *text) {
|
||||
int x, y, w, h;
|
||||
it.get_text_bounds(0, 0, text, font, display::TextAlign::TOP_LEFT, &x, &y, &w, &h);
|
||||
|
||||
@@ -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);
|
||||
|
||||
+24
-17
@@ -223,7 +223,7 @@ void d_mail(EL512Display &it, const Fonts &f, const HomeData &d) {
|
||||
for (int b = 0; b < 4; b++)
|
||||
it.filled_rectangle(146 + b * 6, 26 - b * 2 - 2, 4, b * 2 + 2, COLOR_OFF);
|
||||
if (c < SEND) {
|
||||
it.print(35, 34, f.pixel, TextAlign::TOP_LEFT, "TO: ME, 1 WEEK AGO");
|
||||
it.print(35, 34, f.pixel, TextAlign::TOP_LEFT, "TO: ME, -1 WEEK");
|
||||
it.horizontal_line(34, 45, 138);
|
||||
std::string body = c < TYPE_AT ? "" : typed(msg, c - TYPE_AT, CPS);
|
||||
if ((t / 300) % 2)
|
||||
@@ -267,7 +267,11 @@ void d_mail(EL512Display &it, const Fonts &f, const HomeData &d) {
|
||||
it.draw_pixel_at(x, y, COLOR_ON);
|
||||
it.printf(MX + 209, MY + 23, f.mono, TextAlign::CENTER, "0:%02u", (unsigned) ((ZAP + 300 - c) / 1000));
|
||||
} else {
|
||||
it.print(MX + 209, MY + 23, f.mono, TextAlign::CENTER, upper(watts_text(d.house)).c_str());
|
||||
// The microwave's little display: "850W" or "12.0kW".
|
||||
const std::string lcd = std::isnan(d.house) ? "--"
|
||||
: d.house >= 1000 ? str("%.1fkW", d.house / 1000.0f)
|
||||
: str("%.0fW", d.house);
|
||||
it.print(MX + 209, MY + 23, f.mono, TextAlign::CENTER, lcd.c_str());
|
||||
}
|
||||
// Turntable with a banana on it.
|
||||
for (int a = 0; a < 360; a += 15) {
|
||||
@@ -374,17 +378,19 @@ void gadget_lab(EL512Display &it, const Fonts &f, const HomeData &d) {
|
||||
// Detail card for the selected gadget.
|
||||
const Gadget &g = gadgets[sel];
|
||||
const uint32_t since = t % STEP;
|
||||
it.rectangle(340, 38, 170, 128);
|
||||
it.rectangle(342, 40, 166, 124);
|
||||
it.printf(350, 46, f.title, TextAlign::TOP_LEFT, "NO.%s", g.no.c_str());
|
||||
it.print(502, 46, f.title, TextAlign::TOP_RIGHT, g.value.c_str());
|
||||
int y = 74;
|
||||
// Number, name, value, then the description.
|
||||
it.rectangle(340, 38, 170, 140);
|
||||
it.rectangle(342, 40, 166, 136);
|
||||
it.printf(350, 44, f.title, TextAlign::TOP_LEFT, "NO.%s", g.no.c_str());
|
||||
int y = 68;
|
||||
for (const auto &l : wrap(it, f.mono, g.name, 150, 2)) {
|
||||
it.print(350, y, f.mono, TextAlign::TOP_LEFT, l.c_str());
|
||||
y += 15;
|
||||
}
|
||||
it.horizontal_line(350, y + 2, 150);
|
||||
y += 8;
|
||||
it.print(350, y + 2, f.title, TextAlign::TOP_LEFT, g.value.c_str());
|
||||
y += 26;
|
||||
it.horizontal_line(350, y, 150);
|
||||
y += 5;
|
||||
for (const auto &l : wrap(it, f.mono, typed(g.desc, since, 40), 150, 3)) {
|
||||
it.print(350, y, f.mono, TextAlign::TOP_LEFT, l.c_str());
|
||||
y += 15;
|
||||
@@ -429,7 +435,7 @@ void world_lines(EL512Display &it, const Fonts &f, const HomeData &d, ESPTime no
|
||||
}
|
||||
for (int h = 0; h <= 24; h += 6) {
|
||||
it.vertical_line(xmap(h), PY1, 4);
|
||||
it.printf(xmap(h), PY1 + 8, f.pixel, TextAlign::TOP_CENTER, "%02d:00", h % 24);
|
||||
it.printf(xmap(h), PY1 + 8, f.pixel, h == 24 ? TextAlign::TOP_RIGHT : TextAlign::TOP_CENTER, "%02d:00", h);
|
||||
}
|
||||
|
||||
// Attractor fields.
|
||||
@@ -441,14 +447,15 @@ void world_lines(EL512Display &it, const Fonts &f, const HomeData &d, ESPTime no
|
||||
const int phase = (int) (t / 80);
|
||||
for (const auto &fl : fields) {
|
||||
const int y = ymap(fl.div);
|
||||
if (fl.div == 1.048596f) {
|
||||
// Labels on the left, where the chart stays empty until sunrise: Beta's
|
||||
// above its line, the others below theirs (Steins Gate and Beta are only a
|
||||
// few pixels apart).
|
||||
if (fl.div == 1.048596f)
|
||||
it.horizontal_line(PX0 + 1, y, PX1 - PX0);
|
||||
it.print(PX1, y - 3, f.pixel, TextAlign::BOTTOM_RIGHT, fl.name);
|
||||
} else {
|
||||
else
|
||||
dashed_hline(it, PX0 + 1, y, PX1 - PX0, 4, 4, phase);
|
||||
it.print(PX1, fl.div > 1.1f ? y - 3 : y + 3, f.pixel, fl.div > 1.1f ? TextAlign::BOTTOM_RIGHT : TextAlign::TOP_RIGHT,
|
||||
fl.name);
|
||||
}
|
||||
const bool above = fl.div > 1.1f;
|
||||
it.print(PX0 + 6, above ? y - 3 : y + 3, f.pixel, above ? TextAlign::BOTTOM_LEFT : TextAlign::TOP_LEFT, fl.name);
|
||||
}
|
||||
|
||||
// Today's world line, drawn in over the first 1.5 s.
|
||||
@@ -472,7 +479,7 @@ void world_lines(EL512Display &it, const Fonts &f, const HomeData &d, ESPTime no
|
||||
points++;
|
||||
}
|
||||
if (points == 0 && (t / 600) % 2)
|
||||
it.print((PX0 + PX1) / 2, (PY0 + PY1) / 2 + 30, f.mono, TextAlign::CENTER, "COLLECTING WORLD LINE DATA...");
|
||||
it.print((PX0 + PX1) / 2 - 40, PY1 - 10, f.mono, TextAlign::CENTER, "COLLECTING WORLD LINE DATA...");
|
||||
|
||||
// Where we are now, and the futures branching off it.
|
||||
if (now.is_valid() && t >= 1500) {
|
||||
|
||||
@@ -173,16 +173,16 @@ void lcars_status(EL512Display &it, const Fonts &f, const HomeData &d, ESPTime n
|
||||
for (int i = 0; i < 3; i++) {
|
||||
const int y = 48 + i * 24;
|
||||
block(it, f.small, 84, y, 66, 18, rows[i].name);
|
||||
readout(it, 156, y + 4, 90, 10, rows[i].w / 5000.0f);
|
||||
readout(it, 156, y + 4, 80, 10, rows[i].w / 5000.0f);
|
||||
it.print(298, y + 18, f.small, TextAlign::BOTTOM_RIGHT, kw(rows[i].w).c_str());
|
||||
}
|
||||
|
||||
// Environmental.
|
||||
it.print(84, 128, f.small, TextAlign::TOP_LEFT, "ENVIRONMENTAL DECKS 1-4");
|
||||
for (int i = 0; i < 4; i++) {
|
||||
const int x = 84 + (i % 2) * 110, y = 150 + (i / 2) * 40;
|
||||
const int x = 84 + (i % 2) * 110, y = 148 + (i / 2) * 44;
|
||||
it.print(x, y, f.small, TextAlign::TOP_LEFT, fit(f.small, upper(d.rooms[i].name), 104).c_str());
|
||||
it.print(x, y + 16, f.medium, TextAlign::TOP_LEFT, deg(d.rooms[i].value).c_str());
|
||||
it.print(x, y + 14, f.medium, TextAlign::TOP_LEFT, deg(d.rooms[i].value).c_str());
|
||||
}
|
||||
|
||||
// Clock, sensors, fabrication bay.
|
||||
@@ -200,7 +200,7 @@ void lcars_status(EL512Display &it, const Fonts &f, const HomeData &d, ESPTime n
|
||||
for (int col = 0; col < 2; col++) {
|
||||
for (int row = 0; row < 9; row++) {
|
||||
const uint32_t r = hash(col * 31 + row, (t / 180) + row * 3);
|
||||
const int x = 446 + col * 32, y = 96 + row * 12;
|
||||
const int x = 456 + col * 30, y = 96 + row * 12;
|
||||
if (r % 5 == 0)
|
||||
continue;
|
||||
it.printf(x, y, f.pixel, TextAlign::TOP_LEFT, "%03u", (unsigned) (r % 1000));
|
||||
@@ -325,8 +325,8 @@ void warp_core(EL512Display &it, const Fonts &f, const HomeData &d) {
|
||||
for (int i = 0; i < 3; i++) {
|
||||
const int y = 36 + i * 44;
|
||||
it.print(0, y, f.small, TextAlign::TOP_LEFT, rows[i].name);
|
||||
it.print(204, y, f.small, TextAlign::TOP_RIGHT, rows[i].value.c_str());
|
||||
readout(it, 0, y + 22, 200, 10, rows[i].frac);
|
||||
it.print(196, y, f.small, TextAlign::TOP_RIGHT, rows[i].value.c_str());
|
||||
readout(it, 0, y + 22, 196, 10, rows[i].frac);
|
||||
}
|
||||
it.print(0, 172, f.small, TextAlign::TOP_LEFT, "MATTER / ANTIMATTER 1:1");
|
||||
it.print(0, 192, f.small, TextAlign::TOP_LEFT, "DILITHIUM MATRIX STABLE");
|
||||
@@ -409,7 +409,7 @@ void captains_log(EL512Display &it, const Fonts &f, const HomeData &d, ESPTime n
|
||||
std::string entry = "We are holding position at home.";
|
||||
if (!std::isnan(d.temp))
|
||||
entry += str(" Outside it is %.0f degrees", d.temp) +
|
||||
(d.weather.empty() ? std::string(".") : " and " + weather_text(d.weather) + ".");
|
||||
(d.weather.empty() ? std::string(".") : " and " + lower(weather_text(d.weather)) + ".");
|
||||
if (!std::isnan(d.solar))
|
||||
entry += " The solar collectors deliver " + watts(d.solar) +
|
||||
(std::isnan(d.house) ? std::string(".") : " while the replicators draw " + watts(d.house) + ".");
|
||||
|
||||
Reference in new issue
Block a user