Files
planar-EL512.256-H/components/el512/yourname.cpp
T
mangavalkandClaude Opus 5.5 f46f7ab6c5 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>
2026-10-19 18:59:00 +02:00

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