From f46f7ab6c57a067fc2c38b9ed96650f8f1d5643d Mon Sep 17 00:00:00 2001 From: MangaValk Date: Mon, 19 Oct 2026 18:59:00 +0200 Subject: [PATCH] Add Interstellar and Your Name pages Interstellar: Gargantua with a time dilation clock (Earth today vs. Miller's planet), TARS with his settings and remarks about the house, the Endurance spinning at a solar-driven RPM while a Ranger docks, and the tesseract's bookshelf with the time in binary dust and the Message's first word in Morse on a watch. Your Name: comet Tiamat splitting over Itomori's lake, and kataware-doki, a countdown to sunset from sun.sun where two silhouettes stand on the crater rim during the twilight hour. HomeData gains sunset, sunrise and sun elevation (with an ISO time parser); wrap() and typed() move to el512_pages.h. Share Tech Mono 22 gets the full Latin set. All pages are in the renderer, checked with normal, busy and empty data. Co-Authored-By: Claude Opus 5.5 --- README.md | 61 +++- components/el512/el512_display.h | 2 + components/el512/el512_pages.h | 66 ++++ components/el512/interstellar.cpp | 481 ++++++++++++++++++++++++++++ components/el512/interstellar.h | 33 ++ components/el512/konosuba.cpp | 34 -- components/el512/steins.cpp | 32 -- components/el512/trek.cpp | 32 -- components/el512/yourname.cpp | 239 ++++++++++++++ components/el512/yourname.h | 32 ++ components/el512/yourname_sprites.h | 65 ++++ docs/images/pages/is_endurance.png | Bin 0 -> 6887 bytes docs/images/pages/is_gargantua.png | Bin 0 -> 7563 bytes docs/images/pages/is_tars.png | Bin 0 -> 6290 bytes docs/images/pages/is_tesseract.png | Bin 0 -> 8443 bytes docs/images/pages/yn_comet.png | Bin 0 -> 6808 bytes docs/images/pages/yn_twilight.png | Bin 0 -> 7439 bytes esphome/el512-panel.yaml | 95 +++++- tools/render/el512_display.h | 2 + tools/render/render_main.cpp | 14 + tools/render_pages.py | 8 +- tools/yourname_sprites.py | 73 +++++ 22 files changed, 1159 insertions(+), 110 deletions(-) create mode 100644 components/el512/interstellar.cpp create mode 100644 components/el512/interstellar.h create mode 100644 components/el512/yourname.cpp create mode 100644 components/el512/yourname.h create mode 100644 components/el512/yourname_sprites.h create mode 100644 docs/images/pages/is_endurance.png create mode 100644 docs/images/pages/is_gargantua.png create mode 100644 docs/images/pages/is_tars.png create mode 100644 docs/images/pages/is_tesseract.png create mode 100644 docs/images/pages/yn_comet.png create mode 100644 docs/images/pages/yn_twilight.png create mode 100644 tools/yourname_sprites.py diff --git a/README.md b/README.md index 8cb6ea5..567faff 100644 --- a/README.md +++ b/README.md @@ -86,6 +86,8 @@ components/el512/ shared driver + ESPHome component (one copy of the code) konosuba.* animated KonoSuba pages; konosuba_sprites.h from tools/ steins.* animated Steins;Gate pages; steins_sprites.h from tools/ trek.* animated Star Trek / Lower Decks pages; trek_sprites.h from tools/ + interstellar.* animated Interstellar pages + yourname.* animated Your Name pages; yourname_sprites.h from tools/ el512_pages.h home data and helpers shared by the themed pages el512_display.* ESPHome display platform `el512` __init__.py, display.py ESPHome config schema @@ -95,7 +97,8 @@ src/test_images/ test sketch: cycles through 12 test images (env esp32dev esphome/ el512-panel.yaml (Home Assistant display), el512-smoke.yaml home_assistant/packages/ el512_panel.yaml: helper sensors for the ESPHome pages tools/ import_still_alive.py, konosuba_sprites.py, steins_sprites.py, - trek_sprites.py, render_pages.py (+ render/: PC renderer) + trek_sprites.py, yourname_sprites.py, render_pages.py + (+ render/: PC renderer) test/ unit tests (placeholder) docs/ datasheet and notes ``` @@ -114,11 +117,12 @@ pio device monitor ## ESPHome / Home Assistant [esphome/el512-panel.yaml](esphome/el512-panel.yaml) turns the panel into a Home -Assistant display with twenty-four pages: clock and weather, forecast, agenda, a -sensor dashboard with a message line, now playing, Plex, solar, Still Alive, and -sixteen animated themed pages ([Cyberpunk](#cyberpunk-pages), +Assistant display with thirty pages: clock and weather, forecast, agenda, a sensor +dashboard with a message line, now playing, Plex, solar, Still Alive, and +twenty-two animated themed pages ([Cyberpunk](#cyberpunk-pages), [KonoSuba](#konosuba-pages), [Steins;Gate](#steinsgate-pages), -[Star Trek](#star-trek-pages)). Pages +[Star Trek](#star-trek-pages), [Interstellar](#interstellar-pages), +[Your Name](#your-name-pages)). Pages rotate on an interval, in a random order that is reshuffled every hour; Home Assistant gets a page select, a pause switch, the interval, a message text, a "Play Still Alive" button, and Display / Low power mode switches (ENABLE, _LOWPWR). @@ -329,6 +333,53 @@ These pages are fan art: the pixel art, layouts and texts are original, and no m from the series is used. Star Trek and Star Trek: Lower Decks are trademarks of CBS Studios Inc.; this project is not affiliated with or endorsed by them. +## Interstellar pages + +| | | +|---|---| +| ![Gargantua](docs/images/pages/is_gargantua.png) | ![TARS](docs/images/pages/is_tars.png) | +| ![Endurance](docs/images/pages/is_endurance.png) | ![Tesseract](docs/images/pages/is_tesseract.png) | + +Four animated pages drawn by +[components/el512/interstellar.cpp](components/el512/interstellar.cpp) with Michroma +and Share Tech Mono, all in the rotation: + +- **Gargantua**: the black hole with its lensed accretion disk turning, and time + dilation: today's Earth time next to today on Miller's planet (one hour there is seven + years here), with a bar filling towards the next Miller second, 17 Earth hours away. +- **TARS**: the robot's four slabs stepping in turn, his settings (humor, honesty, + discretion, trust) next to the home's readings, and dry remarks about the house. +- **Endurance**: the ring spins at an RPM set by solar power while a Ranger flies in, + matches the spin and docks in the hub. +- **Tesseract**: the bookshelf from behind with a book falling out, the time as + binary bars of dust, and the first word of the Message (or STAY) in Morse code on a + watch's second hand. + +These pages are fan art: drawings and texts are original. Interstellar is © Paramount +Pictures and Warner Bros.; this project is not affiliated with or endorsed by them. + +## Your Name pages + +| | | +|---|---| +| ![Comet Tiamat](docs/images/pages/yn_comet.png) | ![Kataware-doki](docs/images/pages/yn_twilight.png) | + +Two animated pages drawn by [components/el512/yourname.cpp](components/el512/yourname.cpp) +with Yuji Syuku, Caveat and Cormorant Garamond, both in the rotation: + +- **Comet Tiamat**: the comet crosses the night sky over Itomori's round lake and + splits in two, shedding sparks; the shrine gate on the hill, the town's lights, the + clock and the weather. +- **Kataware-doki**: a countdown to sunset (at night, to sunrise) from Home Assistant's + `sun.sun`. In the twilight hour, from 45 minutes before sunset until civil dusk, + the sky glows, the sun sinks behind the crater rim and two silhouettes stand on it. + +The pixel art lives in [tools/yourname_sprites.py](tools/yourname_sprites.py). + +These pages are fan art: the pixel art, drawings and texts are original. Your Name +(Kimi no Na wa) is by Makoto Shinkai, © its rights holders; this project is not +affiliated with or endorsed by them. + ## Rendering pages on the PC [tools/render_pages.py](tools/render_pages.py) draws the themed pages to PNG without diff --git a/components/el512/el512_display.h b/components/el512/el512_display.h index 3c49380..a0f87a6 100644 --- a/components/el512/el512_display.h +++ b/components/el512/el512_display.h @@ -4,9 +4,11 @@ #include "cyberpunk.h" #include "el512_helpers.h" +#include "interstellar.h" #include "konosuba.h" #include "steins.h" #include "trek.h" +#include "yourname.h" #include "el512_panel.h" #include "esphome/components/display/display_buffer.h" #include "esphome/core/component.h" diff --git a/components/el512/el512_pages.h b/components/el512/el512_pages.h index bc89996..693cf07 100644 --- a/components/el512/el512_pages.h +++ b/components/el512/el512_pages.h @@ -9,7 +9,9 @@ #include #include #include +#include +#include "el512_helpers.h" #include "esphome/components/display/display.h" #include "esphome/core/time.h" @@ -34,6 +36,9 @@ struct HomeData { std::string agenda; // sensor.el512_agenda events: "day|time|title;..." std::string message; // the panel's Message text std::string printer; // 3D printer state (Moonraker: printing, paused, error, ...) + int64_t sunset{0}; // next sunset, Unix time (sun.sun next_setting); 0 = unknown + int64_t sunrise{0}; // next sunrise, Unix time (sun.sun next_rising); 0 = unknown + float sun_elevation{NAN}; // degrees above the horizon Reading rooms[4]; // Today's solar power in 15-minute slots (highest value seen per slot; -1 = @@ -96,6 +101,30 @@ inline std::string upper(std::string s) { return s; } +// Days since 1970-01-01 for a civil date (Howard Hinnant's algorithm). +inline int64_t days_from_civil(int y, int m, int d) { + y -= m <= 2; + const int era = (y >= 0 ? y : y - 399) / 400; + const int yoe = y - era * 400; + const int doy = (153 * (m + (m > 2 ? -3 : 9)) + 2) / 5 + d - 1; + const int doe = yoe * 365 + yoe / 4 - yoe / 100 + doy; + return (int64_t) era * 146097 + doe - 719468; +} + +// "2026-10-19T16:32:10.123+00:00" (as Home Assistant sends datetimes) to Unix +// time; 0 if it doesn't parse. +inline int64_t parse_iso_utc(const std::string &s) { + int y, mo, d, h, mi, se; + if (sscanf(s.c_str(), "%d-%d-%dT%d:%d:%d", &y, &mo, &d, &h, &mi, &se) != 6) + return 0; + int64_t t = days_from_civil(y, mo, d) * 86400 + h * 3600 + mi * 60 + se; + const size_t p = s.find_first_of("+-", 19); + int oh, om; + if (p != std::string::npos && sscanf(s.c_str() + p + 1, "%d:%d", &oh, &om) == 2) + t -= (s[p] == '+' ? 1 : -1) * (int64_t) (oh * 3600 + om * 60); + return t; +} + inline std::string lower(std::string s) { for (auto &c : s) c = (char) tolower((unsigned char) c); @@ -108,6 +137,43 @@ inline int text_width(display::Display &it, display::BaseFont *font, const char return w; } +// Greedy word wrap into at most max_lines lines of `width` pixels; the last +// line ends in "..." when text is left over. +inline std::vector wrap(display::Display &it, display::BaseFont *font, const std::string &text, int width, + size_t max_lines) { + std::vector lines; + std::string line; + size_t i = 0; + while (i <= text.size()) { + size_t j = text.find(' ', i); + if (j == std::string::npos) + j = text.size(); + const std::string word = text.substr(i, j - i); + const std::string joined = line.empty() ? word : line + " " + word; + if (!line.empty() && text_width(it, font, joined.c_str()) > width) { + lines.push_back(line); + line = word; + } else { + line = joined; + } + i = j + 1; + } + if (!line.empty()) + lines.push_back(line); + if (lines.size() > max_lines) { + lines.resize(max_lines); + lines.back() += "..."; + } + for (auto &l : lines) + l = fit(font, l, width); + return lines; +} + +// The part of `s` typed so far at `cps` characters per second. +inline std::string typed(const std::string &s, uint32_t t, int cps) { + return s.substr(0, std::min(s.size(), (size_t) t * cps / 1000)); +} + // Draw a sprite (optionally only its first `rows` rows), scaled up in blocks. inline void sprite(display::Display &it, const Sprite &s, int x, int y, int scale = 1, int rows = -1, Color color = display::COLOR_ON) { diff --git a/components/el512/interstellar.cpp b/components/el512/interstellar.cpp new file mode 100644 index 0000000..ddf16d4 --- /dev/null +++ b/components/el512/interstellar.cpp @@ -0,0 +1,481 @@ +#include "interstellar.h" + +#include +#include +#include +#include + +#include "el512_display.h" +#include "esphome/core/hal.h" + +namespace esphome::el512::interstellar { + +using display::COLOR_OFF; +using display::COLOR_ON; +using display::Display; +using display::TextAlign; + +namespace { + +using namespace fx; + +constexpr int W = 512, H = 256; +const uint8_t BAYER[4][4] = {{0, 8, 2, 10}, {12, 4, 14, 6}, {3, 11, 1, 9}, {15, 7, 13, 5}}; +bool dither(int x, int y, int level) { return BAYER[y & 3][x & 3] < level; } + +std::string kw(float w) { return std::isnan(w) ? "--" : watts(w); } + +// Faint, slowly twinkling stars; skip(x, y) keeps them off busy areas. +template void stars(EL512Display &it, uint32_t t, int count, uint32_t seed, Skip skip) { + for (int i = 0; i < count; i++) { + const uint32_t r = hash(i, seed); + const int x = (int) (r % W), y = (int) ((r >> 9) % H); + if (skip(x, y) || (t / 230 + (r >> 20)) % 17 == 0) + continue; + it.draw_pixel_at(x, y, COLOR_ON); + } +} + +// Segmented bar, lit up to frac. +void seg_bar(Display &it, int x, int y, int w, int h, float frac) { + const int n = w / 5, lit = std::isnan(frac) ? 0 : (int) std::lround(std::clamp(frac, 0.0f, 1.0f) * n); + for (int i = 0; i < n; i++) { + if (i < lit) + it.filled_rectangle(x + i * 5, y, 4, h); + else + it.horizontal_line(x + i * 5, y + h - 1, 4); + } +} + +// Seconds on Earth per second on Miller's planet: one hour there is seven +// years here. +constexpr float DILATION = 7 * 365.25f * 24; + +// International Morse code, A-Z then 0-9. +const char *const MORSE[] = {".-", "-...", "-.-.", "-..", ".", "..-.", "--.", "....", "..", + ".---", "-.-", ".-..", "--", "-.", "---", ".--.", "--.-", ".-.", + "...", "-", "..-", "...-", ".--", "-..-", "-.--", "--..", "-----", + ".----", "..---", "...--", "....-", ".....", "-....", "--...", "---..", "----."}; +const char *morse(char c) { + if (c >= 'A' && c <= 'Z') + return MORSE[c - 'A']; + if (c >= '0' && c <= '9') + return MORSE[26 + c - '0']; + return nullptr; // a space, or something Morse can't send +} + +} // namespace + +// --- 1. Gargantua and time dilation --------------------------------------------------- + +void gargantua(EL512Display &it, const Fonts &f, ESPTime now) { + if (f.large == nullptr) + return; + const uint32_t t = it.page_age_ms(); + constexpr int CX = 168, CY = 124, SHADOW = 40, DISK = 160; + stars(it, t, 90, 42, [](int x, int y) { + const int dx = x - CX, dy = y - CY; + return x > 320 || dx * dx + dy * dy < 80 * 80 || (std::abs(dy) < 12 && std::abs(dx) < DISK); + }); + const float spin = t * 0.0025f; + + // The far side of the disk, bent over the top and under the bottom of the + // shadow by its gravity: a thick halo above, a thinner one below. Streaks + // run around it; the approaching (left) side is brighter. + for (int y = CY - 72; y <= CY + 58; y++) { + for (int x = CX - 72; x <= CX + 72; x++) { + const int dx = x - CX, dy = y - CY, r2 = dx * dx + dy * dy; + const int outer = dy < 0 ? 70 : 56; + if (r2 < (SHADOW + 5) * (SHADOW + 5) || r2 > outer * outer) + continue; + const float r = std::sqrt((float) r2), a = std::atan2((float) dy, (float) dx); + const int level = 8 + (int) (5 * std::sin(a * 7 + spin * 3 + r * 0.35f)) + (dx < 0 ? 3 : -1) - + (int) ((r - SHADOW) / 5); + if (dither(x, y, level)) + it.draw_pixel_at(x, y, COLOR_ON); + } + } + // Photon ring around the shadow. + it.circle(CX, CY, SHADOW + 2); + it.circle(CX, CY, SHADOW + 3); + + // The near side of the disk, edge-on across the front of the shadow. + for (int x = CX - DISK; x <= CX + DISK; x++) { + const int dx = x - CX; + const float k = 1.0f - (float) (dx * dx) / (DISK * DISK); + const int half = (int) (1 + 6 * k); + for (int y = CY - half; y <= CY + half; y++) { + const int level = 9 + (int) (5 * std::sin(dx * 0.12f - spin * 9)) + (dx < 0 ? 4 : 0) - 2 * std::abs(y - CY); + it.draw_pixel_at(x, y, dither(x, y, level) ? COLOR_ON : COLOR_OFF); + } + } + + // Time dilation. + constexpr int X = 336; + it.print(X, 6, f.title, TextAlign::TOP_LEFT, "GARGANTUA"); + it.print(X, 26, f.mono, TextAlign::TOP_LEFT, "1 HOUR = 7 EARTH YEARS"); + it.horizontal_line(X, 46, W - X - 4); + const float today = now.is_valid() ? now.hour * 3600.0f + now.minute * 60 + now.second : NAN; + it.print(X, 54, f.mono, TextAlign::TOP_LEFT, "EARTH, TODAY"); + if (now.is_valid()) { + it.strftime(X, 70, f.large, TextAlign::TOP_LEFT, "%H:%M", now); + it.printf(506, 74, f.code, TextAlign::TOP_RIGHT, ":%02d", now.second); + } + it.print(X, 116, f.mono, TextAlign::TOP_LEFT, "MILLER'S PLANET, TODAY"); + if (!std::isnan(today)) { + // One Miller second takes 17 Earth hours: three decimals, a bar for the rest. + const float miller = today / DILATION; + it.printf(X, 132, f.large, TextAlign::TOP_LEFT, "%.3f", miller); + it.print(506, 136, f.code, TextAlign::TOP_RIGHT, "S"); + it.print(X, 178, f.mono, TextAlign::TOP_LEFT, "NEXT MILLER SECOND"); + seg_bar(it, X, 196, 170, 8, miller - std::floor(miller)); + const float left = (1.0f - (miller - std::floor(miller))) * DILATION / 3600; + it.printf(X, 212, f.mono, TextAlign::TOP_LEFT, "IN %.1f EARTH HOURS", left); + } +} + +// --- 2. TARS and its settings ----------------------------------------------------------- + +void tars(EL512Display &it, const Fonts &f, const HomeData &d) { + if (f.mono == nullptr) + return; + const uint32_t t = it.page_age_ms(); + constexpr uint32_t QUIP = 7000; + const int quip = (int) ((t / QUIP) % 6); + const uint32_t q = t % QUIP; + + // TARS: four slabs; the middle pair and the outer pair step in turn. + constexpr int BX = 22, BY = 30, SW = 26, GAP = 3, BH = 192; + const int lift = ((t / 450) % 2) ? 4 : -4; + int screen_y = 0; + for (int s = 0; s < 4; s++) { + const int x = BX + s * (SW + GAP), y = BY + ((s == 1 || s == 2) ? lift : -lift); + it.rectangle(x, y, SW, BH); + for (int g = 1; g < 4; g++) + it.horizontal_line(x + 3, y + g * BH / 4, SW - 6); + it.vertical_line(x + SW / 2, y + BH - 34, 26); + if (s == 1) + screen_y = y + 16; + } + // His screen, with a waveform while he talks. + const int sx = BX + SW + GAP + 3, sw = 2 * SW + GAP - 6; + it.filled_rectangle(sx, screen_y, sw, 30); + const bool talking = q < 4000; + for (int x = 0; x < sw - 4; x++) { + const float amp = talking ? 9 * std::fabs(std::sin(x * 0.21f + t * 0.013f) * std::sin(x * 0.05f + t * 0.004f)) : 0.6f; + const int yy = screen_y + 15 + (int) (amp * std::sin(x * 0.7f + t * 0.02f)); + it.draw_pixel_at(sx + 2 + x, yy, COLOR_OFF); + } + + // Settings and home readings. + constexpr int X = 150; + it.print(X, 2, f.large, TextAlign::TOP_LEFT, "TARS"); + it.print(X + text_width(it, f.large, "TARS") + 12, 14, f.title, TextAlign::TOP_LEFT, "SETTINGS"); + const int humor = quip == 5 && q > 2600 ? 60 : 75; + struct Row { + const char *name; + float frac; + std::string value; + }; + const Row settings[] = {{"HUMOR", humor / 100.0f, str("%d%%", humor)}, + {"HONESTY", 0.9f, "90%"}, + {"DISCRETION", 0.8f, "80%"}, + {"TRUST", 1.0f, "100%"}}; + const float tf = std::isnan(d.temp) ? NAN : (d.temp + 10) / 45; + const Row readings[] = {{"SOLAR", d.solar / 5000, kw(d.solar)}, + {"HOUSE", d.house / 5000, kw(d.house)}, + {"GRID", std::fabs(d.grid) / 5000, kw(std::fabs(d.grid))}, + {"OUTSIDE", tf, std::isnan(d.temp) ? "--" : str("%.1f C", d.temp)}}; + for (int i = 0; i < 4; i++) { + const int y = 48 + i * 27; + for (int col = 0; col < 2; col++) { + const Row &r = col == 0 ? settings[i] : readings[i]; + const int x = X + col * 182; + it.print(x, y, f.mono, TextAlign::TOP_LEFT, r.name); + it.print(x + 172, y, f.mono, TextAlign::TOP_RIGHT, r.value.c_str()); + seg_bar(it, x, y + 16, 170, 5, r.frac); + } + } + + // What TARS has to say about the house. + std::string say; + switch (quip) { + case 0: + say = std::isnan(d.temp) ? "Outside temperature unknown. I'd bring a jacket anyway." + : str("Outside it's %.0f degrees. Humor setting 75%%: ", d.temp) + + (d.temp < 15 ? "bring a jacket." : "leave the jacket."); + break; + case 1: + say = "Solar is delivering " + kw(d.solar) + ". Not bad for a star 150 million kilometers away."; + break; + case 2: + say = "House load " + kw(d.house) + ". Honesty setting 90%: it's probably the dishwasher."; + break; + case 3: + say = std::isnan(d.grid) ? "Grid status unknown. Discretion setting says I shouldn't guess." + : d.grid < 0 ? "We're sending " + kw(-d.grid) + " back to the grid. Very generous of us." + : "We're drawing " + kw(d.grid) + " from the grid. Discretion setting: no comment."; + break; + case 4: { + const auto events = split(d.agenda, ';'); + say = events.empty() ? "No missions on the plan. Initiating relaxation protocol." + : "Next on the mission plan: " + field(events[0], 2) + "."; + break; + } + default: + say = "That was a joke. Lowering humor setting to 60%."; + } + it.rectangle(X, 160, W - X - 2, 92); + int y = 168; + for (const auto &l : wrap(it, f.mono, typed(say, q, 32), W - X - 20, 5)) { + it.print(X + 8, y, f.mono, TextAlign::TOP_LEFT, l.c_str()); + y += 16; + } +} + +// --- 3. The Endurance spins while a Ranger docks --------------------------------------- + +void endurance(EL512Display &it, const Fonts &f, const HomeData &d, ESPTime now) { + if (f.large == nullptr) + return; + const uint32_t t = it.page_age_ms(); + constexpr int CX = 150, CY = 128, RING = 86; + constexpr uint32_t CYCLE = 16000, MATCH = 6000, DOCK = 10000; + const uint32_t c = t % CYCLE; + const float solar = std::isnan(d.solar) ? 0 : std::max(0.0f, d.solar); + const float rpm = 20 + std::min(solar, 5000.0f) / 5000 * 50; + // Degrees per ms, shown at a tenth of the speed, or it would be a blur. + const float rate = rpm * 6 / 10 / 1000; + const float rot = rate * t; + + stars(it, t, 80, 7, [](int x, int y) { return (x - CX) * (x - CX) + (y - CY) * (y - CY) < 100 * 100 || x > 286; }); + + // The ring: twelve modules around a hub on four spokes. + it.circle(CX, CY, RING); + for (int k = 0; k < 4; k++) { + int x0, y0, x1, y1; + polar(CX, CY, 18, rot + 45 + k * 90, &x0, &y0); + polar(CX, CY, RING - 8, rot + 45 + k * 90, &x1, &y1); + it.line(x0, y0, x1, y1); + } + for (int i = 0; i < 12; i++) { + const float a = rot + i * 30; + const int half = i % 2 ? 5 : 8; + int mx, my; + polar(CX, CY, RING, a, &mx, &my); + int px[4], py[4]; + for (int k = 0; k < 4; k++) + polar(mx, my, half * 1.41f, a + 45 + k * 90, &px[k], &py[k]); + it.filled_triangle(px[0], py[0], px[1], py[1], px[2], py[2]); + it.filled_triangle(px[0], py[0], px[2], py[2], px[3], py[3]); + } + it.filled_circle(CX, CY, 18, COLOR_OFF); + it.circle(CX, CY, 18); + it.circle(CX, CY, 9); + + // The Ranger: flies in, spins up to match, then docks in the hub. + float rx, ry = CY, ang, match; + const char *status; + if (c < MATCH) { + // In from the top right of the starfield, nose at the hub. + const float k = (float) c / MATCH, e = 1 - (1 - k) * (1 - k); + rx = 258 + (CX + 40 - 258) * e; + ry = 20 + (CY - 20) * e; + ang = std::atan2(CX - rx, -(CY - ry)) * 180 / 3.14159f; + match = 0; + status = "APPROACH"; + } else if (c < DOCK) { + // Spin rate ramps up to the ring's; the angle is its integral. + const float e = (float) (c - MATCH), k = e / (DOCK - MATCH); + rx = CX + 40 - 40 * k; + match = k; + ang = 270 + rate * e * e / (2 * (DOCK - MATCH)); + status = "MATCHING SPIN"; + } else { + rx = CX; + match = 1; + ang = 270 + rate * (DOCK - MATCH) / 2 + rate * (c - DOCK); + status = "DOCKED"; + } + int nx, ny, lx, ly, bx, by, tx, ty; + polar((int) rx, (int) ry, 16, ang, &nx, &ny); // nose + polar((int) rx, (int) ry, 12, ang + 140, &lx, &ly); // wing + polar((int) rx, (int) ry, 12, ang - 140, &bx, &by); // wing + polar((int) rx, (int) ry, 5, ang + 180, &tx, &ty); // tail notch + it.filled_triangle(nx, ny, lx, ly, tx, ty); + it.filled_triangle(nx, ny, tx, ty, bx, by); + + // Readouts. + constexpr int X = 300; + it.print(X, 6, f.title, TextAlign::TOP_LEFT, "ENDURANCE"); + it.horizontal_line(X, 26, W - X - 4); + it.print(X, 34, f.mono, TextAlign::TOP_LEFT, "RING SPEED"); + it.printf(X, 50, f.large, TextAlign::TOP_LEFT, "%.0f", rpm); + it.print(X + 70, 62, f.mono, TextAlign::TOP_LEFT, "RPM"); + it.print(X, 96, f.mono, TextAlign::TOP_LEFT, "MATCH SPIN"); + it.printf(506, 96, f.mono, TextAlign::TOP_RIGHT, "%.0f%%", match * 100); + seg_bar(it, X, 114, 205, 8, match); + if (c < MATCH) + it.printf(X, 132, f.mono, TextAlign::TOP_LEFT, "DISTANCE %.0f M", (rx - CX - 18) * 3); + if (c < DOCK || (t / 400) % 2) // DOCKED blinks + it.print(X, 150, f.code, TextAlign::TOP_LEFT, status); + it.horizontal_line(X, 186, W - X - 4); + it.printf(X, 194, f.mono, TextAlign::TOP_LEFT, "POWER (SOLAR) %s", kw(d.solar).c_str()); + if (now.is_valid()) + it.printf(X, 214, f.mono, TextAlign::TOP_LEFT, "MISSION DAY %d %02d:%02d", now.day_of_year, now.hour, now.minute); +} + +// --- 4. The tesseract: books, the time in dust, a message in Morse ----------------------- + +void tesseract(EL512Display &it, const Fonts &f, const HomeData &d, ESPTime now) { + if (f.mono == nullptr) + return; + const uint32_t t = it.page_age_ms(); + + // The bookshelf, seen from behind; now and then a book falls out. + it.print(4, 2, f.title, TextAlign::TOP_LEFT, "TESSERACT"); + it.rectangle(4, 22, 190, 226); + const int shelf_y[3] = {96, 170, 246}; + const uint32_t cycle = t / 6000, p = t % 6000; + int index = 0; + const int falling = 3 + (int) (hash(cycle, 5) % 8); + for (int s = 0; s < 3; s++) { + if (s < 2) + it.horizontal_line(4, shelf_y[s], 190); + int x = 8; + for (int b = 0; x < 186; b++, index++) { + const uint32_t r = hash(s * 37 + b, 11); + const int w = std::min(186 - x, 5 + (int) (r % 9)), h = 40 + (int) ((r >> 8) % 26); + const int y = shelf_y[s] - h; + if (s == 1 && b == falling) { + if (p < 500) { + it.rectangle(x + ((p / 60) % 2), y, w - 1, h); // it shakes + } else if (p < 1300) { + const int drop = (int) ((p - 500) * (p - 500) / 8000); + if (y + drop < 244) + it.filled_rectangle(x, y + drop, w - 1, std::min(h, 244 - y - drop)); + } else if (p > 4500) { + it.rectangle(x, y, w - 1, h); // back on the shelf + } + } else if (r % 4 == 0) { + it.filled_rectangle(x, y, w - 1, h); + } else { + it.rectangle(x, y, w - 1, h); + } + x += w; + } + } + + // The time in dust: HH:MM as four binary digits, thick bars for 1. + constexpr int DX = 212, DTOP = 40, DBOT = 224; + it.print(DX, 4, f.mono, TextAlign::TOP_LEFT, "THE TIME, IN DUST"); + int digits[4] = {0, 0, 0, 0}; + if (now.is_valid()) { + digits[0] = now.hour / 10; + digits[1] = now.hour % 10; + digits[2] = now.minute / 10; + digits[3] = now.minute % 10; + } + std::string code; + for (int i = 0; i < 16; i++) { + const bool one = digits[i / 4] & (8 >> (i % 4)); + code += one ? '1' : '0'; + if (i % 4 == 3 && i < 15) + code += i == 7 ? " : " : " "; + const int x = DX + i * 10 + (i / 4) * 6; + const int bw = one ? 8 : 2, bx = x + (one ? 0 : 3); + for (int y = DTOP + 60; y < DBOT; y++) + for (int xx = bx; xx < bx + bw; xx++) + if (dither(xx, y, one ? 11 : 9)) + it.draw_pixel_at(xx, y, COLOR_ON); + // Grains drifting down into the bar. + for (int k = 0; k < 4; k++) { + const uint32_t r = hash(i * 7 + k, 3); + const int gy = DTOP + (int) ((t / 9 + r) % 70); + it.draw_pixel_at(bx + (int) (r >> 12) % bw, gy, COLOR_ON); + } + } + it.print(DX, 232, f.mono, TextAlign::TOP_LEFT, code.c_str()); + + // A wristwatch ticking the message out in Morse. + // The message's first word (up to 6 letters, to fit beside the watch). + std::string word = upper(d.message.empty() ? "STAY" : d.message); + word = word.substr(0, std::min(word.find(' '), (size_t) 6)); + if (word.empty()) + word = "STAY"; + // Timeline: dot 200 ms, dash 600, 200 between symbols, 600 between letters, + // 1400 between words, 2000 before it starts over. + struct Mark { + uint32_t at, len; + int letter, symbol; + }; + std::vector marks; + uint32_t at = 0; + for (int l = 0; l < (int) word.size(); l++) { + const char *m = morse(word[l]); + if (m == nullptr) { + at += 1400; + continue; + } + for (int s = 0; m[s]; s++) { + const uint32_t len = m[s] == '.' ? 200 : 600; + marks.push_back({at, len, l, s}); + at += len + 200; + } + at += 400; + } + const uint32_t total = at + 2000, mt = t % std::max(total, 1); + const Mark *cur = nullptr; + int letter = -1; + for (const auto &m : marks) { + if (mt >= m.at) + letter = m.letter; + if (mt >= m.at && mt < m.at + m.len) + cur = &m; + } + constexpr int WX = 456, WY = 84, WR = 40; + it.circle(WX, WY, WR); + it.circle(WX, WY, WR - 3); + for (int k = 0; k < 12; k++) { + int x0, y0, x1, y1; + polar(WX, WY, WR - 6, k * 30, &x0, &y0); + polar(WX, WY, WR - (k % 3 ? 9 : 13), k * 30, &x1, &y1); + it.line(x0, y0, x1, y1); + } + const int sec = now.is_valid() ? now.second : 0; + int hx, hy; + polar(WX, WY, WR - 7, sec * 6 + (cur != nullptr ? 12 : 0), &hx, &hy); // the hand twitches on each mark + it.line(WX, WY, hx, hy); + it.filled_circle(WX, WY, 3); + it.filled_rectangle(WX - 8, WY - WR - 10, 16, 8); + it.filled_rectangle(WX - 8, WY + WR + 2, 16, 8); + + // The word, the letter being sent, and its dots and dashes. + // The word in one go; the letter being sent is inverted, its cell measured + // from the widths of the word's prefixes so it lines up with the glyphs. + const int x0 = WX - text_width(it, f.code, word.c_str()) / 2; + it.print(x0, 158, f.code, TextAlign::CENTER_LEFT, word.c_str()); + if (letter >= 0) { + const int a = text_width(it, f.code, word.substr(0, letter).c_str()); + const int b = text_width(it, f.code, word.substr(0, letter + 1).c_str()); + it.invert_rect(x0 + a - 1, 146, b - a + 2, 24); + } + if (letter >= 0) { + const char *m = morse(word[letter]); + const int n = (int) strlen(m); + int mx = WX - (n * 18) / 2; + for (int s = 0; s < n; s++) { + const bool on = cur != nullptr && cur->letter == letter && cur->symbol == s; + if (m[s] == '.') { + on ? it.filled_circle(mx + 5, 190, 4) : it.circle(mx + 5, 190, 4); + } else { + on ? it.filled_rectangle(mx, 187, 14, 7) : it.rectangle(mx, 187, 14, 7); + } + mx += 18; + } + } + it.print(WX, 210, f.mono, TextAlign::TOP_CENTER, "MORSE, VIA THE"); + it.print(WX, 226, f.mono, TextAlign::TOP_CENTER, "SECOND HAND"); +} + +} // namespace esphome::el512::interstellar diff --git a/components/el512/interstellar.h b/components/el512/interstellar.h new file mode 100644 index 0000000..d66dd25 --- /dev/null +++ b/components/el512/interstellar.h @@ -0,0 +1,33 @@ +// Interstellar-themed animated pages for the ESPHome display: Gargantua with +// a time dilation clock, TARS and its settings, the Endurance spinning while a +// Ranger docks, and the tesseract's bookshelf with the time in dust and a +// message in Morse. Fan art: layouts, drawings and texts are original. +// +// Each function draws a whole page. Call it from a page lambda and list that +// page under animated_pages; the animation clock is the display's page_age_ms(). +#pragma once + +#include "el512_pages.h" +#include "esphome/components/display/display.h" +#include "esphome/core/time.h" + +namespace esphome::el512 { + +class EL512Display; + +namespace interstellar { + +struct Fonts { + display::BaseFont *title{nullptr}; // wide headers (Michroma 12) + display::BaseFont *large{nullptr}; // wide numbers (Michroma 28) + display::BaseFont *mono{nullptr}; // readouts and TARS (Share Tech Mono 14) + display::BaseFont *code{nullptr}; // larger monospace (Share Tech Mono 22) +}; + +void gargantua(EL512Display &it, const Fonts &f, ESPTime now); +void tars(EL512Display &it, const Fonts &f, const HomeData &d); +void endurance(EL512Display &it, const Fonts &f, const HomeData &d, ESPTime now); +void tesseract(EL512Display &it, const Fonts &f, const HomeData &d, ESPTime now); + +} // namespace interstellar +} // namespace esphome::el512 diff --git a/components/el512/konosuba.cpp b/components/el512/konosuba.cpp index c6dc6eb..f845078 100644 --- a/components/el512/konosuba.cpp +++ b/components/el512/konosuba.cpp @@ -99,40 +99,6 @@ void bar(Display &it, int x, int y, int w, int h, float frac) { it.filled_rectangle(x + 2, y + 2, (int) ((w - 4) * std::clamp(frac, 0.0f, 1.0f)), h - 4); } -// Greedy word wrap into at most max_lines lines of `width` pixels. -std::vector wrap(Display &it, display::BaseFont *font, const std::string &text, int width, - size_t max_lines) { - std::vector lines; - std::string line; - size_t i = 0; - while (i <= text.size()) { - size_t j = text.find(' ', i); - if (j == std::string::npos) - j = text.size(); - const std::string word = text.substr(i, j - i); - const std::string joined = line.empty() ? word : line + " " + word; - if (!line.empty() && text_width(it, font, joined.c_str()) > width) { - lines.push_back(line); - line = word; - } else { - line = joined; - } - i = j + 1; - } - if (!line.empty()) - lines.push_back(line); - if (lines.size() > max_lines) { - lines.resize(max_lines); - lines.back() += "..."; - } - for (auto &l : lines) - l = fit(font, l, width); - return lines; -} - -// The first part of `s` that has been typed at `cps` characters per second. -std::string typed(const std::string &s, uint32_t t, int cps) { return s.substr(0, std::min(s.size(), t * cps / 1000)); } - // Dissolve the page in over its first 400 ms. void intro(EL512Display &it, uint32_t t) { if (t < 400) diff --git a/components/el512/steins.cpp b/components/el512/steins.cpp index 3b2a93f..5c06477 100644 --- a/components/el512/steins.cpp +++ b/components/el512/steins.cpp @@ -51,38 +51,6 @@ void ripple(EL512Display &it, uint32_t age, uint32_t dur, float amp) { it.shift_rows(y, 1, (int) std::lround(a * std::sin(y * 0.11f + age * 0.025f))); } -std::vector wrap(Display &it, display::BaseFont *font, const std::string &text, int width, - size_t max_lines) { - std::vector lines; - std::string line; - size_t i = 0; - while (i <= text.size()) { - size_t j = text.find(' ', i); - if (j == std::string::npos) - j = text.size(); - const std::string word = text.substr(i, j - i); - const std::string joined = line.empty() ? word : line + " " + word; - if (!line.empty() && text_width(it, font, joined.c_str()) > width) { - lines.push_back(line); - line = word; - } else { - line = joined; - } - i = j + 1; - } - if (!line.empty()) - lines.push_back(line); - if (lines.size() > max_lines) { - lines.resize(max_lines); - lines.back() += "..."; - } - for (auto &l : lines) - l = fit(font, l, width); - return lines; -} - -std::string typed(const std::string &s, uint32_t t, int cps) { return s.substr(0, std::min(s.size(), t * cps / 1000)); } - std::string watts_text(float w) { return std::isnan(w) ? "--" : watts(w); } std::string temp_text(float v) { return std::isnan(v) ? "--" : str("%.1f C", v); } diff --git a/components/el512/trek.cpp b/components/el512/trek.cpp index 9836a0e..ccf96f0 100644 --- a/components/el512/trek.cpp +++ b/components/el512/trek.cpp @@ -68,38 +68,6 @@ void readout(Display &it, int x, int y, int w, int h, float frac) { pill(it, x, y, std::max(h, (int) (w * std::clamp(frac, 0.0f, 1.0f))), h); } -std::vector wrap(Display &it, display::BaseFont *font, const std::string &text, int width, - size_t max_lines) { - std::vector lines; - std::string line; - size_t i = 0; - while (i <= text.size()) { - size_t j = text.find(' ', i); - if (j == std::string::npos) - j = text.size(); - const std::string word = text.substr(i, j - i); - const std::string joined = line.empty() ? word : line + " " + word; - if (!line.empty() && text_width(it, font, joined.c_str()) > width) { - lines.push_back(line); - line = word; - } else { - line = joined; - } - i = j + 1; - } - if (!line.empty()) - lines.push_back(line); - if (lines.size() > max_lines) { - lines.resize(max_lines); - lines.back() += "..."; - } - for (auto &l : lines) - l = fit(font, l, width); - return lines; -} - -std::string typed(const std::string &s, uint32_t t, int cps) { return s.substr(0, std::min(s.size(), t * cps / 1000)); } - std::string kw(float w) { return std::isnan(w) ? "--" : upper(watts(w)); } std::string deg(float v) { return std::isnan(v) ? "--" : str("%.1f", v); } diff --git a/components/el512/yourname.cpp b/components/el512/yourname.cpp new file mode 100644 index 0000000..e4f4dcd --- /dev/null +++ b/components/el512/yourname.cpp @@ -0,0 +1,239 @@ +#include "yourname.h" + +#include +#include + +#include "el512_display.h" +#include "esphome/core/hal.h" +#include "yourname_sprites.h" + +namespace esphome::el512::yourname { + +using display::COLOR_OFF; +using display::COLOR_ON; +using display::Display; +using display::TextAlign; + +namespace { + +using namespace fx; + +constexpr int W = 512, H = 256; +const uint8_t BAYER[4][4] = {{0, 8, 2, 10}, {12, 4, 14, 6}, {3, 11, 1, 9}, {15, 7, 13, 5}}; +bool dither(int x, int y, int level) { return BAYER[y & 3][x & 3] < level; } + +// "12° · partly cloudy", for the corner of the comet page. +std::string weather_line(const HomeData &d) { + std::string s = std::isnan(d.temp) ? "" : str("%.0f°", d.temp); + if (!d.weather.empty()) + s += (s.empty() ? "" : " · ") + lower(weather_text(d.weather)); + return s; +} + +// Unix time to local "HH:MM", using now's offset from UTC. +std::string local_hhmm(ESPTime now, int64_t when) { + const int64_t local_secs = now.hour * 3600 + now.minute * 60 + now.second; + const int64_t offset = ((local_secs - (int64_t) now.timestamp % 86400) % 86400 + 86400 + 43200) % 86400 - 43200; + const int64_t s = ((when + offset) % 86400 + 86400) % 86400; + return str("%02d:%02d", (int) (s / 3600), (int) (s / 60 % 60)); +} + +} // namespace + +// --- 1. Comet Tiamat over Itomori ------------------------------------------------------ + +void comet(EL512Display &it, const Fonts &f, const HomeData &d, ESPTime now) { + if (f.jp == nullptr) + return; + const uint32_t t = it.page_age_ms(); + constexpr uint32_t CYCLE = 20000; + const float k = (float) (t % CYCLE) / CYCLE; + auto ridge = [](int x) { + return 172 + (int) (12 * std::sin(x * 0.021f) + 8 * std::sin(x * 0.053f + 1.3f) + 4 * std::sin(x * 0.11f + 0.4f)); + }; + + // Stars above the mountains. + for (int i = 0; i < 90; i++) { + const uint32_t r = hash(i, 1200); + const int x = (int) (r % W), y = 40 + (int) ((r >> 9) % 140); + if (y >= ridge(x) - 4 || (t / 200 + (r >> 20)) % 15 == 0) + continue; + it.draw_pixel_at(x, y, COLOR_ON); + } + + // The comet crosses the sky and, past halfway, splits in two. + const float hx = 110 + 360 * k, hy = 70 + 60 * k; + const float dx = 360, dy = 60, len = std::sqrt(dx * dx + dy * dy); + const float ux = dx / len, uy = dy / len; // direction of travel + auto draw_comet = [&](float x, float y, float tail, int glow) { + for (int s = -3; s <= 3; s++) { + const float spread = s * 3.5f; + it.line((int) x, (int) y, (int) (x - ux * tail - uy * spread), (int) (y - uy * tail + ux * spread)); + } + for (int yy = (int) y - glow; yy <= (int) y + glow; yy++) + for (int xx = (int) x - glow; xx <= (int) x + glow; xx++) + if ((xx - x) * (xx - x) + (yy - y) * (yy - y) <= glow * glow && dither(xx, yy, 10)) + it.draw_pixel_at(xx, yy, COLOR_ON); + it.filled_circle((int) x, (int) y, 2); + }; + constexpr float SPLIT = 0.55f; + if (k < SPLIT) { + draw_comet(hx, hy, 130, 6); + } else { + const float s = (k - SPLIT) * 120; // how far the fragments have drifted apart + draw_comet(hx, hy, 120, 5); + draw_comet(hx + uy * s * 0.4f, hy + ux * s, 100, 4); + // Sparks shed where it broke up. + const float px = 110 + 360 * SPLIT, py = 70 + 60 * SPLIT; + for (int i = 0; i < 10; i++) { + const uint32_t r = hash(i, (uint32_t) (t / CYCLE)); + const float a = (r % 360) * 3.14159f / 180, v = 20 + (r >> 9) % 40; + const float age = (k - SPLIT) * 6; + const int sx = (int) (px + std::cos(a) * v * age), sy = (int) (py + std::sin(a) * v * age + 30 * age * age); + if (sy < ridge(sx) - 2) + it.draw_pixel_at(sx, sy, COLOR_ON); + } + } + + // Mountains, misty slopes, and the shrine gate on the left hill. + for (int x = 0; x < W; x++) { + const int y = ridge(x); + it.draw_pixel_at(x, y, COLOR_ON); + if (x > 0) + it.line(x - 1, ridge(x - 1), x, y); + for (int yy = y + 1; yy < H; yy++) + it.draw_pixel_at(x, yy, dither(x, yy, 1) ? COLOR_ON : COLOR_OFF); + } + sprite(it, TORII, 60, ridge(70) - TORII.h - 1); + + // The round lake, with the comet's reflection and the town's lights. + constexpr int LX = 300, LY = 222, RX = 110, RY = 18; + for (int y = LY - RY; y <= LY + RY; y++) + for (int x = LX - RX; x <= LX + RX; x++) { + const float e = (float) ((x - LX) * (x - LX)) / (RX * RX) + (float) ((y - LY) * (y - LY)) / (RY * RY); + if (e <= 1.0f) + it.draw_pixel_at(x, y, COLOR_OFF); + } + for (int a = 0; a < 360; a += 3) { + const float r = a * 3.14159f / 180; + it.draw_pixel_at(LX + (int) (RX * std::cos(r)), LY + (int) (RY * std::sin(r)), COLOR_ON); + } + if (hx > LX - RX + 10 && hx < LX + RX - 10) + for (int y = LY - RY + 4; y < LY + RY - 2; y += 3) + it.horizontal_line((int) hx - 3 + (int) ((t / 120 + y) % 5), y, 4 + (y % 3)); + for (int i = 0; i < 14; i++) { + const float r = (200 + i * 10) * 3.14159f / 180; + if ((t / 300 + hash(i, 9)) % 7 != 0) + it.filled_rectangle(LX + (int) ((RX + 6) * std::cos(r)), LY + (int) ((RY + 4) * std::sin(r)) - 1, 2, 2); + } + + // Title, clock and weather. + it.print(10, 6, f.jp, TextAlign::TOP_LEFT, "君の名は。"); + if (now.is_valid()) + it.strftime(504, 0, f.clock, TextAlign::TOP_RIGHT, "%H:%M", now); + it.print(504, 50, f.hand, TextAlign::TOP_RIGHT, weather_line(d).c_str()); +} + +// --- 2. Kataware-doki: the twilight hour ------------------------------------------------- + +void twilight(EL512Display &it, const Fonts &f, const HomeData &d, ESPTime now) { + if (f.jp == nullptr) + return; + const uint32_t t = it.page_age_ms(); + const int64_t nowt = now.is_valid() ? (int64_t) now.timestamp : 0; + const float elev = d.sun_elevation; + const float to_sunset = d.sunset > 0 && nowt > 0 ? (d.sunset - nowt) / 60.0f : NAN; // minutes + const float to_sunrise = d.sunrise > 0 && nowt > 0 ? (d.sunrise - nowt) / 60.0f : NAN; + // Golden hour before sunset through civil dusk after it. + const bool dusk = (!std::isnan(elev) && elev > -6 && elev < 4 && !std::isnan(to_sunrise) && + !std::isnan(to_sunset) && to_sunset < to_sunrise) || + (!std::isnan(to_sunset) && to_sunset <= 45); + const bool night = !std::isnan(elev) && elev <= -6; + + // The crater rim, highest in the middle where they stand (once per frame). + int rims[W]; + for (int x = 0; x < W; x++) { + const float u = (x - 256) / 120.0f; + rims[x] = 190 - (int) (12 * std::exp(-u * u)); + } + auto rim = [&rims](int x) { return rims[std::clamp(x, 0, W - 1)]; }; + + // Sky: a glow that thickens towards the horizon at dusk; stars at night. + if (dusk) { + for (int y = 90; y < 190; y++) + for (int x = 0; x < W; x++) + if (y < rim(x) && dither(x, y, (y - 90) / 22)) + it.draw_pixel_at(x, y, COLOR_ON); + } + if (night || dusk) { + for (int i = 0; i < (night ? 80 : 30); i++) { + const uint32_t r = hash(i, 777); + const int x = (int) (r % W), y = 60 + (int) ((r >> 9) % (night ? 120 : 50)); + if ((t / 250 + (r >> 20)) % 13 != 0) + it.draw_pixel_at(x, y, COLOR_ON); + } + } + + // The sun, by its elevation (or by the time left), cut off by the rim. + if (!night) { + float e = elev; + if (std::isnan(e)) + e = std::isnan(to_sunset) ? 20 : std::min(40.0f, to_sunset / 12); + const int sx = 256, sy = rim(256) - (int) (e * 6); + for (int y = sy - 24; y <= sy + 24; y++) + for (int x = sx - 24; x <= sx + 24; x++) + if ((x - sx) * (x - sx) + (y - sy) * (y - sy) <= 24 * 24 && y < rim(x) - 1 && dither(x, y, dusk ? 9 : 13)) + it.draw_pixel_at(x, y, COLOR_ON); + } else { + it.filled_circle(430, 70, 14); + it.filled_circle(424, 66, 13, COLOR_OFF); + } + + // The rim and the dark ground below it. + for (int x = 0; x < W; x++) { + const int y = rim(x); + for (int yy = y; yy < H; yy++) + it.draw_pixel_at(x, yy, dither(x, yy, 2) ? COLOR_ON : COLOR_OFF); + it.draw_pixel_at(x, y, COLOR_ON); + } + + if (dusk) { + // Two people on the rim, for as long as the twilight lasts. + // Dark silhouettes against the glow, with a thin lit outline. + const int ty = rim(232) - TAKI.h * 2, my = rim(280) - MITSUHA.h * 2; + for (int o = 0; o < 4; o++) { + const int ox = o == 0 ? -1 : o == 1 ? 1 : 0, oy = o == 2 ? -1 : o == 3 ? 1 : 0; + sprite(it, TAKI, 214 + ox, ty + oy, 2); + sprite(it, MITSUHA, 266 + ox, my + oy, 2); + } + sprite(it, TAKI, 214, ty, 2, -1, COLOR_OFF); + sprite(it, MITSUHA, 266, my, 2, -1, COLOR_OFF); + const int tw = (int) ((t / 160) % 8); + if (tw < 4) + for (int r = 0; r <= tw; r++) { + it.draw_pixel_at(256 - r, ty + 8, COLOR_ON); + it.draw_pixel_at(256 + r, ty + 8, COLOR_ON); + it.draw_pixel_at(256, ty + 8 - r, COLOR_ON); + it.draw_pixel_at(256, ty + 8 + r, COLOR_ON); + } + it.print(10, 6, f.jp, TextAlign::TOP_LEFT, "かたわれ時"); + it.print(12, 44, f.hand, TextAlign::TOP_LEFT, "kataware-doki, when the worlds blur"); + } else { + it.print(10, 6, f.hand, TextAlign::TOP_LEFT, night ? "the sun is down" : "waiting for twilight"); + } + + // Countdown to sunset (or, at night, to sunrise), and today's times. + const bool to_rise = night || (!std::isnan(to_sunset) && !std::isnan(to_sunrise) && to_sunrise < to_sunset); + const float mins = to_rise ? to_sunrise : to_sunset; + if (!std::isnan(mins) && mins >= 0) { + it.print(504, 4, f.hand, TextAlign::TOP_RIGHT, to_rise ? "sunrise in" : "sunset in"); + it.printf(504, 26, f.clock, TextAlign::TOP_RIGHT, "%d:%02d", (int) (mins / 60), (int) mins % 60); + } + if (now.is_valid() && d.sunset > 0 && d.sunrise > 0) + it.printf(504, 234, f.mono, TextAlign::TOP_RIGHT, "SUNSET %s SUNRISE %s", local_hhmm(now, d.sunset).c_str(), + local_hhmm(now, d.sunrise).c_str()); + else if (d.sunset == 0) + it.print(504, 234, f.mono, TextAlign::TOP_RIGHT, "NO SUN DATA (SUN.SUN)"); +} + +} // namespace esphome::el512::yourname diff --git a/components/el512/yourname.h b/components/el512/yourname.h new file mode 100644 index 0000000..63b302f --- /dev/null +++ b/components/el512/yourname.h @@ -0,0 +1,32 @@ +// Your Name (Kimi no Na wa)-themed animated pages for the ESPHome display: the +// comet splitting over Itomori's lake, and kataware-doki, the twilight hour, +// counting down to sunset from Home Assistant's sun. Fan art: the pixel art +// (yourname_sprites.h, from tools/yourname_sprites.py), drawings and texts +// are original. +// +// Each function draws a whole page. Call it from a page lambda and list that +// page under animated_pages; the animation clock is the display's page_age_ms(). +#pragma once + +#include "el512_pages.h" +#include "esphome/components/display/display.h" +#include "esphome/core/time.h" + +namespace esphome::el512 { + +class EL512Display; + +namespace yourname { + +struct Fonts { + display::BaseFont *jp{nullptr}; // brush lettering (Yuji Syuku) + display::BaseFont *hand{nullptr}; // handwriting (Caveat) + display::BaseFont *clock{nullptr}; // clock digits (Cormorant Garamond) + display::BaseFont *mono{nullptr}; // small print (Share Tech Mono 14) +}; + +void comet(EL512Display &it, const Fonts &f, const HomeData &d, ESPTime now); +void twilight(EL512Display &it, const Fonts &f, const HomeData &d, ESPTime now); + +} // namespace yourname +} // namespace esphome::el512 diff --git a/components/el512/yourname_sprites.h b/components/el512/yourname_sprites.h new file mode 100644 index 0000000..1c39e2e --- /dev/null +++ b/components/el512/yourname_sprites.h @@ -0,0 +1,65 @@ +// Pixel art for the Your Name pages, generated by tools/yourname_sprites.py; +// edit the art there. Original 1-bit fan art, '#' = lit pixel. +#pragma once + +#include "el512_pages.h" // Sprite + +namespace esphome::el512::yourname { + +static const char *const TORII_ROWS[] = { + "####################", + ".##################.", + "...##..........##...", + "..################..", + "...##..........##...", + "...##..........##...", + "...##..........##...", + "...##..........##...", + "...##..........##...", + "..####........####..", +}; +static constexpr Sprite TORII{20, 10, TORII_ROWS}; + +static const char *const TAKI_ROWS[] = { + "...####...", + "..######..", + "..######..", + "..#####...", + "...###....", + "..#####...", + ".#######..", + ".#######..", + ".##.###...", + ".##.###...", + "....###...", + "...####...", + "...#..#...", + "...#..#...", + "...#..#...", + "...#..#...", + "..##..##..", +}; +static constexpr Sprite TAKI{10, 17, TAKI_ROWS}; + +static const char *const MITSUHA_ROWS[] = { + "...####...", + "..######..", + "..######..", + "...#####..", + "....###.##", + "...#####.#", + "..#######.", + "..#######.", + "...###.##.", + "..#####...", + ".#######..", + ".#######..", + "...#..#...", + "...#..#...", + "...#..#...", + "...#..#...", + "..##..##..", +}; +static constexpr Sprite MITSUHA{10, 17, MITSUHA_ROWS}; + +} // namespace esphome::el512::yourname diff --git a/docs/images/pages/is_endurance.png b/docs/images/pages/is_endurance.png new file mode 100644 index 0000000000000000000000000000000000000000..0eefb4e566e65d1c56c8bdb678dec1cfa85c0f09 GIT binary patch literal 6887 zcmb_hcUV)|w%;KLf*LFr9UYnltjwUuC_Q#j1V$+)ATXk!LS zrp-@?4y&Lzx$hC~`(&Q6WgPhh5Q_7%UhlnYaysh>2sIWu0-LGniIKWwV=`ND;W8pO zQ4w8@0H8LmXC1Ws?LD)b>{CK)`BcljFd%)b@o0gLIlDC^C+;W$cr&VqO0 z>oyxGCM->JcfeCdb8jHWsU>62gK|y=MK7$gk-yoQ;snWTk_g$trP#gsONJ$-WxWAK zU?WS%#xweO50n4!T?);ZzXkzFia}R3xJMXC z%_fk$L!FRs3@U7s703B$+vg^Do8u|Wwhe^0M&9ZCSXs|!?YVOX;L0jSel6&z8)O!>+b5?u)Z)-mbEyRqqXXOJ|M_RZ*{qo`NP!|l}oD+=5=gjDBV z(av=eO)z{9QtGT9*cP0BBj^aaWNDdjSy&%ix+I3avrKaScC>_K>!d;r$uO|^kEfo` z{=|9&P%XDjk3C!ZSThC3v8UxSrA7ud4WdqMuya2wX;`|H;%P7LeR0QG9K)P!m`r+< zgK0Sui*MUWIlVT_fk$9oq@BnohUE7wk=PGbEDIZm`}U3URJSXVCHa{kr3k|x zaa!9}WjePO-St5!GeQpAyAP_mIAE^PKG|7MbxNU42EyRnn6}hY$P&* z3knBZI>Fkz%3djOD>IXfb~xya`AB)~nj{+)b_@V2rg+FV1zE6+Gt3Cx*#O43CU(b^=bTCv~h|CMS`TiUCWPRaqGRjq{YQhFKH^dd!N%poL`7di!ql?m)e*b)T~G9 zz-F%vXgPzi@nfeU!-_B4wmuOn&N(ccG=ptmR4}>Dz2(D5N020qiKYL-7&#v;Xeq;o z{3c-I8X(sQsJu6ahOlEOx2mES)Fi?3bB3#z-tv%!7Ix7Y#G3bd>F{o7iGlz9qNT1? z`G561v3ZN*q#-RR7jOy*fvH!&=)rYZIfKUqZXPE)Wgg&Y`b7DyVt8N0xz?E!Pmi*frVRgxzxM8Dr0BucmX>Vhj!S%^g^! zX8Gb|Uwoh$shi&X0kiy|iPFMi#Dmlh$Cwk+l=okpOL3d(WUkfR*m71Ng*2fw*$V?; zl94}i_T5?qCiBW~uTd~G27q(@z1M@0wC`!vX+4El0nY%;3233uAWl~IG_I`wj7=7% zX}MFT=kP}ABK@+?Wvj zX~HTDL&9A5ViS5#O!;ygr+0g>B1s=uLct!Q-3aiZiApjER)9G~FU-U4YttR+tC^miYdk?RFOz((gXjL#~}ZH<)g&xYzFTA*pG{TyUX0 z9@u|6$@xkjC$U4gegKEim$QVjo9c*kH7GLIl%bFoO8Qo5?q99qNYZZi5YA(p#csQj z_0JBEE_hdCio1D42+}Bj2p?w^qsrc||3&2#t zS~GVsqq8bmP@;Q*khwYbz$XA&)+@L0M67uxBWU?CJoh(93uH(jfMI~X-9yllMDZCh z3IcBYBS@C&*<}-@8aYQo$NahzgRHr-q~HeGA8eadUZO=& z>ZsJ)drTA=ToMj}H5=BUKi-PzG8=_*FA0K(h+K8nTbtCH5ZosB`}=zr(7O?~5lKhV zm4a*xq4vy2Tfu!%4-WYbSa+=lMR`jI+m6#8lCjEEpQ>6 zu^yneC9zA9wb{xuL*$iMDruh#IXdrv7O4b|4B(Js2%FkQF62=lUCs&abzj&N0r~!S zu#?uFo-Q6>mG2mrwa5|vKda~cVP~}&EieWHDgs>!dKJGyo(Xe^4p>N)45R;(m0L?y8&*eYW9HaW=-9vuf8rh|V<1jNs6aFGzm0gU|aZ6N3P^RqW8?&_h1| zY?M|!mWd2wQ`#oZ()<0qMZ8Wq!-ygjH4`LfK$NKxY6HNhv3VxTkHBv~^w{bW@IS9U zxAsLbGyl&}e($uMOs`f3V+thq1KHE^iJ6SuBKG&-t&7r(qezQSSoEeRoT4n`xr*ZO z221O;pvzS`)`%cE4Is%2rOdX z5lm!u-f_J6L-9i1HvD>Dm~g>{5Nd0JI1iD+Uf@-H^odbuDPlAV6|InXp}l3si5%}#Z@)^0M#l>XtHp6Kky7$$Cyoc>r6 z89>6olKLNP%^}cUeRi4{9Y#-oOSk_u-Xp#^TfZaBp{cW`dghCdyIaVP&o=yQ2((_->@_0)Il63$w)*$*EwM4XTUys4Kd= z>Q6+a%A1bQOLQto-Uboc!?IBeE-CXwB@iUKJ^VO~$gcn$RBHy4Q)%twH$~34~RewS`*{{C%-5y*`P_GWG zxy%GVioEZxS1z}yE^<-WdL=WL=(=4NcM$s{BU|Rb8v%1Qj(<3TEcH6A&DBt6Uplpy z2aXH)DLxvYcaze8PwM9u`glv}kJsFWvR*^ra~KXg%QDt#yGi--oQGV*ZM3rUs^7eW zFW)S=%B}gM#P%0k9bl~#|AhhR>e?1Tw*X{SvUPyu7zF&2My=3<0$`rar-Y?xaf6|Fr!-^zI0alN)~!)I`$2@I^|Q2pSLdB;%G{mVIDc1reZkpt9nx1T zIpPRVpb}#pc-Pv@&>hSc{m3{Djd?)PNL6W!YNGdDJTKr^;9cJ~pAoLNuSbA-mEH%y z&lNV4t#cy7N&OWCZN{`r3w60>s;WHRWw5rXB1VRU3p@hP7e6tfA-JxBlQ>#bl7N3mG0S9E3B({UpK zeA9_7bDU~}br9e~>_>B;yy$gL2xzdh_nxvls=aspCe@f$F=h*6? zoRuEZ{3Pb}!9;vHm%<1%|KX>mYFomaDc`t|!?ccN{+>$gtwHp9;y4i!niG|gpc^C6 z49W^%mQ;wV`zkTJn&NaiJZy6at$uj6-{B-gG&x|sJk^N%ido4zx* zL}WGV?xz-X^Ez``W|x(ublPYFNjFr9;jPb>6(M+wl_Jtt9hRw&mt5+cCT%Vy3~BCq?n; z_nEwEW`9r;KfNzvG~$4hVX|Sm5$`-fKQG?4_Evb_m@30n#y>=x6QI4X`Rk;WQT?w) z9pSO8Li=O)U=VfyJbrXcvh)&(!Cn! zA1``8<CS`~BerZzrA0QLO`x{PZh5mSE&*8j|ntmLru)K}K)z-5}E zlo}h#eddjQlz~!b<4eJr_eZ-c@Ce{WRtont?x|z9X4G$a4s{dLUWR1rBrTDtunOU( zawQ1BP`36ffAl9GC@s`iC85?}VQ8qCh2Nt0@wH)s$g=VSQ3SK9i{zCOQD+^0CQj7o zJKD(~6j=#)BSoPl_-pAnKVD~vcrtTAOHU4cnJh$g^?MQpo?E*1&MZN`=^~0fw@Z9~ zA@Hgm*_-QsmWs}EIx1Mhw93Z)Vl%x?K20Vg*k1IFk2OZm4jg zRqRxts?@JKgJG&80DSHS97e+I7iC$mupd1VMT4vx3C&WakI<}JpTKy#F}@;4 ztOebgKp`0VETRpyP{}$wuIhju|K&r7vn_ah*B+X>Xm-R@3bp@>ZzliEx<=#BwKtdS vhq4D@j>?ZL|6f=9|EDYY|8&`JY%O0gRg1`yp@`X#4uI1>m%X&z{+Ip>Ypn2^ literal 0 HcmV?d00001 diff --git a/docs/images/pages/is_gargantua.png b/docs/images/pages/is_gargantua.png new file mode 100644 index 0000000000000000000000000000000000000000..85bdaec74a2fdb78e1ca1c41ed724e27aa165bc2 GIT binary patch literal 7563 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 M*|oWFQ{dVE0t?j3KmY&$ literal 0 HcmV?d00001 diff --git a/docs/images/pages/is_tars.png b/docs/images/pages/is_tars.png new file mode 100644 index 0000000000000000000000000000000000000000..137ec12481c6d7df8bd234dd867414162eea950f GIT binary patch literal 6290 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 byT}(^upQG%3$03nRsr7~{@W|JhMoH-;Eq+Z literal 0 HcmV?d00001 diff --git a/docs/images/pages/is_tesseract.png b/docs/images/pages/is_tesseract.png new file mode 100644 index 0000000000000000000000000000000000000000..2fdbd6dbca21d8a5737f1d1f6afcba657786dd2b GIT binary patch literal 8443 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 literal 0 HcmV?d00001 diff --git a/docs/images/pages/yn_twilight.png b/docs/images/pages/yn_twilight.png new file mode 100644 index 0000000000000000000000000000000000000000..c8c20912d8cb6ce87ceb4a157e9d207925274a71 GIT binary patch literal 7439 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 FzX7fKH2eSn literal 0 HcmV?d00001 diff --git a/esphome/el512-panel.yaml b/esphome/el512-panel.yaml index 2bab2e8..fadf556 100644 --- a/esphome/el512-panel.yaml +++ b/esphome/el512-panel.yaml @@ -22,6 +22,8 @@ esphome: id(f_ks_jp_big), id(icons_l)}; id(sg_fonts) = {id(f_sg_nixie), id(f_sg_title), id(f_cp_mono), id(f_cp_code), id(f_ks_pixel), id(f_sg_jp)}; id(st_fonts) = {id(f_st_small), id(f_st_medium), id(f_st_large), id(f_ks_text), id(f_ks_pixel)}; + id(is_fonts) = {id(f_is_title), id(f_is_large), id(f_cp_mono), id(f_cp_code)}; + id(yn_fonts) = {id(f_yn_jp), id(f_yn_hand), id(f_yn_clock), id(f_cp_mono)}; esp32: board: wemos_d1_mini32 @@ -67,7 +69,7 @@ globals: - id: plex_updated type: uint32_t initial_value: "0" - # Themed pages (Cyberpunk, KonoSuba, Steins;Gate, Star Trek): their fonts and the home data they show. + # Themed pages (Cyberpunk, KonoSuba, Steins;Gate, Star Trek, Interstellar, Your Name): their fonts and the home data they show. - id: cp_fonts type: el512::cyber::Fonts - id: ks_fonts @@ -76,6 +78,10 @@ globals: type: el512::steins::Fonts - id: st_fonts type: el512::trek::Fonts + - id: is_fonts + type: el512::interstellar::Fonts + - id: yn_fonts + type: el512::yourname::Fonts - id: cp_home type: el512::HomeData @@ -174,7 +180,7 @@ font: - file: "gfonts://Share Tech Mono" id: f_cp_code size: 22 - glyphs: " 0123456789ABCDEF.%" + glyphsets: [GF_Latin_Core] # KonoSuba pages - file: "gfonts://Cinzel@700" id: f_ks_title @@ -231,6 +237,28 @@ font: id: f_st_large size: 60 glyphs: " 0123456789:ADELRT" + # Interstellar pages (also use f_cp_mono and f_cp_code) + - file: "gfonts://Michroma" + id: f_is_title + size: 12 + glyphsets: [GF_Latin_Core] + - file: "gfonts://Michroma" + id: f_is_large + size: 28 + glyphs: " 0123456789:.ARST" + # Your Name pages (also use f_cp_mono) + - file: "gfonts://Yuji Syuku" + id: f_yn_jp + size: 26 + glyphs: "君の名は。かたわれ時" + - file: "gfonts://Caveat@700" + id: f_yn_hand + size: 22 + glyphsets: [GF_Latin_Core] + - file: "gfonts://Cormorant Garamond@600" + id: f_yn_clock + size: 48 + glyphs: "0123456789:" # --- Data from Home Assistant ------------------------------------------------- text_sensor: @@ -266,6 +294,14 @@ text_sensor: - platform: homeassistant id: printer_state entity_id: ${printer} + - platform: homeassistant + id: sun_setting + entity_id: sun.sun + attribute: next_setting + - platform: homeassistant + id: sun_rising + entity_id: sun.sun + attribute: next_rising - platform: homeassistant id: plex_update entity_id: update.plex_media_server_mangavalkserver @@ -294,6 +330,10 @@ sensor: - platform: homeassistant id: tautulli_bandwidth entity_id: sensor.tautulli_total_bandwidth + - platform: homeassistant + id: sun_elev + entity_id: sun.sun + attribute: elevation # Solar (Enphase Envoy) and grid - platform: homeassistant id: solar_power @@ -439,6 +479,12 @@ select: - Duty Roster - Warp Core - Captain's Log + - Gargantua + - TARS + - Endurance + - Tesseract + - Comet Tiamat + - Kataware-doki initial_option: Clock set_action: - lambda: |- @@ -467,6 +513,12 @@ select: else if (x == "Duty Roster") p = id(page_st_roster); else if (x == "Warp Core") p = id(page_st_warp); else if (x == "Captain's Log") p = id(page_st_log); + else if (x == "Gargantua") p = id(page_is_gargantua); + else if (x == "TARS") p = id(page_is_tars); + else if (x == "Endurance") p = id(page_is_endurance); + else if (x == "Tesseract") p = id(page_is_tesseract); + else if (x == "Comet Tiamat") p = id(page_yn_comet); + else if (x == "Kataware-doki") p = id(page_yn_twilight); id(panel).show_page(p); id(panel).update(); @@ -506,7 +558,9 @@ interval: id(page_plex), id(page_solar), id(page_cyber_hud), id(page_cyber_breach), id(page_cyber_scan), id(page_cyber_split), id(page_ks_explosion), id(page_ks_guild), id(page_ks_card), id(page_ks_battle), id(page_sg_meter), id(page_sg_dmail), id(page_sg_lab), id(page_sg_world), - id(page_st_lcars), id(page_st_roster), id(page_st_warp), id(page_st_log), id(page_still_alive)}; + id(page_st_lcars), id(page_st_roster), id(page_st_warp), id(page_st_log), + id(page_is_gargantua), id(page_is_tars), id(page_is_endurance), id(page_is_tesseract), + id(page_yn_comet), id(page_yn_twilight), id(page_still_alive)}; for (size_t i = order.size() - 1; i > 0; i--) std::swap(order[i], order[random_uint32() % (i + 1)]); pos = order.size() - 1; // the next step starts at the front @@ -535,6 +589,9 @@ interval: d.agenda = id(agenda_events).state; d.message = id(panel_message).state; d.printer = id(printer_state).state; + d.sunset = el512::fx::parse_iso_utc(id(sun_setting).state); + d.sunrise = el512::fx::parse_iso_utc(id(sun_rising).state); + d.sun_elevation = id(sun_elev).state; d.house = id(house_power).state; d.grid = id(grid_import).state - id(grid_export).state; d.rooms[0] = {"Woonkamer", id(t_woonkamer).state}; @@ -555,7 +612,9 @@ display: animated_pages: [page_cyber_hud, page_cyber_breach, page_cyber_scan, page_cyber_split, page_ks_explosion, page_ks_guild, page_ks_card, page_ks_battle, page_sg_meter, page_sg_dmail, page_sg_lab, page_sg_world, - page_st_lcars, page_st_roster, page_st_warp, page_st_log] + page_st_lcars, page_st_roster, page_st_warp, page_st_log, + page_is_gargantua, page_is_tars, page_is_endurance, page_is_tesseract, + page_yn_comet, page_yn_twilight] animation_interval: 66ms on_page_change: - then: @@ -584,6 +643,12 @@ display: else if (to == id(page_st_roster)) name = "Duty Roster"; else if (to == id(page_st_warp)) name = "Warp Core"; else if (to == id(page_st_log)) name = "Captain's Log"; + else if (to == id(page_is_gargantua)) name = "Gargantua"; + else if (to == id(page_is_tars)) name = "TARS"; + else if (to == id(page_is_endurance)) name = "Endurance"; + else if (to == id(page_is_tesseract)) name = "Tesseract"; + else if (to == id(page_yn_comet)) name = "Comet Tiamat"; + else if (to == id(page_yn_twilight)) name = "Kataware-doki"; if (id(page_select).current_option() != name) id(page_select).publish_state(name); pages: # 1. Clock, date and current weather @@ -853,3 +918,25 @@ display: - id: page_st_log lambda: |- el512::trek::captains_log(*id(panel), id(st_fonts), id(cp_home), id(ha_time).now()); + + # 25-28. Interstellar pages (components/el512/interstellar.cpp) + - id: page_is_gargantua + lambda: |- + el512::interstellar::gargantua(*id(panel), id(is_fonts), id(ha_time).now()); + - id: page_is_tars + lambda: |- + el512::interstellar::tars(*id(panel), id(is_fonts), id(cp_home)); + - id: page_is_endurance + lambda: |- + el512::interstellar::endurance(*id(panel), id(is_fonts), id(cp_home), id(ha_time).now()); + - id: page_is_tesseract + lambda: |- + el512::interstellar::tesseract(*id(panel), id(is_fonts), id(cp_home), id(ha_time).now()); + + # 29-30. Your Name pages (components/el512/yourname.cpp) + - id: page_yn_comet + lambda: |- + el512::yourname::comet(*id(panel), id(yn_fonts), id(cp_home), id(ha_time).now()); + - id: page_yn_twilight + lambda: |- + el512::yourname::twilight(*id(panel), id(yn_fonts), id(cp_home), id(ha_time).now()); diff --git a/tools/render/el512_display.h b/tools/render/el512_display.h index bef5b2f..60f8da8 100644 --- a/tools/render/el512_display.h +++ b/tools/render/el512_display.h @@ -12,9 +12,11 @@ #include "el512_helpers.h" #include "esphome/components/display/display.h" #include "esphome/core/hal.h" +#include "interstellar.h" #include "konosuba.h" #include "steins.h" #include "trek.h" +#include "yourname.h" namespace esphome::el512 { diff --git a/tools/render/render_main.cpp b/tools/render/render_main.cpp index 5076543..cee4788 100644 --- a/tools/render/render_main.cpp +++ b/tools/render/render_main.cpp @@ -50,6 +50,10 @@ static HomeData sample(const std::string &scenario) { d.rooms[1] = {"Voorkamer", 19.8f}; d.rooms[2] = {"Patrick", 20.5f}; d.rooms[3] = {"Badkamer", 22.1f}; + // Sunset 40 minutes after the sample time: twilight. + d.sunset = 1792420645 + 40 * 60; + d.sunrise = 1792420645 + 17 * 3600; + d.sun_elevation = 3.5f; if (scenario == "busy") { d.weather = "lightning-rainy"; d.temp = -12.6f; @@ -65,6 +69,8 @@ static HomeData sample(const std::string &scenario) { "Thu 22|19:00|Dinner"; d.rooms[0] = {"Vloerverwarming", 23.9f}; d.rooms[1] = {"Stans & Paul", 18.2f}; + d.sunset = 1792420645 + 3 * 3600 + 25 * 60; + d.sun_elevation = 24.0f; } // Today's solar curve up to now (14:37), for the world line chart. for (int s = 7 * 4; s <= 14 * 4 + 2; s++) { @@ -85,6 +91,8 @@ static std::vector pages() { const konosuba::Fonts ks{f_ks_title, f_ks_big, f_ks_text, f_ks_pixel, f_ks_pixel_l, f_ks_jp, f_ks_jp_big, icons_l}; const steins::Fonts sg{f_sg_nixie, f_sg_title, f_cp_mono, f_cp_code, f_ks_pixel, f_sg_jp}; const trek::Fonts st{f_st_small, f_st_medium, f_st_large, f_ks_text, f_ks_pixel}; + const interstellar::Fonts is{f_is_title, f_is_large, f_cp_mono, f_cp_code}; + const yourname::Fonts yn{f_yn_jp, f_yn_hand, f_yn_clock, f_cp_mono}; return { {"cyber_hud", {2000}, [=](auto &it, auto &d, auto now) { cyber::netrunner_hud(it, cp, d, now); }}, {"cyber_breach", {4500, 9000}, [=](auto &it, auto &, auto) { cyber::breach_protocol(it, cp); }}, @@ -103,6 +111,12 @@ static std::vector pages() { {"st_roster", {1500}, [=](auto &it, auto &d, auto now) { trek::duty_roster(it, st, d, now); }}, {"st_warp", {2000}, [=](auto &it, auto &d, auto) { trek::warp_core(it, st, d); }}, {"st_log", {14000, 6000}, [=](auto &it, auto &d, auto now) { trek::captains_log(it, st, d, now); }}, + {"is_gargantua", {3000}, [=](auto &it, auto &, auto now) { interstellar::gargantua(it, is, now); }}, + {"is_tars", {3500, 38500}, [=](auto &it, auto &d, auto) { interstellar::tars(it, is, d); }}, + {"is_endurance", {8000, 2000, 12000}, [=](auto &it, auto &d, auto now) { interstellar::endurance(it, is, d, now); }}, + {"is_tesseract", {1700, 900}, [=](auto &it, auto &d, auto now) { interstellar::tesseract(it, is, d, now); }}, + {"yn_comet", {14000, 6000}, [=](auto &it, auto &d, auto now) { yourname::comet(it, yn, d, now); }}, + {"yn_twilight", {3000}, [=](auto &it, auto &d, auto now) { yourname::twilight(it, yn, d, now); }}, }; } diff --git a/tools/render_pages.py b/tools/render_pages.py index 4e344dc..b90407a 100644 --- a/tools/render_pages.py +++ b/tools/render_pages.py @@ -2,7 +2,8 @@ """Render the themed ESPHome pages to PNG on this PC, for checking layouts without flashing and for example images. -Compiles the page code (components/el512/cyberpunk, konosuba, steins, trek) +Compiles the page code (components/el512/cyberpunk, konosuba, steins, trek, +interstellar, yourname) natively with g++ against ESPHome's own display and font code (from the installed esphome package) and the exact font bitmaps of the last `esphome compile` (its generated main.cpp), then draws each page at given @@ -29,8 +30,9 @@ from pathlib import Path ROOT = Path(__file__).resolve().parent.parent WORK = ROOT / ".render" -PAGE_SOURCES = ["cyberpunk", "konosuba", "steins", "trek"] -PAGE_HEADERS = ["el512_pages.h", "el512_helpers.h", "konosuba_sprites.h", "steins_sprites.h", "trek_sprites.h"] +PAGE_SOURCES = ["cyberpunk", "konosuba", "steins", "trek", "interstellar", "yourname"] +PAGE_HEADERS = ["el512_pages.h", "el512_helpers.h", "konosuba_sprites.h", "steins_sprites.h", "trek_sprites.h", + "yourname_sprites.h"] ESPHOME_PARTS = ["core", "components/display", "components/font", "components/host", "components/time", "components/lvgl", "components/graph"] ESPHOME_SOURCES = ["components/display/display.cpp", "components/display/rect.cpp", diff --git a/tools/yourname_sprites.py b/tools/yourname_sprites.py new file mode 100644 index 0000000..4453214 --- /dev/null +++ b/tools/yourname_sprites.py @@ -0,0 +1,73 @@ +#!/usr/bin/env python3 +"""Pixel art for the Your Name pages (components/el512/yourname.cpp). + +Original 1-bit fan art: '#' is a lit pixel. Writes +components/el512/yourname_sprites.h and, with --preview, a magnified PNG. + + python tools/yourname_sprites.py + python tools/yourname_sprites.py --preview sprites.png +""" + +from konosuba_sprites import main, padded + +SPRITES = { + # Shrine gate on the hill above the lake. + "TORII": [ + "####################", + ".##################.", + "...##..........##...", + "..################..", + "...##..........##...", + "...##..........##...", + "...##..........##...", + "...##..........##...", + "...##..........##...", + "..####........####..", + ], + # Two silhouettes on the crater rim at twilight: him facing right... + "TAKI": [ + "...####...", + "..######..", + "..######..", + "..#####...", + "...###....", + "..#####...", + ".#######..", + ".#######..", + ".##.###...", + ".##.###...", + "....###...", + "...####...", + "...#..#...", + "...#..#...", + "...#..#...", + "...#..#...", + "..##..##..", + ], + # ... and her facing left, the cord tied in her hair, a skirt. + "MITSUHA": [ + "...####...", + "..######..", + "..######..", + "...#####..", + "....###.##", + "...#####.#", + "..#######.", + "..#######.", + "...###.##.", + "..#####...", + ".#######..", + ".#######..", + "...#..#...", + "...#..#...", + "...#..#...", + "...#..#...", + "..##..##..", + ], +} + +for _name in SPRITES: + SPRITES[_name] = padded(SPRITES[_name]) + +if __name__ == "__main__": + main(SPRITES, "Your Name", "yourname_sprites.py", "yourname", __doc__)