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>
482 lines
19 KiB
C++
482 lines
19 KiB
C++
#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
|