Public Access
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>
240 lines
9.6 KiB
C++
240 lines
9.6 KiB
C++
#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
|