Files
wadamesh/src/ui-touch/Constellation.cpp
T
Kaj SchittecatandClaude Opus 5.5 218bc6ed3e touch: beta_91, Home becomes widgets, backgrounds, GPS in the bar
Home is a grid of arrangeable widgets (24 kinds, presets, a layout per
orientation, Lua app widgets via wada.widget); Unread, Chat and Mentions
get a large size. Theme > Background puts an aurora (still by default, or
moving), a colour, a picture or nothing behind every screen, with glass
cards and a smoked-glass top bar. A GPS icon in the bar, a GPS battery
saver (u-blox GNSS stop/start where there is no power switch), and a
battery saver that no longer waits for Bluetooth and Wi-Fi to be off.
Also: navigation bar position, every settings page on the redesign's
cards, a faster app drawer, slower screensaver drift for burn-in, text
wrapping at spaces only, the V4-R8 Store Apps tab, and SDK Test marked
extended-SDK only (keeps the 2 MB V4 at 99.8%).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-09 00:14:04 +02:00

1562 lines
65 KiB
C++

// SPDX-License-Identifier: GPL-3.0-or-later
#include "Constellation.h"
#include <initializer_list>
#include <math.h>
#include <stdio.h>
#include <string.h>
// The clock face is a digits-and-colon cut of Montserrat Light (generated by
// scripts/build/gen-touch-fonts.sh). The Tanmatsu's 800x480 logical screen
// needs the large cut; every other board takes 40 px, including the P4, which
// renders at half its panel resolution and so shows it at 80 physical pixels.
#if defined(HAS_TANMATSU)
extern "C" const lv_font_t clock_font_96;
#define CST_CLOCK_FONT (&clock_font_96)
#else
extern "C" const lv_font_t clock_font_40;
#define CST_CLOCK_FONT (&clock_font_40)
#endif
#if defined(DOC_CAPTURE)
int g_cst_debug = 0; // capture builds: 1 = the sweep redraws but draws nothing, 2 = no sweep
#endif
namespace {
constexpr int kMaxNodes = 24;
constexpr int kPings = 6;
constexpr int kDots = 6;
constexpr int kFlows = 3;
struct Pt { lv_coord_t x, y; };
struct Leg { lv_coord_t x0, y0, x1, y1; };
struct Flow { Pt p[3]; int n; }; // a route, from the node in to you
lv_obj_t* s_root = nullptr; // opaque backdrop over the whole UI
lv_obj_t* s_scene = nullptr; // everything that is drawn
lv_obj_t* s_static = nullptr; // what holds still: the rings, the N, the links, the nodes and their names
lv_obj_t* s_static_img = nullptr; // ...drawn once into an image, which is what the frames repaint
lv_obj_t* s_layer = nullptr; // links, nodes and their labels, inside s_static: rebuilt when the set changes
lv_obj_t* s_flow_obj = nullptr; // the light on the routes (flowDrawCb)
lv_obj_t* s_north = nullptr;
lv_obj_t* s_clock = nullptr;
lv_obj_t* s_date = nullptr;
lv_obj_t* s_unread = nullptr; // [dot] 3 new
lv_obj_t* s_unread_n = nullptr;
lv_obj_t* s_unread_w = nullptr;
lv_obj_t* s_info = nullptr; // 14 nodes · heard 12 s ago
lv_obj_t* s_info_n = nullptr;
lv_obj_t* s_info_t = nullptr;
lv_obj_t* s_batt = nullptr; // [bolt] 86% [battery]
lv_obj_t* s_batt_chg = nullptr;
lv_obj_t* s_batt_l = nullptr;
lv_obj_t* s_batt_i = nullptr;
lv_obj_t* s_halo = nullptr;
lv_obj_t* s_ping[kPings] = {};
lv_obj_t* s_dot[kDots] = {};
// The still part of the scene as an image (s_w x s_h, in PSRAM). Without it every
// area a frame repaints walks and redraws some fifty objects; with it the area is
// one copy. Rendered again when the nodes, their names or the N change.
lv_color_t* s_static_buf = nullptr;
lv_img_dsc_t s_static_dsc;
lv_obj_t* s_sweep = nullptr; // the radar cone, its returns, the flares and the sonar ring (sweepDrawCb)
lv_timer_t* s_sweep_timer = nullptr; // the one frame clock: the sweep, the sonar and the light on the routes
uint32_t s_sweep_tick = 0;
float s_sweep_prev = -1.0f; // beam angle at the last frame, to clear behind it
// A ring rolls out from you each time the beam passes north. Drawn by the sweep
// and repainted in thin boxes along its path: as a resized object it invalidated
// its whole square every frame, as much as the cone itself.
constexpr uint32_t kSonarMs = 1800;
uint32_t s_sonar_t0 = 0; // when it left you; 0 = none
lv_coord_t s_sonar_r = -1; // its radius this frame (set by the frame clock, read by the draw)
lv_opa_t s_sonar_opa = 0;
constexpr uint32_t kSweepPeriodMs = 6000; // one turn
constexpr int kTailDeg = 40; // afterglow behind the beam
constexpr int kClutter = 96; // faint returns that light up in the beam's wake
struct Clutter { int16_t deg10; int16_t dx, dy; }; // bearing, and its offset from you
Clutter s_clutter[kClutter];
// The range rings' dots, worked out once per show (offsets from you): the beam
// redraws the rings under it every frame, and trig per dot per frame was most
// of the cost.
struct RingDot { int16_t dx, dy; };
RingDot* s_ring_dot = nullptr;
int s_ring_n = 0;
// The cone is drawn straight into the draw buffer, a pixel at a time: each
// pixel's bearing from you comes from a quarter-disc table (one atan2 per entry,
// per show), and its opacity from how far the beam has turned past it. LVGL's
// masked polygons cost about 17 ms a frame for the same cone, and stepped.
uint16_t* s_qang = nullptr; // (s_R+1)^2 bearings within a quadrant, 16384 = 90 deg; 0xFFFF outside the rings
constexpr int kConeLut = 256;
constexpr int kConePeak = 92; // opacity just behind the beam
uint8_t s_cone_opa[kConeLut]; // by how far behind the beam, over the tail
uint32_t s_tail_units = 1; // kTailDeg in 1/65536 turns
CstTheme s_th = {};
lv_coord_t s_w = 0, s_h = 0, s_cx = 0, s_cy = 0, s_R = 0;
int s_k = 100; // geometry scale in percent; 100 = a 240 px short side
bool s_portrait = false;
// Burn-in. The whole mesh turns slowly about you, a step every five seconds and a
// turn an hour, and you drift a few pixels on a slow figure of eight, so no dot,
// ring, route or name keeps the same pixels for long, and north is wherever the
// turn has got to (the N goes with it). Both follow the time since boot instead of
// starting over with each show: a screensaver that only ever runs for a few seconds
// at a time still turns. Your own node pulses rather than holding a solid disc.
constexpr uint32_t kTurnStepMs = 5000;
constexpr uint32_t kTurnSteps = 720; // half a degree a step
float s_rot = 0.0f; // how far the mesh is turned, degrees clockwise
uint32_t s_turn_step = 0xFFFFFFFFu; // the step last drawn
lv_coord_t s_cx0 = 0, s_cy0 = 0; // the centre before the drift
lv_obj_t* s_core = nullptr; // you: the core, its glow and its ring, all pulsing
lv_obj_t* s_core_glow = nullptr;
lv_obj_t* s_self_ring = nullptr;
CstNode s_nodes[kMaxNodes];
Pt s_pos[kMaxNodes];
int s_n = 0;
uint32_t s_sig = 0;
// lv_line keeps a POINTER to its points instead of copying them, so the arrays
// have to outlive the line objects. Static, sized for the worst case.
constexpr int kLinkPts = 5; // a link in 4 pieces (see drawLineInPieces)
lv_point_t s_link[kMaxNodes][kLinkPts];
lv_point_t s_diamond[kMaxNodes][5];
Flow s_flow[kFlows];
uint32_t s_flow_col[kFlows]; // each route's light, in its node's colour
int s_nflow = 0;
Leg s_leg[kDots];
int s_next_ping = 0;
int s_next_dot = 0;
CstStatus s_st;
bool s_st_set = false;
// Space already taken this rebuild (the HUD, your own node, placed labels), so a
// node's name goes where it can be read or not at all.
struct Box { lv_coord_t x0, y0, x1, y1; };
Box s_box[kMaxNodes * 2 + 8];
int s_nbox = 0;
// The status (clock, date, unread, the line about the mesh, the battery) moves to
// another corner every two minutes, fading out and back in: a whole move, not a
// pixel at a time, and the screen never keeps text in one place for long.
int s_hud = 0; // which corner layout (see placeHud)
uint32_t s_hud_ms = 0; // when it last moved
bool s_hud_moving = false;
constexpr uint32_t kHudMoveMs = 120000;
lv_coord_t K(int px) { return (lv_coord_t)((px * s_k + 50) / 100); }
lv_coord_t K1(int px) { const lv_coord_t v = K(px); return v < 1 ? 1 : v; }
lv_color_t C(uint32_t rgb) { return lv_color_hex(rgb); }
// A node's colour: the glow, or with node_sat set its own hue, from its key, so a
// node keeps its colour from one night to the next.
uint32_t hsv32(int h, int s, int v) {
h = ((h % 360) + 360) % 360;
const int c = v * s / 100, x = c * (60 - abs(h % 120 - 60)) / 60, m = v - c;
int r = 0, g = 0, b = 0;
if (h < 60) { r = c; g = x; } else if (h < 120) { r = x; g = c; } else if (h < 180) { g = c; b = x; }
else if (h < 240) { g = x; b = c; } else if (h < 300) { r = x; b = c; } else { r = c; b = x; }
auto u8 = [](int q) { return (uint32_t)((q * 255 + 50) / 100); };
return (u8(r + m) << 16) | (u8(g + m) << 8) | u8(b + m);
}
uint32_t nodeCol(int i) {
if (!s_th.node_sat || i < 0 || i >= s_n) return s_th.glow;
return hsv32((int)(((s_nodes[i].key * 2654435761u) >> 16) % 360u), s_th.node_sat, s_th.node_val);
}
// A bare object: no theme style, never a target. Input does not reach this
// screen at all (the indev read swallows it while the screensaver is up), but
// nothing drawn here should ever be clickable or scroll.
lv_obj_t* bare(lv_obj_t* parent) {
lv_obj_t* o = lv_obj_create(parent);
lv_obj_remove_style_all(o);
lv_obj_clear_flag(o, LV_OBJ_FLAG_CLICKABLE | LV_OBJ_FLAG_SCROLLABLE);
return o;
}
lv_obj_t* disc(lv_obj_t* parent, lv_coord_t x, lv_coord_t y, lv_coord_t d) {
lv_obj_t* o = bare(parent);
lv_obj_set_size(o, d, d);
lv_obj_set_pos(o, x - d / 2, y - d / 2);
lv_obj_set_style_radius(o, LV_RADIUS_CIRCLE, 0);
return o;
}
lv_obj_t* ring(lv_obj_t* parent, lv_coord_t x, lv_coord_t y, lv_coord_t d, lv_opa_t opa) {
lv_obj_t* o = disc(parent, x, y, d);
lv_obj_set_style_border_width(o, K1(1), 0);
lv_obj_set_style_border_color(o, C(s_th.glow), 0);
lv_obj_set_style_border_opa(o, opa, 0);
return o;
}
// Lines and labels keep their class styles: stripping them would also strip the
// content-sized width/height the class sets, and the object would draw nothing.
lv_obj_t* line(lv_obj_t* parent, const lv_point_t* pts, uint16_t n, uint32_t color, lv_opa_t opa, lv_coord_t w) {
lv_obj_t* l = lv_line_create(parent);
lv_obj_clear_flag(l, LV_OBJ_FLAG_CLICKABLE | LV_OBJ_FLAG_SCROLLABLE);
lv_line_set_points(l, pts, n);
lv_obj_set_style_line_width(l, w, 0);
lv_obj_set_style_line_color(l, C(color), 0);
lv_obj_set_style_line_opa(l, opa, 0);
lv_obj_set_style_line_rounded(l, true, 0);
return l;
}
lv_obj_t* text(lv_obj_t* parent, const lv_font_t* f, uint32_t color) {
lv_obj_t* l = lv_label_create(parent);
lv_obj_clear_flag(l, LV_OBJ_FLAG_CLICKABLE | LV_OBJ_FLAG_SCROLLABLE);
lv_obj_set_style_bg_opa(l, LV_OPA_TRANSP, 0);
lv_obj_set_style_text_font(l, f, 0);
lv_obj_set_style_text_color(l, C(color), 0);
lv_label_set_text(l, "");
return l;
}
// A content-sized row of status parts. START alignment: a content-sized row
// aligned to END lays its first children out left of its own box, clipped.
lv_obj_t* row(lv_obj_t* parent, lv_coord_t gap) {
lv_obj_t* r = bare(parent);
lv_obj_set_size(r, LV_SIZE_CONTENT, LV_SIZE_CONTENT);
lv_obj_set_flex_flow(r, LV_FLEX_FLOW_ROW);
lv_obj_set_flex_align(r, LV_FLEX_ALIGN_START, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER);
lv_obj_set_style_pad_column(r, gap, 0);
lv_obj_add_flag(r, LV_OBJ_FLAG_OVERFLOW_VISIBLE); // glows reach past the row
return r;
}
const lv_font_t* small() { return s_th.small ? s_th.small : LV_FONT_DEFAULT; }
const lv_font_t* strong() { return s_th.strong ? s_th.strong : small(); }
// Recency to opacity. A node heard in the last ten minutes is solid; one not
// heard for six hours is a ghost. This is the part that makes a quiet night
// look quiet.
lv_opa_t levelOpa(uint8_t level) {
switch (level) {
case 3: return LV_OPA_COVER;
case 2: return 158;
case 1: return 107;
default: return 51;
}
}
void layout() {
s_w = lv_disp_get_hor_res(nullptr);
s_h = lv_disp_get_ver_res(nullptr);
const lv_coord_t shortest = LV_MIN(s_w, s_h);
s_k = LV_MAX(100, shortest * 100 / 240);
s_portrait = s_h > s_w;
if (s_portrait) {
// Clock across the top, the status lines along the bottom, and the mesh
// centred in what is left between them, as large as the width allows.
const lv_coord_t lh = lv_font_get_line_height(small());
const lv_coord_t top = K(14) + lv_font_get_line_height(CST_CLOCK_FONT) + K(7) + lh;
const lv_coord_t bot = s_h - K(30) - lh;
s_cx = s_w / 2;
s_cy = (top + bot) / 2;
s_R = LV_MIN(s_w * 44 / 100, (bot - top) / 2 - lh - K(6));
} else {
// Centred across, a little below the middle: the clock sits over the
// upper-left of the rings, as in the redesign.
s_cx = s_w / 2;
s_cy = s_h * 55 / 100;
s_R = LV_MIN(s_h * 45 / 100, s_w * 34 / 100);
}
s_R = LV_MAX(K(20), (lv_coord_t)(s_R - K(5))); // room for the drift (applyTurn) on every side
s_cx0 = s_cx;
s_cy0 = s_cy;
}
// The turn and the drift for this moment (see s_rot): the step from the time since
// boot, the drift a slow figure of eight, out of step with the turn.
void applyTurn(uint32_t now) {
s_turn_step = now / kTurnStepMs;
s_rot = (float)(s_turn_step % kTurnSteps) * (360.0f / (float)kTurnSteps);
const float t = (float)(now % 1380000u) * (6.2831853f / 1380000.0f); // across: 23 minutes
const float u = (float)(now % 1020000u) * (6.2831853f / 1020000.0f); // up and down: 17 minutes
s_cx = s_cx0 + (lv_coord_t)lroundf((float)K(5) * sinf(t));
s_cy = s_cy0 + (lv_coord_t)lroundf((float)K(5) * sinf(u));
}
Pt place(const CstNode& n) {
const float a = ((float)n.bearing + s_rot) * 0.017453292f; // turned with the mesh
const float r = (float)(n.dist > 1000 ? 1000 : n.dist) / 1000.0f * (float)s_R;
return { (lv_coord_t)lroundf((float)s_cx + r * sinf(a)),
(lv_coord_t)lroundf((float)s_cy - r * cosf(a)) };
}
// ---- drawn parts ------------------------------------------------------------
// The range rings, dotted: a point every few pixels, faint, so they read as a
// scale and not as content. Only the rings that cross the area being redrawn
// are walked.
void ringsBuild() {
s_ring_n = 0;
const float step = (float)K(4);
int total = 0;
for (int k = 1; k <= 3; ++k) total += LV_MAX(12, (int)(6.2831853f * ((float)s_R * k / 3.0f) / step));
// The count depends only on the radius, fixed for a show: allocated once, and the
// dots turned with the mesh at every step (cstHide frees it).
if (!s_ring_dot) s_ring_dot = static_cast<RingDot*>(lv_mem_alloc(sizeof(RingDot) * (size_t)total));
if (!s_ring_dot) return;
const float rot = s_rot * 0.017453292f;
for (int k = 1; k <= 3; ++k) {
const float r = (float)s_R * (float)k / 3.0f;
const int n = LV_MAX(12, (int)(6.2831853f * r / step));
for (int j = 0; j < n && s_ring_n < total; ++j) {
const float t = 6.2831853f * (float)j / (float)n + rot;
s_ring_dot[s_ring_n++] = { (int16_t)lroundf(r * sinf(t)), (int16_t)lroundf(-r * cosf(t)) };
}
}
}
void ringsDrawCb(lv_event_t* e) {
lv_draw_ctx_t* dc = lv_event_get_draw_ctx(e);
if (!dc || !s_ring_dot) return;
lv_area_t a;
lv_obj_get_coords(lv_event_get_target(e), &a);
const lv_area_t* clip = dc->clip_area;
const lv_coord_t cx = a.x1 + s_cx, cy = a.y1 + s_cy;
lv_draw_rect_dsc_t rd;
lv_draw_rect_dsc_init(&rd);
rd.bg_color = C(s_th.accent);
rd.bg_opa = 96;
const lv_coord_t dot = K1(1);
for (int i = 0; i < s_ring_n; ++i) {
const lv_coord_t x = cx + s_ring_dot[i].dx, y = cy + s_ring_dot[i].dy;
if (x + dot <= clip->x1 || x > clip->x2 || y + dot <= clip->y1 || y > clip->y2) continue;
const lv_area_t d = { x, y, (lv_coord_t)(x + dot - 1), (lv_coord_t)(y + dot - 1) };
lv_draw_rect(dc, &rd, &d);
}
}
// Light running in along the routes of the freshest nodes: short dashes, evenly
// spaced, moving towards you at a walking pace.
void flowDrawCb(lv_event_t* e) {
lv_draw_ctx_t* dc = lv_event_get_draw_ctx(e);
if (!dc || s_nflow <= 0) return;
lv_area_t a;
lv_obj_get_coords(lv_event_get_target(e), &a);
lv_draw_line_dsc_t ld;
lv_draw_line_dsc_init(&ld);
ld.color = C(s_th.glow);
ld.opa = 230;
ld.width = K1(1);
ld.round_start = ld.round_end = 1;
const float period = (float)K(9), dash = (float)LV_MAX(2, K(2));
const float shift = fmodf((float)((uint64_t)lv_tick_get() * (uint64_t)K(12) % 1000000000ull) / 1000.0f, period);
for (int f = 0; f < s_nflow; ++f) {
const Flow& fl = s_flow[f];
ld.color = C(s_flow_col[f]);
float start = shift;
for (int s = 0; s + 1 < fl.n; ++s) {
const float x0 = (float)(a.x1 + fl.p[s].x), y0 = (float)(a.y1 + fl.p[s].y);
const float x1 = (float)(a.x1 + fl.p[s + 1].x), y1 = (float)(a.y1 + fl.p[s + 1].y);
const float len = sqrtf((x1 - x0) * (x1 - x0) + (y1 - y0) * (y1 - y0));
if (len < 1.0f) continue;
// Clear of the nodes at both ends: the flows draw over the still image, where
// a repeater's hollow diamond would otherwise show the light through it.
const float keep = (float)K(5);
for (float d = start; d < len; d += period) {
if (d < keep || d + dash > len - keep) continue;
const float d1 = LV_MIN(len, d + dash);
const lv_point_t p1 = { (lv_coord_t)lroundf(x0 + (x1 - x0) * d / len), (lv_coord_t)lroundf(y0 + (y1 - y0) * d / len) };
const lv_point_t p2 = { (lv_coord_t)lroundf(x0 + (x1 - x0) * d1 / len), (lv_coord_t)lroundf(y0 + (y1 - y0) * d1 / len) };
lv_draw_line(dc, &ld, &p1, &p2);
}
// Carry the spacing round the corner.
start = fmodf(start - len, period);
if (start < 0) start += period;
}
}
}
// Called by the frame clock every other frame. LVGL keeps 32 invalid areas a
// frame and redraws the whole screen past that, so every part of a frame boxes
// itself in a bounded number: the routes in halves of a leg (a slanted leg's own
// box is mostly empty), at most 12.
void flowInvalidate() {
if (!s_flow_obj || s_nflow <= 0) return;
lv_area_t a;
lv_obj_get_coords(s_flow_obj, &a);
const lv_coord_t m = K(3);
for (int f = 0; f < s_nflow; ++f) {
const Flow& fl = s_flow[f];
for (int i = 0; i + 1 < fl.n; ++i) {
for (int q = 0; q < 2; ++q) {
const lv_coord_t x0 = fl.p[i].x + (fl.p[i + 1].x - fl.p[i].x) * q / 2;
const lv_coord_t y0 = fl.p[i].y + (fl.p[i + 1].y - fl.p[i].y) * q / 2;
const lv_coord_t x1 = fl.p[i].x + (fl.p[i + 1].x - fl.p[i].x) * (q + 1) / 2;
const lv_coord_t y1 = fl.p[i].y + (fl.p[i + 1].y - fl.p[i].y) * (q + 1) / 2;
const lv_area_t b = { (lv_coord_t)(a.x1 + LV_MIN(x0, x1) - m), (lv_coord_t)(a.y1 + LV_MIN(y0, y1) - m),
(lv_coord_t)(a.x1 + LV_MAX(x0, x1) + m), (lv_coord_t)(a.y1 + LV_MAX(y0, y1) + m) };
lv_obj_invalidate_area(s_flow_obj, &b);
}
}
}
}
// The battery as in the redesign: an outline with a nub, filled to the level,
// in the glow colour while charging. user_data: the percentage, +1000 charging.
void battDrawCb(lv_event_t* e) {
lv_draw_ctx_t* dc = lv_event_get_draw_ctx(e);
lv_obj_t* obj = lv_event_get_target(e);
if (!dc) return;
const int key = (int)(intptr_t)lv_obj_get_user_data(obj);
const bool chg = key >= 1000;
const int pct = LV_CLAMP(0, chg ? key - 1000 : key, 100);
lv_area_t a;
lv_obj_get_coords(obj, &a);
const lv_coord_t h = lv_area_get_height(&a);
const lv_coord_t nub = (lv_coord_t)LV_MAX(2, lv_area_get_width(&a) / 9);
lv_area_t body = a;
body.x2 -= nub;
lv_draw_rect_dsc_t od;
lv_draw_rect_dsc_init(&od);
od.radius = K1(2);
od.bg_opa = LV_OPA_TRANSP;
od.border_color = C(s_th.sub);
od.border_width = K1(1);
lv_draw_rect(dc, &od, &body);
lv_draw_rect_dsc_t fd;
lv_draw_rect_dsc_init(&fd);
fd.radius = K1(1);
fd.bg_opa = LV_OPA_COVER;
fd.bg_color = C(s_th.sub);
const lv_area_t na = { (lv_coord_t)(body.x2 + 1), (lv_coord_t)(a.y1 + h / 3), a.x2, (lv_coord_t)(a.y2 - h / 3) };
lv_draw_rect(dc, &fd, &na);
const lv_coord_t in = K1(1) + 1;
lv_area_t f = { (lv_coord_t)(body.x1 + in), (lv_coord_t)(body.y1 + in), (lv_coord_t)(body.x2 - in), (lv_coord_t)(body.y2 - in) };
const lv_coord_t w = (lv_coord_t)(lv_area_get_width(&f) * pct / 100);
if (w > 0) {
f.x2 = (lv_coord_t)(f.x1 + w - 1);
if (chg) fd.bg_color = C(s_th.glow);
lv_draw_rect(dc, &fd, &f);
}
}
void hideReady(lv_anim_t* a);
// LVGL masks an anti-aliased slanted line over its whole bounding box, so a long
// diagonal costs about its length squared. In short pieces it costs about its
// length: the beam, redrawn every frame, is drawn this way.
void drawLineInPieces(lv_draw_ctx_t* dc, const lv_draw_line_dsc_t* d, lv_point_t a, lv_point_t b, int pieces) {
lv_point_t prev = a;
for (int i = 1; i <= pieces; ++i) {
const lv_point_t p = { (lv_coord_t)(a.x + (int32_t)(b.x - a.x) * i / pieces),
(lv_coord_t)(a.y + (int32_t)(b.y - a.y) * i / pieces) };
lv_draw_line(dc, d, &prev, &p);
prev = p;
}
}
// ---- the radar sweep ----------------------------------------------------------
// A beam turns around you once every six seconds. Behind it a soft afterglow and
// a scatter of faint returns fade out, and every node it crosses flares and
// settles back, the way a radar's phosphor holds a contact. It runs while the
// screensaver is up (charging, by default), and redraws only the wedge it is in.
float beamAngle(uint32_t now) { // degrees, 0 = north, clockwise (as the bearings)
return (float)(now % kSweepPeriodMs) * 360.0f / (float)kSweepPeriodMs;
}
float behindBeam(float beam, float a) { // how far the beam has turned past a, 0..360
float d = beam - a;
while (d < 0.0f) d += 360.0f;
while (d >= 360.0f) d -= 360.0f;
return d;
}
lv_point_t polarPt(lv_coord_t cx, lv_coord_t cy, float deg, float r) {
const float a = deg * 0.017453292f;
return { (lv_coord_t)lroundf((float)cx + r * sinf(a)), (lv_coord_t)lroundf((float)cy - r * cosf(a)) };
}
void coneBuild() {
if (s_qang) { lv_mem_free(s_qang); s_qang = nullptr; }
if (s_R <= 0) return;
const int n = s_R + 1;
s_qang = static_cast<uint16_t*>(lv_mem_alloc(sizeof(uint16_t) * (size_t)n * (size_t)n));
if (!s_qang) return;
const int32_t r2 = (int32_t)s_R * s_R;
for (int v = 0; v < n; ++v)
for (int u = 0; u < n; ++u)
s_qang[v * n + u] = (u * u + v * v > r2) ? (uint16_t)0xFFFF
: (uint16_t)lroundf(atan2f((float)u, (float)v) * (16384.0f / 1.5707963f));
s_tail_units = (uint32_t)kTailDeg * 65536u / 360u;
// A short ramp up at the leading edge (a soft edge, never a line), then a fall
// faster than linear, so the light gathers just behind the beam.
const float edge = 1.2f / (float)kTailDeg;
for (int i = 0; i < kConeLut; ++i) {
const float t = ((float)i + 0.5f) / (float)kConeLut;
float o;
if (t < edge) {
o = t / edge;
} else {
const float s = 1.0f - (t - edge) / (1.0f - edge);
o = s * s;
}
s_cone_opa[i] = (uint8_t)lroundf((float)kConePeak * o);
}
}
// Blends the cone into the part of the draw buffer being rendered.
void coneDraw(lv_draw_ctx_t* dc, lv_coord_t cx, lv_coord_t cy, uint16_t beam) {
if (!s_qang || !dc->buf || !dc->buf_area) return;
const lv_area_t* clip = dc->clip_area;
const lv_area_t* ba = dc->buf_area;
const int32_t stride = lv_area_get_width(ba);
lv_color_t* buf = static_cast<lv_color_t*>(dc->buf);
const lv_color_t col = C(s_th.glow);
const int n = s_R + 1;
const int32_t x0 = LV_MAX(clip->x1, cx - s_R), x1 = LV_MIN(clip->x2, cx + s_R);
const int32_t y0 = LV_MAX(clip->y1, cy - s_R), y1 = LV_MIN(clip->y2, cy + s_R);
if (x0 > x1 || y0 > y1) return;
const uint32_t inv = ((uint32_t)kConeLut << 16) / s_tail_units;
#if LV_COLOR_DEPTH == 16
// Blended in 8 bits a channel and ordered-dithered down to 16: the gradient is
// faint, and 16-bit colour has so few levels there that a plain blend shows
// the fade as a fan of rays.
static const uint8_t kBayer[16] = { 0, 8, 2, 10, 12, 4, 14, 6, 3, 11, 1, 9, 15, 7, 13, 5 };
const int32_t cr = (int32_t)((s_th.glow >> 16) & 0xFF), cg = (int32_t)((s_th.glow >> 8) & 0xFF),
cb = (int32_t)(s_th.glow & 0xFF);
(void)col;
#endif
for (int32_t y = y0; y <= y1; ++y) {
const int32_t dy = y - cy;
const uint16_t* row = s_qang + (dy < 0 ? -dy : dy) * n;
lv_color_t* px = buf + (y - ba->y1) * stride + (x0 - ba->x1);
for (int32_t x = x0; x <= x1; ++x, ++px) {
const int32_t dx = x - cx;
const uint16_t q = row[dx < 0 ? -dx : dx];
if (q == 0xFFFF) continue;
uint16_t a; // bearing, clockwise from north, 65536 a turn
if (dx >= 0) a = dy <= 0 ? q : (uint16_t)(32768u - q);
else a = dy > 0 ? (uint16_t)(32768u + q) : (uint16_t)(0u - q);
const uint32_t d = (uint16_t)(beam - a); // how far the beam has turned past it
if (d >= s_tail_units) continue;
const int32_t o = s_cone_opa[(d * inv) >> 16];
if (!o) continue;
#if LV_COLOR_DEPTH == 16
const int32_t o256 = o + (o >> 7);
const int32_t r5 = LV_COLOR_GET_R(*px), g6 = LV_COLOR_GET_G(*px), b5 = LV_COLOR_GET_B(*px);
const int32_t pr = (r5 << 3) | (r5 >> 2), pg = (g6 << 2) | (g6 >> 4), pb = (b5 << 3) | (b5 >> 2);
const int32_t t = kBayer[((y & 3) << 2) | (x & 3)];
const int32_t r = (pr * 256 + (cr - pr) * o256 + t * 128) >> 11;
const int32_t g = (pg * 256 + (cg - pg) * o256 + t * 64) >> 10;
const int32_t b = (pb * 256 + (cb - pb) * o256 + t * 128) >> 11;
LV_COLOR_SET_R(*px, r > 31 ? 31 : r);
LV_COLOR_SET_G(*px, g > 63 ? 63 : g);
LV_COLOR_SET_B(*px, b > 31 ? 31 : b);
#else
*px = lv_color_mix(col, *px, (lv_opa_t)o);
#endif
}
}
}
void sweepDrawCb(lv_event_t* e) {
lv_draw_ctx_t* dc = lv_event_get_draw_ctx(e);
if (!dc || s_R <= 0) return;
#if defined(DOC_CAPTURE)
if (g_cst_debug == 1) return;
#endif
lv_area_t a;
lv_obj_get_coords(lv_event_get_target(e), &a);
const lv_coord_t cx = a.x1 + s_cx, cy = a.y1 + s_cy;
const uint32_t now = lv_tick_get();
const float beam = beamAngle(now);
coneDraw(dc, cx, cy, (uint16_t)((uint64_t)(now % kSweepPeriodMs) * 65536u / kSweepPeriodMs));
// Returns in the wake: faint specks that fade as the beam moves on, written
// straight into the buffer too.
if (dc->buf && dc->buf_area) {
const lv_area_t* clip = dc->clip_area;
const lv_area_t* ba = dc->buf_area;
const int32_t stride = lv_area_get_width(ba);
lv_color_t* buf = static_cast<lv_color_t*>(dc->buf);
const lv_color_t col = C(s_th.glow);
const lv_coord_t sp = K1(1);
for (int i = 0; i < kClutter; ++i) {
const lv_coord_t x = cx + s_clutter[i].dx, y = cy + s_clutter[i].dy;
if (x + sp <= clip->x1 || x > clip->x2 || y + sp <= clip->y1 || y > clip->y2) continue;
const float d = behindBeam(beam, (float)s_clutter[i].deg10 / 10.0f);
if (d > (float)kTailDeg) continue;
const lv_opa_t o = (lv_opa_t)(150.0f * (1.0f - d / (float)kTailDeg));
for (lv_coord_t yy = LV_MAX(y, clip->y1); yy <= LV_MIN((lv_coord_t)(y + sp - 1), clip->y2); ++yy)
for (lv_coord_t xx = LV_MAX(x, clip->x1); xx <= LV_MIN((lv_coord_t)(x + sp - 1), clip->x2); ++xx) {
lv_color_t* px = buf + (yy - ba->y1) * stride + (xx - ba->x1);
*px = lv_color_mix(col, *px, o);
}
}
}
// Nodes the beam has just crossed flare and settle back.
lv_draw_rect_dsc_t rd;
lv_draw_rect_dsc_init(&rd);
rd.radius = LV_RADIUS_CIRCLE;
for (int i = 0; i < s_n; ++i) {
const float d = behindBeam(beam, (float)s_nodes[i].bearing + s_rot); // where the turn has it
if (d > (float)kTailDeg) continue;
const float f = d / (float)kTailDeg; // 0 at the beam, 1 faded
const lv_point_t p = { (lv_coord_t)(a.x1 + s_pos[i].x), (lv_coord_t)(a.y1 + s_pos[i].y) };
const lv_coord_t halo = (lv_coord_t)(K(4) + (float)K(9) * f);
const lv_color_t nc = C(nodeCol(i));
rd.bg_color = nc;
rd.bg_opa = (lv_opa_t)(110.0f * (1.0f - f));
const lv_area_t h = { (lv_coord_t)(p.x - halo), (lv_coord_t)(p.y - halo), (lv_coord_t)(p.x + halo), (lv_coord_t)(p.y + halo) };
lv_draw_rect(dc, &rd, &h);
const lv_coord_t core = K1(3);
rd.bg_color = lv_color_mix(lv_color_white(), nc, 140);
rd.bg_opa = (lv_opa_t)(255.0f * (1.0f - f));
const lv_area_t k = { (lv_coord_t)(p.x - core), (lv_coord_t)(p.y - core), (lv_coord_t)(p.x + core), (lv_coord_t)(p.y + core) };
lv_draw_rect(dc, &rd, &k);
}
// The sonar ring, at the radius the frame clock set for this frame.
if (s_sonar_r > 0 && s_sonar_opa) {
lv_draw_rect_dsc_t sd;
lv_draw_rect_dsc_init(&sd);
sd.radius = LV_RADIUS_CIRCLE;
sd.bg_opa = LV_OPA_TRANSP;
sd.border_width = K1(2);
sd.border_color = C(s_th.glow);
sd.border_opa = s_sonar_opa;
const lv_area_t r = { (lv_coord_t)(cx - s_sonar_r), (lv_coord_t)(cy - s_sonar_r),
(lv_coord_t)(cx + s_sonar_r), (lv_coord_t)(cy + s_sonar_r) };
lv_draw_rect(dc, &sd, &r);
}
}
// The ring's place this frame: it eases out from you to the outer ring and fades
// as it goes. Its repaint covers the band between last frame's ring and this one,
// in ten boxes round the circle (a box per arc, not the whole square).
void sonarStep(uint32_t now, lv_coord_t cx, lv_coord_t cy) {
lv_coord_t r = -1;
lv_opa_t opa = 0;
if (s_sonar_t0) {
const uint32_t t = now - s_sonar_t0;
if (t >= kSonarMs) {
s_sonar_t0 = 0;
} else {
const float f = (float)t / (float)kSonarMs;
const float e = 1.0f - (1.0f - f) * (1.0f - f) * (1.0f - f); // ease out
r = (lv_coord_t)((float)K(5) + (float)(s_R - K(5)) * e);
opa = (lv_opa_t)(150.0f * (1.0f - e));
}
}
const lv_coord_t prev = s_sonar_r;
s_sonar_r = r;
s_sonar_opa = opa;
if (prev < 0 && r < 0) return;
const lv_coord_t lo = (prev < 0 ? r : r < 0 ? prev : LV_MIN(prev, r)) - K1(2) - 2;
const lv_coord_t hi = (prev < 0 ? r : r < 0 ? prev : LV_MAX(prev, r)) + 2;
const float r0 = (float)LV_MAX(0, lo), r1 = (float)hi;
constexpr int kSeg = 10;
for (int k = 0; k < kSeg; ++k) {
const float a0 = 360.0f * (float)k / kSeg, a1 = 360.0f * (float)(k + 1) / kSeg;
lv_area_t b = { 32767, 32767, -32768, -32768 };
for (float ang : { a0, (a0 + a1) / 2.0f, a1 }) {
for (float rr : { r0, r1 }) {
const lv_point_t p = polarPt(cx, cy, ang, rr);
b.x1 = LV_MIN(b.x1, p.x); b.y1 = LV_MIN(b.y1, p.y);
b.x2 = LV_MAX(b.x2, p.x); b.y2 = LV_MAX(b.y2, p.y);
}
}
b.x1 -= 2; b.y1 -= 2; b.x2 += 2; b.y2 += 2;
lv_obj_invalidate_area(s_sweep, &b);
}
}
// Each frame: redraw what the cone covers now together with what it covered last
// frame, so the trail behind it is cleared as it turns. In three radial bands,
// each boxed on its own, plus a small box per flaring node: one box around the
// whole wedge was mostly empty corners, and the repaint under it is the cost.
void sweepTimerCb(lv_timer_t*) {
if (!s_sweep) return;
++s_sweep_tick;
if (s_sweep_tick & 1) flowInvalidate(); // the routes' light at half the rate
#if defined(DOC_CAPTURE)
if (g_cst_debug == 2) return;
#endif
const uint32_t now = lv_tick_get();
const float beam = beamAngle(now);
lv_area_t o;
lv_obj_get_coords(s_sweep, &o);
const lv_coord_t cx = o.x1 + s_cx, cy = o.y1 + s_cy;
if (s_sweep_prev >= 0.0f && beam < s_sweep_prev) s_sonar_t0 = now ? now : 1; // the beam passed north
sonarStep(now, cx, cy);
const float from = (s_sweep_prev < 0.0f ? beam : s_sweep_prev) - (float)kTailDeg - 3.0f;
float span = beam + 2.0f - from;
while (span < 0.0f) span += 360.0f;
if (span > 360.0f) span = 360.0f;
for (int band = 0; band < 3; ++band) {
const float r0 = (float)s_R * (float)band / 3.0f, r1 = (float)s_R * (float)(band + 1) / 3.0f + 2.0f;
lv_area_t b = { 32767, 32767, -32768, -32768 };
for (float t = 0.0f; ; t += 6.0f) {
if (t > span) t = span;
const lv_point_t p0 = polarPt(cx, cy, from + t, r0), p1 = polarPt(cx, cy, from + t, r1);
b.x1 = LV_MIN(b.x1, LV_MIN(p0.x, p1.x)); b.y1 = LV_MIN(b.y1, LV_MIN(p0.y, p1.y));
b.x2 = LV_MAX(b.x2, LV_MAX(p0.x, p1.x)); b.y2 = LV_MAX(b.y2, LV_MAX(p0.y, p1.y));
if (t >= span) break;
}
b.x1 -= 2; b.y1 -= 2; b.x2 += 2; b.y2 += 2;
lv_obj_invalidate_area(s_sweep, &b);
}
// The flares: a box each, but no more than five; past that, one box round them all.
const lv_coord_t m = K(14); // a flare's reach
lv_area_t all = { 32767, 32767, -32768, -32768 };
int flares = 0;
for (int i = 0; i < s_n; ++i) {
if (behindBeam(beam, (float)s_nodes[i].bearing + s_rot) > (float)kTailDeg + 8.0f) continue;
const lv_area_t f = { (lv_coord_t)(o.x1 + s_pos[i].x - m), (lv_coord_t)(o.y1 + s_pos[i].y - m),
(lv_coord_t)(o.x1 + s_pos[i].x + m), (lv_coord_t)(o.y1 + s_pos[i].y + m) };
if (flares < 5) lv_obj_invalidate_area(s_sweep, &f);
all.x1 = LV_MIN(all.x1, f.x1); all.y1 = LV_MIN(all.y1, f.y1);
all.x2 = LV_MAX(all.x2, f.x2); all.y2 = LV_MAX(all.y2, f.y2);
++flares;
}
if (flares > 5) lv_obj_invalidate_area(s_sweep, &all);
s_sweep_prev = beam;
}
// Renders s_static into s_static_buf (as lv_snapshot does) and shows it as the
// image; s_static itself stays hidden, so LVGL never walks it during a frame.
// Without the memory for it, the scene is simply drawn live.
void staticCache() {
if (!s_static || !s_static_img) return;
if (!s_static_buf)
s_static_buf = static_cast<lv_color_t*>(lv_mem_alloc(sizeof(lv_color_t) * (size_t)s_w * (size_t)s_h));
lv_disp_t* disp = lv_obj_get_disp(s_static);
lv_draw_ctx_t* dc = (s_static_buf && disp && disp->driver->draw_ctx_init)
? static_cast<lv_draw_ctx_t*>(lv_mem_alloc(disp->driver->draw_ctx_size)) : nullptr;
if (!dc) {
lv_obj_add_flag(s_static_img, LV_OBJ_FLAG_HIDDEN);
lv_obj_clear_flag(s_static, LV_OBJ_FLAG_HIDDEN);
return;
}
lv_obj_update_layout(s_static);
lv_area_t area;
lv_obj_get_coords(s_static, &area);
lv_disp_drv_t drv;
lv_disp_drv_init(&drv);
drv.hor_res = lv_disp_get_hor_res(disp);
drv.ver_res = lv_disp_get_ver_res(disp);
lv_disp_t fake;
memset(&fake, 0, sizeof fake);
fake.driver = &drv;
disp->driver->draw_ctx_init(&drv, dc);
drv.draw_ctx = dc;
dc->clip_area = &area;
dc->buf_area = &area;
dc->buf = s_static_buf;
lv_disp_t* refreshing = _lv_refr_get_disp_refreshing();
_lv_refr_set_disp_refreshing(&fake);
lv_obj_redraw(dc, s_static); // draws a hidden top object too; its children are not hidden
_lv_refr_set_disp_refreshing(refreshing);
disp->driver->draw_ctx_deinit(&drv, dc);
lv_mem_free(dc);
memset(&s_static_dsc, 0, sizeof s_static_dsc);
s_static_dsc.header.cf = LV_IMG_CF_TRUE_COLOR;
s_static_dsc.header.w = (uint32_t)lv_area_get_width(&area);
s_static_dsc.header.h = (uint32_t)lv_area_get_height(&area);
s_static_dsc.data_size = (uint32_t)(sizeof(lv_color_t) * (size_t)s_static_dsc.header.w * s_static_dsc.header.h);
s_static_dsc.data = reinterpret_cast<const uint8_t*>(s_static_buf);
lv_img_cache_invalidate_src(&s_static_dsc);
lv_img_set_src(s_static_img, &s_static_dsc);
lv_obj_add_flag(s_static, LV_OBJ_FLAG_HIDDEN);
lv_obj_clear_flag(s_static_img, LV_OBJ_FLAG_HIDDEN);
lv_obj_invalidate(s_static_img);
}
// ---- motion ---------------------------------------------------------------
// Each animation's var is the object it moves, so LVGL removes it when the
// object is deleted (lv_obj destructor -> lv_anim_del(obj, NULL)). Callbacks
// only hide; nothing is ever deleted from inside one.
void haloExec(void* var, int32_t v) {
lv_obj_t* o = static_cast<lv_obj_t*>(var);
const lv_coord_t d0 = K(8), d1 = K(17);
const lv_coord_t d = d0 + (lv_coord_t)((int32_t)(d1 - d0) * v / 1000);
lv_obj_set_size(o, d, d);
lv_obj_set_pos(o, s_cx - d / 2, s_cy - d / 2);
lv_obj_set_style_border_opa(o, (lv_opa_t)(140 * (1000 - v) / 1000), 0);
}
// You, pulsing: the core, its glow and its ring swell and brighten together, then
// shrink and dim, so the centre never holds one solid disc. Positions are set here
// too, which keeps them on the drifting centre.
void corePulseExec(void* var, int32_t v) {
lv_obj_t* core = static_cast<lv_obj_t*>(var);
const lv_coord_t d = K(5) + (lv_coord_t)((int32_t)(K(9) - K(5)) * v / 1000);
lv_obj_set_size(core, d, d);
lv_obj_set_pos(core, s_cx - d / 2, s_cy - d / 2);
lv_obj_set_style_bg_opa(core, (lv_opa_t)(70 + 185 * v / 1000), 0);
if (s_core_glow) {
const lv_coord_t g = K(9) + (lv_coord_t)((int32_t)(K(16) - K(9)) * v / 1000);
lv_obj_set_size(s_core_glow, g, g);
lv_obj_set_pos(s_core_glow, s_cx - g / 2, s_cy - g / 2);
lv_obj_set_style_bg_opa(s_core_glow, (lv_opa_t)(20 + 60 * v / 1000), 0);
}
if (s_self_ring) lv_obj_set_style_border_opa(s_self_ring, (lv_opa_t)(25 + 75 * v / 1000), 0);
}
void pingExec(void* var, int32_t v) {
lv_obj_t* o = static_cast<lv_obj_t*>(var);
const uint32_t xy = (uint32_t)(uintptr_t)lv_obj_get_user_data(o);
const lv_coord_t x = (int16_t)(xy >> 16), y = (int16_t)(xy & 0xFFFFu);
const lv_coord_t d0 = K(6), d1 = K(56);
const lv_coord_t d = d0 + (lv_coord_t)((int32_t)(d1 - d0) * v / 1000);
lv_obj_set_size(o, d, d);
lv_obj_set_pos(o, x - d / 2, y - d / 2);
lv_obj_set_style_border_opa(o, (lv_opa_t)(255 * (1000 - v) / 1000), 0);
}
void hideReady(lv_anim_t* a) { lv_obj_add_flag(static_cast<lv_obj_t*>(a->var), LV_OBJ_FLAG_HIDDEN); }
void pingAt(lv_coord_t x, lv_coord_t y, uint32_t col) {
if (!s_root) return;
lv_obj_t* o = s_ping[s_next_ping];
s_next_ping = (s_next_ping + 1) % kPings;
lv_anim_del(o, pingExec);
lv_obj_set_style_border_color(o, C(col), 0);
lv_obj_set_user_data(o, (void*)(uintptr_t)(((uint32_t)(uint16_t)x << 16) | (uint16_t)y));
lv_obj_clear_flag(o, LV_OBJ_FLAG_HIDDEN);
lv_anim_t a;
lv_anim_init(&a);
lv_anim_set_var(&a, o);
lv_anim_set_exec_cb(&a, pingExec);
lv_anim_set_values(&a, 0, 1000);
lv_anim_set_time(&a, 1550);
lv_anim_set_path_cb(&a, lv_anim_path_ease_out);
lv_anim_set_ready_cb(&a, hideReady);
lv_anim_start(&a);
}
void dotExec(void* var, int32_t v) {
lv_obj_t* o = static_cast<lv_obj_t*>(var);
const Leg& L = s_leg[(int)(intptr_t)lv_obj_get_user_data(o)];
const lv_coord_t x = L.x0 + (lv_coord_t)((int32_t)(L.x1 - L.x0) * v / 1000);
const lv_coord_t y = L.y0 + (lv_coord_t)((int32_t)(L.y1 - L.y0) * v / 1000);
const lv_coord_t d = K(5);
lv_obj_set_pos(o, x - d / 2, y - d / 2);
// Fade on arrival rather than vanishing on the node.
lv_obj_set_style_bg_opa(o, v > 820 ? (lv_opa_t)(255 * (1000 - v) / 180) : LV_OPA_COVER, 0);
}
void dotStart(lv_anim_t* a) { lv_obj_clear_flag(static_cast<lv_obj_t*>(a->var), LV_OBJ_FLAG_HIDDEN); }
// A packet travelling one leg of its path. Delayed legs stay hidden until they
// start, so a two-hop arrival reads as one dot handed on by the repeater.
void launchDot(Pt from, Pt to, uint32_t delay_ms, uint32_t col) {
if (!s_root) return;
const int i = s_next_dot;
s_next_dot = (s_next_dot + 1) % kDots;
lv_obj_t* o = s_dot[i];
lv_anim_del(o, dotExec);
lv_obj_set_style_bg_color(o, lv_color_mix(lv_color_white(), C(col), 110), 0);
lv_obj_set_style_outline_color(o, C(col), 0);
s_leg[i] = { from.x, from.y, to.x, to.y };
lv_obj_set_user_data(o, (void*)(intptr_t)i);
lv_obj_add_flag(o, LV_OBJ_FLAG_HIDDEN);
lv_anim_t a;
lv_anim_init(&a);
lv_anim_set_var(&a, o);
lv_anim_set_exec_cb(&a, dotExec);
lv_anim_set_values(&a, 0, 1000);
lv_anim_set_time(&a, 650);
lv_anim_set_delay(&a, delay_ms);
lv_anim_set_path_cb(&a, lv_anim_path_ease_in_out);
lv_anim_set_start_cb(&a, dotStart);
lv_anim_set_ready_cb(&a, hideReady);
lv_anim_start(&a);
}
// ---- nodes ----------------------------------------------------------------
uint32_t fnv(uint32_t h, const void* p, size_t n) {
const uint8_t* b = static_cast<const uint8_t*>(p);
for (size_t i = 0; i < n; ++i) { h ^= b[i]; h *= 16777619u; }
return h;
}
// Hashed field by field: CstNode has tail padding, and padding is not
// guaranteed to be initialised, so hashing the raw struct would see changes
// that are not there and rebuild the scene for nothing.
uint32_t signature(const CstNode* n, int count) {
uint32_t h = 2166136261u;
h = fnv(h, &count, sizeof count);
for (int i = 0; i < count; ++i) {
h = fnv(h, &n[i].key, sizeof n[i].key);
h = fnv(h, &n[i].bearing, sizeof n[i].bearing);
h = fnv(h, &n[i].dist, sizeof n[i].dist);
h = fnv(h, &n[i].kind, sizeof n[i].kind);
h = fnv(h, &n[i].level, sizeof n[i].level);
h = fnv(h, &n[i].via, sizeof n[i].via);
h = fnv(h, n[i].label, strnlen(n[i].label, sizeof n[i].label));
}
return h;
}
bool boxFree(const Box& b) {
if (b.x0 < 0 || b.y0 < 0 || b.x1 > s_w || b.y1 > s_h) return false;
for (int i = 0; i < s_nbox; ++i) {
const Box& o = s_box[i];
if (b.x0 < o.x1 && b.x1 > o.x0 && b.y0 < o.y1 && b.y1 > o.y0) return false;
}
return true;
}
void boxTake(const Box& b) {
if (s_nbox < (int)(sizeof s_box / sizeof s_box[0])) s_box[s_nbox++] = b;
}
// The parts of a node name the font can draw. Names are full of emoji, and a
// row of placeholder boxes says less than a shorter name does. Spaces left
// doubled or trailing by a dropped character go too.
void drawable(char* out, size_t cap, const char* in, const lv_font_t* f) {
size_t o = 0;
uint32_t i = 0;
const uint32_t len = (uint32_t)strlen(in);
while (i < len) {
const uint32_t at = i;
const uint32_t cp = _lv_txt_encoded_next(in, &i);
if (!cp || i <= at) break;
if (cp == ' ') {
if (o == 0 || out[o - 1] == ' ') continue;
} else {
lv_font_glyph_dsc_t g;
if (!lv_font_get_glyph_dsc(f, &g, cp, 0) || g.is_placeholder) continue;
}
const size_t n = i - at;
if (o + n >= cap) break;
memcpy(out + o, in + at, n);
o += n;
}
while (o > 0 && out[o - 1] == ' ') --o;
out[o] = 0;
}
void drawNodeShape(int i) {
const CstNode& n = s_nodes[i];
const Pt p = s_pos[i];
const lv_opa_t opa = levelOpa(n.level);
const uint32_t col = nodeCol(i);
boxTake({ (lv_coord_t)(p.x - K(5)), (lv_coord_t)(p.y - K(5)), (lv_coord_t)(p.x + K(5)), (lv_coord_t)(p.y + K(5)) });
if (n.kind == CST_REPEATER) {
// Repeaters are the infrastructure: a hollow diamond, masked underneath so
// the line arriving at its centre does not show through.
const lv_coord_t hs = n.level >= 3 ? K(4) : K(3);
lv_obj_t* mask = disc(s_layer, p.x, p.y, hs * 2);
lv_obj_set_style_bg_color(mask, C(s_th.bg), 0);
lv_obj_set_style_bg_opa(mask, LV_OPA_COVER, 0);
lv_point_t* d = s_diamond[i];
d[0] = { p.x, (lv_coord_t)(p.y - hs) };
d[1] = { (lv_coord_t)(p.x + hs), p.y };
d[2] = { p.x, (lv_coord_t)(p.y + hs) };
d[3] = { (lv_coord_t)(p.x - hs), p.y };
d[4] = d[0];
line(s_layer, d, 5, col, opa, K1(1));
} else if (n.kind == CST_ROOM) {
const lv_coord_t s = n.level >= 3 ? K(7) : K(6);
lv_obj_t* o = bare(s_layer);
lv_obj_set_size(o, s, s);
lv_obj_set_pos(o, p.x - s / 2, p.y - s / 2);
lv_obj_set_style_radius(o, K(1), 0);
lv_obj_set_style_bg_color(o, C(s_th.bg), 0);
lv_obj_set_style_bg_opa(o, LV_OPA_COVER, 0);
lv_obj_set_style_border_width(o, K1(1), 0);
lv_obj_set_style_border_color(o, C(col), 0);
lv_obj_set_style_border_opa(o, opa, 0);
} else {
const lv_coord_t d = n.level >= 3 ? K(6) : n.level == 2 ? K(5) : K(4);
// Every node glows, in proportion to how fresh it is: a soft disc behind it.
// (A shadow blur would be re-run each time the beam passes over it.)
lv_obj_t* g = disc(s_layer, p.x, p.y, d + K(6));
lv_obj_set_style_bg_color(g, C(col), 0);
lv_obj_set_style_bg_opa(g, (lv_opa_t)(opa * 40 / 255), 0);
lv_obj_t* o = disc(s_layer, p.x, p.y, d);
lv_obj_set_style_bg_color(o, C(col), 0);
lv_obj_set_style_bg_opa(o, opa, 0);
}
}
// After every shape, so a name steers around all of them, not only the ones
// drawn before it.
void drawNodeLabel(int i) {
const CstNode& n = s_nodes[i];
const Pt p = s_pos[i];
if (!n.label[0]) return;
char name[sizeof n.label];
drawable(name, sizeof name, n.label, small());
if (!name[0]) return;
const lv_coord_t tw = lv_txt_get_width(name, (uint32_t)strlen(name), small(), 0, LV_TEXT_FLAG_NONE);
const lv_coord_t lh = lv_font_get_line_height(small());
const lv_coord_t y0 = p.y - lh / 2;
// Right of the node, else left of it, else above or below, else no name.
const Box right = { (lv_coord_t)(p.x + K(7)), y0, (lv_coord_t)(p.x + K(7) + tw), (lv_coord_t)(y0 + lh) };
const Box left = { (lv_coord_t)(p.x - K(7) - tw), y0, (lv_coord_t)(p.x - K(7)), (lv_coord_t)(y0 + lh) };
const Box above = { (lv_coord_t)(p.x - tw / 2), (lv_coord_t)(p.y - K(6) - lh), (lv_coord_t)(p.x - tw / 2 + tw), (lv_coord_t)(p.y - K(6)) };
const Box below = { (lv_coord_t)(p.x - tw / 2), (lv_coord_t)(p.y + K(6)), (lv_coord_t)(p.x - tw / 2 + tw), (lv_coord_t)(p.y + K(6) + lh) };
const Box* at = boxFree(right) ? &right : boxFree(left) ? &left
: boxFree(above) ? &above : boxFree(below) ? &below : nullptr;
if (!at) return;
boxTake(*at);
lv_obj_t* l = text(s_layer, small(), s_th.sub);
lv_label_set_text(l, name);
lv_obj_set_pos(l, at->x0, at->y0);
}
// The N: just outside the outer ring where the turn has taken north, or just inside
// it where outside would leave the screen.
Box s_north_box = { 0, 0, 0, 0 };
void northPlace() {
if (!s_north) return;
const lv_coord_t lh = lv_font_get_line_height(small());
const lv_coord_t hw = K(10);
lv_point_t p = polarPt(s_cx, s_cy, s_rot, (float)(s_R + K(2) + lh / 2));
if (p.x - hw < 0 || p.x + hw > s_w || p.y - lh / 2 < 0 || p.y + lh / 2 > s_h)
p = polarPt(s_cx, s_cy, s_rot, (float)(s_R - K(2) - lh / 2));
lv_obj_set_pos(s_north, p.x - hw, p.y - lh / 2);
s_north_box = { (lv_coord_t)(p.x - hw), (lv_coord_t)(p.y - lh / 2), (lv_coord_t)(p.x + hw), (lv_coord_t)(p.y + lh / 2) };
}
// What labels must stay clear of: the status text and your own node. Sizes are
// worked out from the fonts, not read back from the labels, which have not been
// laid out yet when the nodes are drawn.
void reserveHud() {
s_nbox = 0;
const lv_coord_t lh = lv_font_get_line_height(small());
const lv_coord_t ch = lv_font_get_line_height(CST_CLOCK_FONT);
const lv_coord_t cw = lv_txt_get_width("00:00", 5, CST_CLOCK_FONT, 0, LV_TEXT_FLAG_NONE);
const lv_coord_t g = K(4);
boxTake({ (lv_coord_t)(s_cx - K(14)), (lv_coord_t)(s_cy - K(14)), (lv_coord_t)(s_cx + K(14)), (lv_coord_t)(s_cy + K(14)) });
boxTake(s_north_box); // the N, wherever the turn has it
const lv_coord_t clock_block = K(14) + ch + K(7) + lh + g;
if (s_portrait) {
const bool top = (s_hud & 1) == 0;
const lv_coord_t x0 = s_w / 2 - cw / 2 - g, x1 = s_w / 2 + cw / 2 + g;
if (top) {
boxTake({ x0, 0, x1, clock_block });
boxTake({ (lv_coord_t)(s_w - K(70)), 0, s_w, (lv_coord_t)(K(6) + lh + g) });
boxTake({ 0, (lv_coord_t)(s_h - K(30) - lh - g), s_w, s_h });
} else {
boxTake({ x0, (lv_coord_t)(s_h - clock_block), x1, s_h });
boxTake({ (lv_coord_t)(s_w - K(70)), (lv_coord_t)(s_h - K(6) - lh - g), s_w, s_h });
boxTake({ 0, 0, s_w, (lv_coord_t)(K(30) + lh + g) });
}
} else {
const bool right = s_hud == 1 || s_hud == 2, bottom = s_hud >= 2;
const lv_coord_t cbw = K(14) + cw + g;
const lv_coord_t cy0 = bottom ? (lv_coord_t)(s_h - clock_block) : 0;
const lv_coord_t cy1 = bottom ? s_h : clock_block;
boxTake({ right ? (lv_coord_t)(s_w - cbw) : (lv_coord_t)0, cy0, right ? s_w : cbw, cy1 }); // clock and date
const lv_coord_t uh = K(12) + lh + g;
boxTake({ right ? (lv_coord_t)0 : (lv_coord_t)(s_w - K(90)), bottom ? (lv_coord_t)(s_h - uh) : (lv_coord_t)0,
right ? K(90) : s_w, bottom ? s_h : uh }); // unread
const lv_coord_t sh = K(10) + lh + g;
boxTake({ 0, bottom ? (lv_coord_t)0 : (lv_coord_t)(s_h - sh), s_w, bottom ? sh : s_h }); // mesh line, battery
}
}
// The status in its current corner layout (s_hud): the clock and the date in one
// corner, the unread count beside them on the same edge, the line about the mesh
// and the battery along the opposite edge. The clock face, or nothing: the face
// only has digits and a colon, and a clock we cannot trust is not drawn as one;
// the date line, which then says why, moves into its place. The face's line
// height is its digit height (it has nothing else).
bool s_clock_shown = true;
void placeHud() {
const lv_coord_t clock_h = lv_font_get_line_height(CST_CLOCK_FONT);
const lv_coord_t lh = lv_font_get_line_height(small());
if (s_clock_shown) lv_obj_clear_flag(s_clock, LV_OBJ_FLAG_HIDDEN);
else lv_obj_add_flag(s_clock, LV_OBJ_FLAG_HIDDEN);
const lv_coord_t stack = s_clock_shown ? clock_h + K(7) : 0; // the date under the clock
if (s_portrait) {
if ((s_hud & 1) == 0) {
lv_obj_align(s_clock, LV_ALIGN_TOP_MID, 0, K(14));
lv_obj_align(s_date, LV_ALIGN_TOP_MID, 0, K(14) + stack);
lv_obj_align(s_batt, LV_ALIGN_TOP_RIGHT, -K(10), K(6));
lv_obj_align(s_unread, LV_ALIGN_BOTTOM_MID, 0, -K(30));
lv_obj_align(s_info, LV_ALIGN_BOTTOM_MID, 0, -K(12));
} else {
lv_obj_align(s_date, LV_ALIGN_BOTTOM_MID, 0, -K(14));
lv_obj_align(s_clock, LV_ALIGN_BOTTOM_MID, 0, -(K(14) + lh + K(7)));
lv_obj_align(s_batt, LV_ALIGN_BOTTOM_RIGHT, -K(10), -K(6));
lv_obj_align(s_unread, LV_ALIGN_TOP_MID, 0, K(12));
lv_obj_align(s_info, LV_ALIGN_TOP_MID, 0, K(12) + lh + K(6));
}
return;
}
const bool right = s_hud == 1 || s_hud == 2, bottom = s_hud >= 2;
const lv_coord_t sx = right ? -1 : 1;
const lv_align_t near_top = right ? LV_ALIGN_TOP_RIGHT : LV_ALIGN_TOP_LEFT;
const lv_align_t near_bot = right ? LV_ALIGN_BOTTOM_RIGHT : LV_ALIGN_BOTTOM_LEFT;
const lv_align_t far_top = right ? LV_ALIGN_TOP_LEFT : LV_ALIGN_TOP_RIGHT;
const lv_align_t far_bot = right ? LV_ALIGN_BOTTOM_LEFT : LV_ALIGN_BOTTOM_RIGHT;
if (!bottom) {
lv_obj_align(s_clock, near_top, sx * K(14), K(14));
lv_obj_align(s_date, near_top, sx * K(15), K(14) + stack);
lv_obj_align(s_unread, far_top, -sx * K(14), K(12));
lv_obj_align(s_info, near_bot, sx * K(14), -K(10));
lv_obj_align(s_batt, far_bot, -sx * K(14), -K(10));
} else {
lv_obj_align(s_date, near_bot, sx * K(15), -K(12));
lv_obj_align(s_clock, near_bot, sx * K(14), -(K(12) + lh + K(7)));
lv_obj_align(s_unread, far_bot, -sx * K(14), -K(12));
lv_obj_align(s_info, near_top, sx * K(14), K(10));
lv_obj_align(s_batt, far_top, -sx * K(14), K(10));
}
}
void placeClock(bool shown) {
s_clock_shown = shown;
placeHud();
}
void hudOpaExec(void* var, int32_t v) {
(void)var;
for (lv_obj_t* o : { s_clock, s_date, s_unread, s_info, s_batt })
if (o) lv_obj_set_style_opa(o, (lv_opa_t)v, 0);
}
void hudShownReady(lv_anim_t*) { s_hud_moving = false; }
void hudHiddenReady(lv_anim_t*) {
if (!s_root) return;
s_hud = (s_hud + 1) % (s_portrait ? 2 : 4);
placeHud();
s_sig = 0; // re-place the node names around the status in its new corner
lv_anim_t a;
lv_anim_init(&a);
lv_anim_set_var(&a, s_clock);
lv_anim_set_exec_cb(&a, hudOpaExec);
lv_anim_set_values(&a, 0, 255);
lv_anim_set_time(&a, 500);
lv_anim_set_path_cb(&a, lv_anim_path_ease_out);
lv_anim_set_ready_cb(&a, hudShownReady);
lv_anim_start(&a);
}
void hudMove() {
if (!s_root || s_hud_moving || !s_clock) return;
s_hud_moving = true;
lv_anim_t a;
lv_anim_init(&a);
lv_anim_set_var(&a, s_clock);
lv_anim_set_exec_cb(&a, hudOpaExec);
lv_anim_set_values(&a, 255, 0);
lv_anim_set_time(&a, 500);
lv_anim_set_path_cb(&a, lv_anim_path_ease_in);
lv_anim_set_ready_cb(&a, hudHiddenReady);
lv_anim_start(&a);
}
} // namespace
void cstShow(lv_obj_t* parent, const CstTheme& theme) {
if (s_root || !parent) return;
s_th = theme;
if (!s_th.glow) s_th.glow = s_th.accent;
if (!s_th.ter) s_th.ter = s_th.sub;
layout();
applyTurn(lv_tick_get()); // where the turn and the drift are now
s_root = bare(parent);
lv_obj_set_size(s_root, s_w, s_h);
lv_obj_set_pos(s_root, 0, 0);
lv_obj_set_style_bg_color(s_root, C(s_th.bg), 0);
lv_obj_set_style_bg_opa(s_root, LV_OPA_COVER, 0);
s_scene = bare(s_root);
lv_obj_set_size(s_scene, s_w, s_h);
lv_obj_set_pos(s_scene, 0, 0);
// The still part, on its own ground so its image is complete.
s_static = bare(s_scene);
lv_obj_set_size(s_static, s_w, s_h);
lv_obj_set_pos(s_static, 0, 0);
lv_obj_set_style_bg_color(s_static, C(s_th.bg), 0);
lv_obj_set_style_bg_opa(s_static, LV_OPA_COVER, 0);
lv_obj_t* rings = bare(s_static);
lv_obj_set_size(rings, s_w, s_h);
lv_obj_set_pos(rings, 0, 0);
lv_obj_add_event_cb(rings, ringsDrawCb, LV_EVENT_DRAW_MAIN_END, nullptr);
ringsBuild();
s_north = text(s_static, strong(), s_th.ter);
lv_label_set_text(s_north, "N");
lv_obj_set_width(s_north, K(20));
lv_obj_set_style_text_align(s_north, LV_TEXT_ALIGN_CENTER, 0);
lv_obj_add_flag(s_north, LV_OBJ_FLAG_HIDDEN);
northPlace();
s_layer = bare(s_static);
lv_obj_set_size(s_layer, s_w, s_h);
lv_obj_set_pos(s_layer, 0, 0);
s_static_img = lv_img_create(s_scene);
lv_obj_clear_flag(s_static_img, LV_OBJ_FLAG_CLICKABLE | LV_OBJ_FLAG_SCROLLABLE);
lv_obj_set_pos(s_static_img, 0, 0);
lv_obj_add_flag(s_static_img, LV_OBJ_FLAG_HIDDEN);
// The light on the routes: over the still image, under you.
s_flow_obj = bare(s_scene);
lv_obj_set_size(s_flow_obj, s_w, s_h);
lv_obj_set_pos(s_flow_obj, 0, 0);
lv_obj_add_event_cb(s_flow_obj, flowDrawCb, LV_EVENT_DRAW_MAIN_END, nullptr);
// You, at the centre: a faint ring, a core with a soft glow, a slow breath going
// out from it, and the three of them pulsing (corePulseExec). Live objects rather
// than part of the still image, which would hold them on the same pixels.
s_self_ring = ring(s_scene, s_cx, s_cy, K(15), 90);
s_halo = ring(s_scene, s_cx, s_cy, K(8), 140);
s_core_glow = disc(s_scene, s_cx, s_cy, K(14)); // no shadow blur: the beam redraws it every frame
lv_obj_set_style_bg_color(s_core_glow, C(s_th.glow), 0);
lv_obj_set_style_bg_opa(s_core_glow, 70, 0);
s_core = disc(s_scene, s_cx, s_cy, K(8));
lv_obj_set_style_bg_color(s_core, C(s_th.glow), 0);
lv_obj_set_style_bg_opa(s_core, LV_OPA_COVER, 0);
{
lv_anim_t a;
lv_anim_init(&a);
lv_anim_set_var(&a, s_core);
lv_anim_set_exec_cb(&a, corePulseExec);
lv_anim_set_values(&a, 0, 1000);
lv_anim_set_time(&a, 2600);
lv_anim_set_playback_time(&a, 2600);
lv_anim_set_repeat_count(&a, LV_ANIM_REPEAT_INFINITE);
lv_anim_set_path_cb(&a, lv_anim_path_ease_in_out);
lv_anim_start(&a);
}
{
lv_anim_t a;
lv_anim_init(&a);
lv_anim_set_var(&a, s_halo);
lv_anim_set_exec_cb(&a, haloExec);
lv_anim_set_values(&a, 0, 1000);
lv_anim_set_time(&a, 2100);
lv_anim_set_repeat_delay(&a, 2100);
lv_anim_set_repeat_count(&a, LV_ANIM_REPEAT_INFINITE);
lv_anim_set_path_cb(&a, lv_anim_path_ease_in_out);
lv_anim_start(&a);
}
for (int i = 0; i < kPings; ++i) {
s_ping[i] = ring(s_scene, 0, 0, K(6), 0);
lv_obj_set_style_border_width(s_ping[i], K1(2), 0);
lv_obj_add_flag(s_ping[i], LV_OBJ_FLAG_HIDDEN);
}
for (int i = 0; i < kDots; ++i) {
// A packet on its way in: a bright glowing orb.
s_dot[i] = disc(s_scene, 0, 0, K(5));
lv_obj_set_style_bg_color(s_dot[i], lv_color_mix(lv_color_white(), C(s_th.glow), 110), 0);
lv_obj_set_style_bg_opa(s_dot[i], LV_OPA_COVER, 0);
lv_obj_set_style_outline_width(s_dot[i], K1(3), 0);
lv_obj_set_style_outline_color(s_dot[i], C(s_th.glow), 0);
lv_obj_set_style_outline_opa(s_dot[i], 110, 0);
lv_obj_add_flag(s_dot[i], LV_OBJ_FLAG_HIDDEN);
}
// The radar sweep, over the mesh and under the status.
{
uint32_t seed = 0x9E3779B9u;
for (int i = 0; i < kClutter; ++i) {
seed ^= seed << 13; seed ^= seed >> 17; seed ^= seed << 5;
const int16_t deg10 = (int16_t)(seed % 3600u);
seed ^= seed << 13; seed ^= seed >> 17; seed ^= seed << 5;
const float r = (float)s_R * (float)(180u + seed % 820u) / 1000.0f;
const float t = (float)deg10 * 0.0017453292f;
s_clutter[i] = { deg10, (int16_t)lroundf(r * sinf(t)), (int16_t)lroundf(-r * cosf(t)) };
}
coneBuild();
s_sweep = bare(s_scene);
lv_obj_set_size(s_sweep, s_w, s_h);
lv_obj_set_pos(s_sweep, 0, 0);
lv_obj_add_event_cb(s_sweep, sweepDrawCb, LV_EVENT_DRAW_MAIN_END, nullptr);
s_sweep_prev = -1.0f;
s_sweep_tick = 0;
s_sonar_t0 = 0;
s_sonar_r = -1;
s_sonar_opa = 0;
s_sweep_timer = lv_timer_create(sweepTimerCb, 66, nullptr); // 15 frames a second
}
// The status, last so it draws over the mesh.
s_clock = text(s_scene, CST_CLOCK_FONT, s_th.text);
lv_obj_set_style_text_letter_space(s_clock, -K1(1), 0);
s_date = text(s_scene, small(), s_th.sub);
lv_obj_set_style_text_letter_space(s_date, K1(1), 0);
s_unread = row(s_scene, K(3));
{
// The dot in a slot a little wider than itself: a wider gap before the
// count than between the count and its word.
lv_obj_t* slot = bare(s_unread);
lv_obj_set_size(slot, K(9), K(6));
lv_obj_t* dot = bare(slot);
lv_obj_set_size(dot, K(6), K(6));
lv_obj_set_style_radius(dot, LV_RADIUS_CIRCLE, 0);
lv_obj_set_style_bg_color(dot, C(s_th.glow), 0);
lv_obj_set_style_bg_opa(dot, LV_OPA_COVER, 0);
lv_obj_set_style_shadow_width(dot, K(6), 0);
lv_obj_set_style_shadow_color(dot, C(s_th.glow), 0);
lv_obj_set_style_shadow_opa(dot, 150, 0);
lv_obj_add_flag(slot, LV_OBJ_FLAG_OVERFLOW_VISIBLE); // the glow reaches past the slot
}
s_unread_n = text(s_unread, strong(), s_th.text);
s_unread_w = text(s_unread, small(), s_th.sub);
lv_obj_add_flag(s_unread, LV_OBJ_FLAG_HIDDEN);
s_info = row(s_scene, K(3));
s_info_n = text(s_info, strong(), s_th.text);
s_info_t = text(s_info, small(), s_th.sub);
s_batt = row(s_scene, K(4));
s_batt_chg = text(s_batt, small(), s_th.glow);
lv_label_set_text(s_batt_chg, LV_SYMBOL_CHARGE);
s_batt_l = text(s_batt, small(), s_th.sub);
s_batt_i = bare(s_batt);
lv_obj_set_size(s_batt_i, K(16), K(9));
lv_obj_add_event_cb(s_batt_i, battDrawCb, LV_EVENT_DRAW_MAIN_END, nullptr);
lv_obj_add_flag(s_batt, LV_OBJ_FLAG_HIDDEN);
s_hud = 0;
s_hud_ms = 0;
s_hud_moving = false;
placeClock(true);
s_n = 0;
s_sig = 0;
s_nflow = 0;
s_st_set = false;
s_next_ping = s_next_dot = 0;
staticCache();
}
void cstHide() {
if (!s_root) return;
lv_obj_t* r = s_root;
if (s_sweep_timer) { lv_timer_del(s_sweep_timer); s_sweep_timer = nullptr; }
s_sweep = nullptr;
s_sonar_t0 = 0;
s_sonar_r = -1;
if (s_ring_dot) { lv_mem_free(s_ring_dot); s_ring_dot = nullptr; s_ring_n = 0; }
if (s_qang) { lv_mem_free(s_qang); s_qang = nullptr; }
// Forget every pointer first: from here on nothing may touch these objects,
// and the delete itself is deferred (this can be reached from code that runs
// inside LVGL's own processing, where a synchronous delete is not safe).
// The image goes dark before its pixels are freed: the delete is deferred.
if (s_static_img) lv_obj_add_flag(s_static_img, LV_OBJ_FLAG_HIDDEN);
if (s_static_buf) { lv_mem_free(s_static_buf); s_static_buf = nullptr; }
s_root = s_scene = s_static = s_static_img = s_layer = s_flow_obj = s_north = nullptr;
s_clock = s_date = s_halo = nullptr;
s_core = s_core_glow = s_self_ring = nullptr;
s_unread = s_unread_n = s_unread_w = nullptr;
s_info = s_info_n = s_info_t = nullptr;
s_batt = s_batt_chg = s_batt_l = s_batt_i = nullptr;
for (int i = 0; i < kPings; ++i) s_ping[i] = nullptr;
for (int i = 0; i < kDots; ++i) s_dot[i] = nullptr;
s_n = 0;
s_sig = 0;
s_nflow = 0;
s_st_set = false;
lv_obj_del_async(r);
}
bool cstShown() { return s_root != nullptr; }
bool cstHudBands(lv_coord_t* top_end, lv_coord_t* bottom_start, bool* clock_top) {
if (!s_root || !s_date) return false;
lv_obj_update_layout(s_root);
const lv_coord_t sh = lv_disp_get_ver_res(nullptr);
lv_coord_t te = 0, bs = sh;
bool ct = true;
for (lv_obj_t* o : { s_clock, s_date, s_unread, s_info, s_batt }) {
if (!o || lv_obj_has_flag(o, LV_OBJ_FLAG_HIDDEN)) continue;
lv_area_t a;
lv_obj_get_coords(o, &a);
const bool top = (a.y1 + a.y2) / 2 < sh / 2;
if (top) te = LV_MAX(te, (lv_coord_t)(a.y2 + 1));
else bs = LV_MIN(bs, a.y1);
if (o == s_date) ct = top;
}
if (top_end) *top_end = te;
if (bottom_start) *bottom_start = bs;
if (clock_top) *clock_top = ct;
return true;
}
namespace {
// Everything in the still image, from the nodes and the turn as they are now:
// placed, linked, lit along their routes, named, and drawn once into the image.
void rebuildScene() {
northPlace();
for (int i = 0; i < s_n; ++i) s_pos[i] = place(s_nodes[i]);
lv_obj_clean(s_layer);
reserveHud();
// Lines first, so the nodes sit on top of them.
for (int i = 0; i < s_n; ++i) {
const int16_t via = s_nodes[i].via;
Pt to;
if (via == -1) to = { s_cx, s_cy };
else if (via >= 0 && via < s_n && via != i) to = s_pos[via];
else continue;
for (int k = 0; k < kLinkPts; ++k)
s_link[i][k] = { (lv_coord_t)(s_pos[i].x + (int32_t)(to.x - s_pos[i].x) * k / (kLinkPts - 1)),
(lv_coord_t)(s_pos[i].y + (int32_t)(to.y - s_pos[i].y) * k / (kLinkPts - 1)) };
line(s_layer, s_link[i], kLinkPts, s_th.node_sat ? nodeCol(i) : s_th.accent, s_nodes[i].level >= 2 ? 66 : 38, K1(1));
}
// The routes that carry light: the freshest nodes with a known way in. The
// caller lists nodes newest first.
s_nflow = 0;
for (int i = 0; i < s_n && s_nflow < kFlows; ++i) {
const CstNode& n = s_nodes[i];
if (n.level < 3) continue;
Flow& f = s_flow[s_nflow];
f.n = 0;
f.p[f.n++] = s_pos[i];
if (n.via >= 0 && n.via < s_n && n.via != i) {
f.p[f.n++] = s_pos[n.via];
// On to you only when the repeater is itself a direct neighbour.
if (s_nodes[n.via].via == -1) f.p[f.n++] = { s_cx, s_cy };
} else if (n.via == -1) {
f.p[f.n++] = { s_cx, s_cy };
} else {
continue;
}
s_flow_col[s_nflow] = nodeCol(i);
++s_nflow;
}
if (s_flow_obj) lv_obj_invalidate(s_flow_obj);
for (int i = 0; i < s_n; ++i) drawNodeShape(i);
for (int i = 0; i < s_n; ++i) drawNodeLabel(i);
staticCache();
}
} // namespace
void cstSetNodes(const CstNode* nodes, int count) {
if (!s_root || !nodes) return;
if (count < 0) count = 0;
if (count > kMaxNodes) count = kMaxNodes;
const uint32_t sig = signature(nodes, count);
if (sig == s_sig) return;
s_sig = sig;
memcpy(s_nodes, nodes, sizeof(CstNode) * (size_t)count);
s_n = count;
rebuildScene();
}
void cstSetStatus(const CstStatus& s) {
if (!s_root) return;
const bool all = !s_st_set;
if (all || strcmp(s.clock, s_st.clock) != 0) lv_label_set_text(s_clock, s.clock);
if (all || s.clock_ok != s_st.clock_ok) placeClock(s.clock_ok);
if (all || strcmp(s.date, s_st.date) != 0) lv_label_set_text(s_date, s.date);
if (all || s.unread != s_st.unread || strcmp(s.unread_word, s_st.unread_word) != 0) {
if (s.unread > 0) {
char b[8];
snprintf(b, sizeof b, s.unread > 999 ? "999+" : "%d", s.unread);
lv_label_set_text(s_unread_n, b);
lv_label_set_text(s_unread_w, s.unread_word);
lv_obj_clear_flag(s_unread, LV_OBJ_FLAG_HIDDEN);
} else {
lv_obj_add_flag(s_unread, LV_OBJ_FLAG_HIDDEN);
}
}
if (all || s.heard != s_st.heard || strcmp(s.info, s_st.info) != 0) {
if (s.heard >= 0) {
char b[8];
snprintf(b, sizeof b, "%d", s.heard);
lv_label_set_text(s_info_n, b);
lv_obj_clear_flag(s_info_n, LV_OBJ_FLAG_HIDDEN);
} else {
lv_obj_add_flag(s_info_n, LV_OBJ_FLAG_HIDDEN);
}
lv_label_set_text(s_info_t, s.info);
}
if (all || s.batt != s_st.batt || s.charging != s_st.charging) {
if (s.batt < 0 && !s.charging) {
lv_obj_add_flag(s_batt, LV_OBJ_FLAG_HIDDEN);
} else {
lv_obj_clear_flag(s_batt, LV_OBJ_FLAG_HIDDEN);
// Charging: the bolt and the glow fill say it; the voltage under charge
// overstates the level, so no percentage then.
if (s.charging) {
lv_obj_clear_flag(s_batt_chg, LV_OBJ_FLAG_HIDDEN);
lv_obj_add_flag(s_batt_l, LV_OBJ_FLAG_HIDDEN);
} else {
char b[8];
snprintf(b, sizeof b, "%d%%", s.batt);
lv_label_set_text(s_batt_l, b);
lv_obj_add_flag(s_batt_chg, LV_OBJ_FLAG_HIDDEN);
lv_obj_clear_flag(s_batt_l, LV_OBJ_FLAG_HIDDEN);
}
lv_obj_set_user_data(s_batt_i, (void*)(intptr_t)(s.charging ? 1000 + (s.batt < 0 ? 100 : s.batt) : s.batt));
lv_obj_invalidate(s_batt_i);
}
}
if (all || s.north_up != s_st.north_up) {
if (s.north_up) lv_obj_clear_flag(s_north, LV_OBJ_FLAG_HIDDEN);
else lv_obj_add_flag(s_north, LV_OBJ_FLAG_HIDDEN);
staticCache();
}
s_st = s;
s_st_set = true;
}
void cstPing(uint32_t key) {
if (!s_root) return;
for (int i = 0; i < s_n; ++i) {
if (s_nodes[i].key != key) continue;
const uint32_t col = nodeCol(i);
pingAt(s_pos[i].x, s_pos[i].y, col);
// The packet then travels in: through its repeater when we know which one
// it came through, straight in when it is a direct neighbour.
const int16_t via = s_nodes[i].via;
const Pt self = { s_cx, s_cy };
if (via >= 0 && via < s_n && via != i) {
launchDot(s_pos[i], s_pos[via], 0, col);
launchDot(s_pos[via], self, 600, col);
} else if (via == -1) {
launchDot(s_pos[i], self, 0, col);
}
return;
}
cstPingFar();
}
void cstPingSelf() {
if (!s_root) return;
pingAt(s_cx, s_cy, s_th.glow);
}
void cstPingFar() {
if (!s_root) return;
// Somewhere on the edge of the map: we heard it but cannot say from where.
// The angle is different each time so it never looks like a fixed beacon.
static uint32_t seed = 2463534242u;
seed ^= seed << 13; seed ^= seed >> 17; seed ^= seed << 5;
const float a = (float)(seed % 360u) * 0.017453292f;
pingAt((lv_coord_t)lroundf((float)s_cx + (float)s_R * sinf(a)),
(lv_coord_t)lroundf((float)s_cy - (float)s_R * cosf(a)), s_th.glow);
}
void cstTick(uint32_t now_ms) {
if (!s_root) return;
// Burn-in: the mesh takes its next step round and drifts (applyTurn), and the
// status moves to another corner every two minutes (a fade out, a whole move, a
// fade in). The sweep keeps every part of the mesh changing in between.
if (now_ms / kTurnStepMs != s_turn_step) {
applyTurn(now_ms);
ringsBuild();
if (s_self_ring) lv_obj_set_pos(s_self_ring, s_cx - K(15) / 2, s_cy - K(15) / 2);
rebuildScene();
}
if (!s_hud_ms) { s_hud_ms = now_ms ? now_ms : 1; return; }
if ((uint32_t)(now_ms - s_hud_ms) >= kHudMoveMs) {
s_hud_ms = now_ms ? now_ms : 1;
hudMove();
}
}