// SPDX-License-Identifier: GPL-3.0-or-later #include "Constellation.h" #include #include #include #include // 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(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(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(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(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_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_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(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(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(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(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(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(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(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(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(); } }