Public Access
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 <noreply@anthropic.com>
This commit is contained in:
1 parent
a4ce97d744
commit
f46f7ab6c5
22 files changed
+1159
-110
No files matched your search
@@ -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
|
||||
|
||||
| | |
|
||||
|---|---|
|
||||
|  |  |
|
||||
|  |  |
|
||||
|
||||
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
|
||||
|
||||
| | |
|
||||
|---|---|
|
||||
|  |  |
|
||||
|
||||
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
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -9,7 +9,9 @@
|
||||
#include <cstdint>
|
||||
#include <cstdio>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#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<std::string> wrap(display::Display &it, display::BaseFont *font, const std::string &text, int width,
|
||||
size_t max_lines) {
|
||||
std::vector<std::string> 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<size_t>(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) {
|
||||
|
||||
@@ -0,0 +1,481 @@
|
||||
#include "interstellar.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <cmath>
|
||||
#include <cstring>
|
||||
#include <vector>
|
||||
|
||||
#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<typename Skip> 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<Mark> 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<uint32_t>(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
|
||||
@@ -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
|
||||
@@ -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<std::string> wrap(Display &it, display::BaseFont *font, const std::string &text, int width,
|
||||
size_t max_lines) {
|
||||
std::vector<std::string> 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<size_t>(s.size(), t * cps / 1000)); }
|
||||
|
||||
// Dissolve the page in over its first 400 ms.
|
||||
void intro(EL512Display &it, uint32_t t) {
|
||||
if (t < 400)
|
||||
|
||||
@@ -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<std::string> wrap(Display &it, display::BaseFont *font, const std::string &text, int width,
|
||||
size_t max_lines) {
|
||||
std::vector<std::string> 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<size_t>(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); }
|
||||
|
||||
|
||||
@@ -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<std::string> wrap(Display &it, display::BaseFont *font, const std::string &text, int width,
|
||||
size_t max_lines) {
|
||||
std::vector<std::string> 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<size_t>(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); }
|
||||
|
||||
|
||||
@@ -0,0 +1,239 @@
|
||||
#include "yourname.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <cmath>
|
||||
|
||||
#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
|
||||
@@ -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
|
||||
@@ -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
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 6.7 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 7.4 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 6.1 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 8.2 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 6.6 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 7.3 KiB |
@@ -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());
|
||||
@@ -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 {
|
||||
|
||||
|
||||
@@ -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<Page> 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<Page> 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); }},
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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__)
|
||||
Reference in new issue
Block a user