#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