diff --git a/utils/plymouth-theme-openwrt-demo/Makefile b/utils/plymouth-theme-openwrt-demo/Makefile new file mode 100644 index 0000000..50e19dc --- /dev/null +++ b/utils/plymouth-theme-openwrt-demo/Makefile @@ -0,0 +1,72 @@ +include $(TOPDIR)/rules.mk + +PKG_NAME:=plymouth-theme-openwrt-demo +PKG_VERSION:=1 +PKG_RELEASE:=1 + +PKG_MAINTAINER:=Daniel Golle +PKG_LICENSE:=GPL-2.0-or-later + +include $(INCLUDE_DIR)/package.mk + +define Package/plymouth-theme-openwrt-demo + SECTION:=utils + CATEGORY:=Utilities + TITLE:=OpenWrt boot splash demo theme + URL:=https://openwrt.org/ + DEPENDS:=+plymouth-plugin-ucode +ucode-mod-plutovg +ucode-mod-plutosvg \ + +ucode-mod-fs +endef + +define Package/plymouth-theme-openwrt-demo/description + The OpenWrt logo held still above a mesh network that is generated + afresh on every boot and carries traffic while the system starts. The + whole theme is a ucode program and an SVG, with no rendered frames. + + OpenWrt is a registered trademark owned by Software Freedom Conservancy + (SFC), used under the trademark policy at https://openwrt.org/trademark. + The logo artwork is all rights reserved and is not covered by this + package's licence. +endef + +define Build/Prepare +endef + +define Build/Configure +endef + +define Build/Compile +endef + +define Package/plymouth-theme-openwrt-demo/install + $(INSTALL_DIR) $(1)/usr/share/plymouth/themes/openwrt-demo + $(INSTALL_DATA) ./files/openwrt.plymouth \ + $(1)/usr/share/plymouth/themes/openwrt-demo/ + $(INSTALL_DATA) ./files/openwrt.uc \ + $(1)/usr/share/plymouth/themes/openwrt-demo/ + $(INSTALL_DATA) ./files/logo.svg \ + $(1)/usr/share/plymouth/themes/openwrt-demo/ + $(INSTALL_DIR) $(1)/etc/hotplug.d/ntp + $(INSTALL_DATA) ./files/hotplug-ntp.sh \ + $(1)/etc/hotplug.d/ntp/00-plymouth + $(INSTALL_DIR) $(1)/usr/share/plymouth/themes/openwrt-demo/pictograms + $(INSTALL_DATA) ./files/pictograms/*.svg \ + $(1)/usr/share/plymouth/themes/openwrt-demo/pictograms/ +endef + +define Package/plymouth-theme-openwrt-demo/postinst +#!/bin/sh +conf="$${IPKG_INSTROOT}/etc/plymouth/plymouthd.conf" + +[ -f "$$conf" ] || exit 0 + +if grep -q '^Theme=' "$$conf"; then + sed -i 's/^Theme=.*/Theme=openwrt-demo/' "$$conf" +else + echo "Theme=openwrt-demo" >> "$$conf" +fi + +exit 0 +endef + +$(eval $(call BuildPackage,plymouth-theme-openwrt-demo)) diff --git a/utils/plymouth-theme-openwrt-demo/files/hotplug-ntp.sh b/utils/plymouth-theme-openwrt-demo/files/hotplug-ntp.sh new file mode 100644 index 0000000..f200710 --- /dev/null +++ b/utils/plymouth-theme-openwrt-demo/files/hotplug-ntp.sh @@ -0,0 +1,4 @@ +[ "$ACTION" = "stratum" ] || exit 0 +[ -x /usr/bin/plymouth ] || exit 0 + +/usr/bin/plymouth update --status="service:sysntpd" 2>/dev/null diff --git a/utils/plymouth-theme-openwrt-demo/files/logo.svg b/utils/plymouth-theme-openwrt-demo/files/logo.svg new file mode 100644 index 0000000..a806488 --- /dev/null +++ b/utils/plymouth-theme-openwrt-demo/files/logo.svg @@ -0,0 +1,275 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/utils/plymouth-theme-openwrt-demo/files/openwrt.plymouth b/utils/plymouth-theme-openwrt-demo/files/openwrt.plymouth new file mode 100644 index 0000000..791e0bb --- /dev/null +++ b/utils/plymouth-theme-openwrt-demo/files/openwrt.plymouth @@ -0,0 +1,9 @@ +[Plymouth Theme] +Name=OpenWrt demo +Description=A network graph growing and carrying the boot, drawn in ucode +ModuleName=ucode + +[ucode] +ScriptFile=/usr/share/plymouth/themes/openwrt-demo/openwrt.uc +ThemeDir=/usr/share/plymouth/themes/openwrt-demo +FrameRate=30 diff --git a/utils/plymouth-theme-openwrt-demo/files/openwrt.uc b/utils/plymouth-theme-openwrt-demo/files/openwrt.uc new file mode 100644 index 0000000..73a3ecb --- /dev/null +++ b/utils/plymouth-theme-openwrt-demo/files/openwrt.uc @@ -0,0 +1,1133 @@ +import * as pvg from 'plutovg'; +import * as psvg from 'plutosvg'; +import { open, lsdir } from 'fs'; + +const PI = 3.14159265358979; + +const CYAN = { r: 0.0, g: 0.710, b: 0.886, a: 1.0 }; +const NAVY = { r: 0.0, g: 0.169, b: 0.286, a: 1.0 }; +const DEEP = { r: 0.0, g: 0.082, b: 0.149, a: 1.0 }; +const WHITE = { r: 1.0, g: 1.0, b: 1.0, a: 1.0 }; +const WARM = { r: 1.0, g: 0.83, b: 0.45, a: 1.0 }; + +const HUB_FILL = { r: 0.80, g: 0.93, b: 1.0, a: 0.90 }; +const LEAF_FILL = { r: 0.55, g: 0.78, b: 0.92, a: 0.58 }; + +const R_CORE = 0.78; +const R_HUB = 0.50; +const R_LEAF = 0.30; + +const CORE_X = 0.50; +const CORE_Y = 0.732; +const RING_R = 0.103; +const FAN_R = 0.062; +const ASPECT = 1.25; +const FAN_ANGLE = 0.55; +const RING_VARY = 0.24; +const FAN_VARY = 0.30; +const BRANCH_MIN = 4; +const BRANCH_MAX = 5; +const CLIENT_MIN = 2; +const CLIENT_MAX = 3; +const ANGLE_JITTER = 0.14; +const PEER_BOW = 0.30; + +const HOP_FRAMES = 11.0; +const HOP_DWELL = 3.0; +const LAUNCH_BACK = 0.055; +const LAUNCH_HOLD = 0.18; +const LAUNCH_CRUISE = 0.50; +const FLOW_GAP = 18; +const FLOW_CAP = 22; +const TRAIL = 3; + +const LOGO_HEIGHT = 0.26; +const BAND_TOP = 0.58; +const ROW_Y = 0.965; + +const PULSE_LIFE = 22.0; +const PULSE_MAX = 14; +const ICON_DIM = { r: 0.34, g: 0.48, b: 0.60, a: 1.0 }; +const ADDON_DIR = '/usr/share/plymouth/pictograms'; +const RC_DIR = '/etc/rc.d'; +const REVEAL_SPACING = 1.15; + +let screen; +let backdrop; +let unit = 1.0; +let icon_size = 1.0; +let packet_radius = 1.0; +let glow_radius = 1.0; +let pulse_r0 = 1.0; +let pulse_r1 = 1.0; +let nodes = []; +let links = []; +let peers = []; +let clients = []; +let hubs = []; +let core; +let reveal = []; +let revealed = 0; +let reveal_at = 0.0; +let endpoints = {}; +let next_endpoint = 0; +let on_bus = false; +let dismantle = false; +let logo_box; + +let packets = []; +let pulses = []; +let stage_list = []; +let stage_boxes = []; +let icons = {}; +let flow_timer = 0; +let primed = false; +let rng_state = 1; + +function seed_pick() { + let fh = open('/dev/urandom', 'r'); + let raw, n; + + if (!fh) + return 1; + + raw = fh.read(4); + fh.close(); + + if (!raw || length(raw) < 4) + return 1; + + n = 0; + + for (let i = 0; i < 4; i++) + n = (n * 251 + ord(raw, i)) % 2147483647; + + return n + 1; +} + +function rnd() { + rng_state = (rng_state * 1103515245 + 12345) % 2147483648; + + return rng_state / 2147483648.0; +} + +function rnd_int(lo, hi) { + return lo + int(rnd() * (hi - lo + 1)); +} + +function fsin(x) { + let x2; + + while (x > PI) + x -= 2.0 * PI; + while (x < -PI) + x += 2.0 * PI; + + x2 = x * x; + + return x * (1.0 - x2 / 6.0 * (1.0 - x2 / 20.0 * + (1.0 - x2 / 42.0 * (1.0 - x2 / 72.0)))); +} + +function fcos(x) { + return fsin(x + PI * 0.5); +} + +function node_add(x, y, r, kind, parent) { + push(nodes, { x: x, y: y, r: r, kind: kind, parent: parent, + cx: x, cy: y }); + + return length(nodes) - 1; +} + +function curve_set(idx) { + let n = nodes[idx]; + let p = nodes[n.parent]; + + n.cx = (n.x + p.x) * 0.5; + n.cy = (n.y + p.y) * 0.5; +} + +function curve_at(idx, t) { + let n = nodes[idx]; + let p = nodes[n.parent]; + let u = 1.0 - t; + let a = u * u; + let b = 2.0 * u * t; + let c = t * t; + + return { + x: a * p.x + b * n.cx + c * n.x, + y: a * p.y + b * n.cy + c * n.y + }; +} + +function link_add(a, b, kind) { + push(links, { a: a, b: b, kind: kind }); +} + +function peer_link() { + let i, a, b, mx, my; + + if (length(hubs) < 3) + return; + + i = rnd_int(1, length(hubs) - 2); + a = hubs[i]; + b = hubs[i + 1]; + mx = (nodes[a].x + nodes[b].x) * 0.5; + my = (nodes[a].y + nodes[b].y) * 0.5; + + push(peers, { + a: a, + b: b, + kind: 'peer', + cx: mx + (mx - nodes[core].x) * PEER_BOW, + cy: my + (my - nodes[core].y) * PEER_BOW + }); +} + +function branch_grow(angle) { + let ring = screen.height * RING_R * + (1.0 - RING_VARY * 0.5 + RING_VARY * rnd()); + let fan = screen.height * FAN_R; + let bx = nodes[core].x + ring * ASPECT * fcos(angle); + let by = nodes[core].y + ring * fsin(angle); + let hub = node_add(bx, by, unit * R_HUB, 'hub', core); + let count = rnd_int(CLIENT_MIN, CLIENT_MAX); + let reach, phi, idx; + + link_add(core, hub, 'trunk'); + curve_set(hub); + push(hubs, hub); + + for (let k = 0; k < count; k++) { + phi = angle + (k - (count - 1.0) * 0.5) * FAN_ANGLE; + reach = fan * (1.0 - FAN_VARY * 0.5 + FAN_VARY * rnd()); + idx = node_add(bx + reach * ASPECT * fcos(phi), + by + reach * fsin(phi), + unit * R_LEAF, 'leaf', hub); + link_add(hub, idx, 'edge'); + curve_set(idx); + push(clients, idx); + } +} + +function topo_build() { + let w = screen.width; + let h = screen.height; + let count = rnd_int(BRANCH_MIN, BRANCH_MAX); + + nodes = []; + links = []; + peers = []; + clients = []; + hubs = []; + + core = node_add(w * CORE_X, h * CORE_Y, unit * R_CORE, 'router', null); + push(hubs, core); + + for (let i = 0; i < count; i++) + branch_grow(-PI * 0.5 + PI * (2.0 * i + 1.0) / count + + (rnd() - 0.5) * ANGLE_JITTER); + + peer_link(); + + if (rnd() < 0.5) + peer_link(); + + reveal = [ core ]; + + for (let i = 1; i < length(hubs); i++) + push(reveal, hubs[i]); + + for (let i = 0; i < length(clients); i++) + push(reveal, clients[i]); + + for (let i = 0; i < length(peers); i++) + push(reveal, -1 - i); +} + +function ease_bullet(t) { + let a = LAUNCH_HOLD; + let b = LAUNCH_CRUISE; + let pb, vb, s; + + if (t <= 0.0) + return 0.0; + if (t >= 1.0) + return 1.0; + if (t < a) + return -LAUNCH_BACK * fsin(PI * t / a); + + pb = 1.0 / (1.0 + 2.0 * (1.0 - b) / (b - a)); + vb = 2.0 * pb / (b - a); + s = (t - a) / (b - a); + + if (t < b) + return pb * s * s; + + return pb + vb * (t - b); +} + +function ancestors(idx) { + let chain = []; + let at = idx; + + while (at != null) { + push(chain, at); + at = nodes[at].parent; + } + + return chain; +} + +function path_between(a, b) { + let up = ancestors(a); + let down = ancestors(b); + let meet = null; + let path = []; + + for (let i = 0; i < length(up) && meet == null; i++) { + for (let j = 0; j < length(down); j++) { + if (up[i] != down[j]) + continue; + + meet = up[i]; + break; + } + } + + if (meet == null) + return null; + + for (let i = 0; i < length(up); i++) { + push(path, up[i]); + + if (up[i] == meet) + break; + } + + for (let i = length(down) - 1; i >= 0; i--) { + if (down[i] == meet) + continue; + + push(path, down[i]); + } + + return (length(path) > 1) ? path : null; +} + +function packet_add(path, kind, delay, colour) { + push(packets, { + path: path, + kind: kind, + colour: colour, + hop: 0, + age: -delay, + dwell: 0.0, + box: null + }); +} + +function flow_inward() { + let src = clients[rnd_int(0, length(clients) - 1)]; + let path = path_between(src, core); + + if (path) + packet_add(path, 'request', 0.0, WHITE); +} + +function flow_lan() { + let a = clients[rnd_int(0, length(clients) - 1)]; + let b = clients[rnd_int(0, length(clients) - 1)]; + let path; + + if (a == b) + return; + + path = path_between(a, b); + + if (path) + packet_add(path, 'request', 0.0, WHITE); +} + +function flow_stream() { + let dst = clients[rnd_int(0, length(clients) - 1)]; + let path = path_between(core, dst); + + if (!path) + return; + + for (let i = 0; i < 5; i++) + packet_add(path, 'stream', i * 5.0, CYAN); +} + +function flow_multicast() { + let count = rnd_int(3, 4); + let dst, path; + + for (let i = 0; i < count; i++) { + dst = clients[rnd_int(0, length(clients) - 1)]; + path = path_between(core, dst); + + if (path) + packet_add(path, 'multicast', i * 2.0, WARM); + } +} + +function flow_spawn() { + let pick = rnd(); + + if (!length(clients)) + return; + + if (length(packets) > FLOW_CAP) + return; + + if (pick < 0.45) + flow_inward(); + else if (pick < 0.70) + flow_lan(); + else if (pick < 0.90) + flow_stream(); + else + flow_multicast(); +} + +function packet_at(p, back) { + let age = p.age - back; + let hop = p.hop; + let from, to, t; + + while (age < 0.0 && hop > 0) { + hop--; + age += HOP_FRAMES; + } + + if (hop > length(p.path) - 2) + hop = length(p.path) - 2; + if (hop < 0) + hop = 0; + + from = p.path[hop]; + to = p.path[hop + 1]; + t = age / HOP_FRAMES; + + if (t < 0.0) + t = 0.0; + if (t > 1.0) + t = 1.0; + + t = ease_bullet(t); + + if (nodes[to].parent == from) + return curve_at(to, t); + + if (nodes[from].parent == to) + return curve_at(from, 1.0 - t); + + return { + x: nodes[from].x + (nodes[to].x - nodes[from].x) * t, + y: nodes[from].y + (nodes[to].y - nodes[from].y) * t + }; +} + +function packet_box(p) { + let head = packet_at(p, 0); + let tail = packet_at(p, TRAIL); + let pad = glow_radius + 3.0; + let x0 = ((head.x < tail.x) ? head.x : tail.x) - pad; + let y0 = ((head.y < tail.y) ? head.y : tail.y) - pad; + let x1 = ((head.x > tail.x) ? head.x : tail.x) + pad; + let y1 = ((head.y > tail.y) ? head.y : tail.y) + pad; + + return { + x: int(x0), + y: int(y0), + w: int(x1 - x0) + 1, + h: int(y1 - y0) + 1 + }; +} + +function packet_reply(p) { + let back = []; + + for (let i = length(p.path) - 1; i >= 0; i--) + push(back, p.path[i]); + + packet_add(back, 'response', 0.0, CYAN); +} + +function pulse_spawn(idx) { + if (!length(nodes)) + return; + + push(pulses, { n: idx ?? rnd_int(0, length(nodes) - 1), age: 0.0 }); + + if (length(pulses) > PULSE_MAX) + shift(pulses); +} + +function pulse_box(p) { + let n = nodes[p.n]; + let r = pulse_r1 + 3.0; + + return { + x: int(n.x - r), + y: int(n.y - r), + w: int(r * 2.0) + 1, + h: int(r * 2.0) + 1 + }; +} + +function key_safe(key) { + return key && index(key, '/') < 0 && index(key, '..') < 0; +} + +function icon_get(key) { + let doc; + + if (exists(icons, key)) + return icons[key]; + + doc = psvg.document_from_file(plymouth.theme_dir() + + '/pictograms/' + key + '.svg'); + + if (!doc) + doc = psvg.document_from_file(ADDON_DIR + '/' + key + '.svg'); + + icons[key] = doc; + + return doc; +} + +function icon_draw(cv, doc, cx, cy, colour) { + let scale = icon_size / doc.height(); + + cv.save(); + cv.translate(cx - doc.width() * scale * 0.5, cy - icon_size * 0.5); + cv.scale(scale, scale); + doc.render(cv, null, colour); + cv.restore(); +} + +function stage_index(key) { + for (let i = 0; i < length(stage_list); i++) + if (stage_list[i].key == key) + return i; + + return -1; +} + +function stage_layout() { + let pitch = unit * 4.2; + let x0 = screen.width * 0.5 - pitch * length(stage_list) * 0.5; + + stage_boxes = []; + + for (let i = 0; i < length(stage_list); i++) + push(stage_boxes, { x: x0 + pitch * (i + 0.5), + y: screen.height * ROW_Y }); +} + +function stage_row_damage() { + plymouth.damage(0, int(screen.height * ROW_Y - icon_size), + screen.width, int(icon_size * 2.0)); +} + +function stage_reach(key) { + let i; + + if (!key_safe(key)) + return; + + i = stage_index(key); + + if (i >= 0) { + if (stage_list[i].on) + return; + + stage_list[i].on = true; + } else { + if (!icon_get(key)) + return; + + push(stage_list, { key: key, on: true }); + stage_layout(); + } + + stage_row_damage(); + + if (length(hubs)) + pulse_spawn(hubs[rnd_int(0, length(hubs) - 1)]); +} + +function stage_add(key) { + if (!key_safe(key) || stage_index(key) >= 0) + return; + + if (!icon_get(key)) + return; + + push(stage_list, { key: key, on: false }); +} + +function stages_init() { + stage_list = []; + stage_layout(); +} + +/* The enabled set only exists once the overlay is mounted, so this runs + * when the bus comes up rather than at setup time. + */ +function services_scan() { + let entries, found, m; + + if (dismantle) + return; + + entries = lsdir(RC_DIR) ?? []; + found = []; + + for (let i = 0; i < length(entries); i++) { + m = match(entries[i], /^S([0-9]+)(.+)$/); + + if (!m) + continue; + + push(found, { order: int(m[1]), name: m[2] }); + } + + sort(found, (a, b) => a.order - b.order); + + for (let i = 0; i < length(found); i++) + stage_add(found[i].name); + + stage_layout(); + stage_row_damage(); +} + +function link_draw(cv, l) { + let trunk = (l.kind == 'trunk'); + let peer = (l.kind == 'peer'); + + cv.set_line_width(unit * (trunk ? 0.17 : (peer ? 0.10 : 0.11))); + cv.set_color({ r: CYAN.r, g: CYAN.g, b: CYAN.b, + a: trunk ? 0.50 : (peer ? 0.16 : 0.30) }); + cv.move_to(nodes[l.a].x, nodes[l.a].y); + + if (peer) + cv.quad_to(l.cx, l.cy, nodes[l.b].x, nodes[l.b].y); + else + cv.line_to(nodes[l.b].x, nodes[l.b].y); + + cv.stroke(); +} + +function node_draw(cv, n) { + if (n.kind == 'router') { + cv.set_color({ r: CYAN.r, g: CYAN.g, b: CYAN.b, a: 0.24 }); + cv.circle(n.x, n.y, n.r * 2.4); + cv.fill(); + } + + cv.set_color((n.kind == 'leaf') ? LEAF_FILL : HUB_FILL); + cv.circle(n.x, n.y, n.r); + cv.fill(); +} + +function step_shown(step) { + for (let i = 0; i < revealed; i++) + if (reveal[i] == step) + return true; + + return false; +} + +function span_box(ax, ay, bx, by, pad) { + let x0 = ((ax < bx) ? ax : bx) - pad; + let y0 = ((ay < by) ? ay : by) - pad; + let x1 = ((ax > bx) ? ax : bx) + pad; + let y1 = ((ay > by) ? ay : by) + pad; + + return { + x: int(x0), + y: int(y0), + w: int(x1 - x0) + 1, + h: int(y1 - y0) + 1 + }; +} + +function span_damage(ax, ay, bx, by, pad) { + let box = span_box(ax, ay, bx, by, pad); + + plymouth.damage(box.x, box.y, box.w, box.h); +} + +function reveal_peer(idx) { + let cv = pvg.canvas(backdrop); + let l = peers[idx]; + + link_draw(cv, l); + span_damage(nodes[l.a].x, nodes[l.a].y, nodes[l.b].x, nodes[l.b].y, + unit * 4.0); +} + +function reveal_node(idx) { + let cv = pvg.canvas(backdrop); + let n = nodes[idx]; + let p = (n.parent != null) ? nodes[n.parent] : n; + + for (let i = 0; i < length(links); i++) + if (links[i].b == idx) + link_draw(cv, links[i]); + + node_draw(cv, n); + span_damage(n.x, n.y, p.x, p.y, n.r * 3.0 + unit); + pulse_spawn(idx); +} + +function reveal_step() { + let step; + + if (revealed >= length(reveal)) + return; + + step = reveal[revealed++]; + + if (step < 0) + return reveal_peer(-1 - step); + + reveal_node(step); +} + +function logo_draw(cv) { + let doc = psvg.document_from_file(plymouth.theme_dir() + '/logo.svg'); + let scale, lw, lh; + + if (!doc) { + plymouth.log('logo.svg missing'); + return; + } + + lh = screen.height * LOGO_HEIGHT; + scale = lh / doc.height(); + lw = doc.width() * scale; + logo_box = { + x: (screen.width - lw) / 2.0, + y: screen.height * BAND_TOP - lh - screen.height * 0.04, + w: lw, + h: lh + }; + + cv.save(); + cv.translate(logo_box.x, logo_box.y); + cv.scale(scale, scale); + doc.render(cv); + cv.restore(); +} + +function ground_draw(cv) { + cv.set_radial_gradient(screen.width * 0.5, screen.height * 0.40, + screen.width * 0.80, + screen.width * 0.5, screen.height * 0.40, 0.0, + 'pad', + [ { offset: 0.0, color: NAVY }, + { offset: 1.0, color: DEEP } ]); + cv.fill_rect(0, 0, screen.width, screen.height); +} + +function backdrop_draw() { + let cv = pvg.canvas(backdrop); + + ground_draw(cv); + logo_draw(cv); +} + +function overlaps(box, x, y, w, h) { + if (!box) + return false; + + return !(box.x > x + w || box.x + box.w < x || + box.y > y + h || box.y + box.h < y); +} + +/* Taking a node off the backdrop means putting back what was under it. + */ +function backdrop_patch(x, y, w, h) { + let cv = pvg.canvas(backdrop); + let n; + + cv.save(); + cv.clip_rect(x, y, w, h); + ground_draw(cv); + + if (overlaps(logo_box, x, y, w, h)) + logo_draw(cv); + + for (let i = 0; i < length(links); i++) + if (step_shown(links[i].b)) + link_draw(cv, links[i]); + + for (let i = 0; i < length(peers); i++) + if (step_shown(-1 - i)) + link_draw(cv, peers[i]); + + for (let i = 0; i < length(nodes); i++) { + n = nodes[i]; + + if (step_shown(i)) + node_draw(cv, n); + } + + cv.restore(); +} + +function hide_step() { + let step, n, p, box; + + if (revealed <= 0) + return; + + step = reveal[--revealed]; + + if (step < 0) { + p = peers[-1 - step]; + box = span_box(nodes[p.a].x, nodes[p.a].y, + nodes[p.b].x, nodes[p.b].y, unit * 4.0); + } else { + n = nodes[step]; + p = (n.parent != null) ? nodes[n.parent] : n; + box = span_box(n.x, n.y, p.x, p.y, n.r * 3.0 + unit); + } + + backdrop_patch(box.x, box.y, box.w, box.h); + plymouth.damage(box.x, box.y, box.w, box.h); + + if (step >= 0) + pulse_spawn(step); +} + +function setup(mode) { + let list = plymouth.screens(); + + if (!length(list)) { + plymouth.log('no pixel display'); + return; + } + + screen = list[0]; + rng_state = seed_pick(); + + unit = screen.height / 90.0; + icon_size = unit * 2.5; + packet_radius = unit * 0.27; + glow_radius = unit * 0.67; + pulse_r0 = unit * 0.45; + pulse_r1 = unit * 2.70; + + dismantle = (mode != 'boot-up'); + + backdrop = pvg.surface(screen.width, screen.height); + topo_build(); + stages_init(); + backdrop_draw(); + + if (dismantle) + while (revealed < length(reveal)) + reveal_step(); + + plymouth.damage(0, 0, screen.width, screen.height); +} + +function packets_step() { + let p, was, now, x0, y0, x1, y1; + + for (let i = length(packets) - 1; i >= 0; i--) { + p = packets[i]; + was = p.box ?? packet_box(p); + + if (p.dwell > 0.0) { + p.dwell -= 1.0; + } else { + p.age += 1.0; + + if (p.age >= HOP_FRAMES) { + p.age = 0.0; + p.hop++; + p.dwell = HOP_DWELL; + } + } + + if (p.hop > length(p.path) - 2) { + plymouth.damage(was.x, was.y, was.w, was.h); + pulse_spawn(p.path[length(p.path) - 1]); + + if (p.kind == 'request') + packet_reply(p); + + splice(packets, i, 1); + continue; + } + + now = packet_box(p); + p.box = now; + p.pts = []; + + for (let k = 0; k <= TRAIL; k++) + push(p.pts, packet_at(p, k)); + + x0 = (was.x < now.x) ? was.x : now.x; + y0 = (was.y < now.y) ? was.y : now.y; + x1 = (was.x + was.w > now.x + now.w) ? was.x + was.w + : now.x + now.w; + y1 = (was.y + was.h > now.y + now.h) ? was.y + was.h + : now.y + now.h; + + plymouth.damage(x0, y0, x1 - x0, y1 - y0); + } +} + +function frame(t) { + let now; + + if (!backdrop) + return; + + if (!primed) { + primed = true; + plymouth.damage(0, 0, screen.width, screen.height); + } + + packets_step(); + + if (dismantle) { + if (t >= reveal_at) { + reveal_at = t + REVEAL_SPACING; + hide_step(); + } + } else if (revealed < length(reveal)) { + if (t >= reveal_at) { + reveal_at = t + REVEAL_SPACING; + reveal_step(); + } + } else if (on_bus && ++flow_timer >= FLOW_GAP) { + flow_timer = 0; + flow_spawn(); + } + + for (let i = length(pulses) - 1; i >= 0; i--) { + now = pulse_box(pulses[i]); + plymouth.damage(now.x, now.y, now.w, now.h); + pulses[i].age += 1.0; + + if (pulses[i].age > PULSE_LIFE) + splice(pulses, i, 1); + } +} + +function pulses_paint(cv, x, y, w, h) { + let n, f, r; + + for (let i = 0; i < length(pulses); i++) { + n = nodes[pulses[i].n]; + f = pulses[i].age / PULSE_LIFE; + r = pulse_r0 + (pulse_r1 - pulse_r0) * f; + + if (n.x + r < x || n.x - r > x + w) + continue; + if (n.y + r < y || n.y - r > y + h) + continue; + + cv.set_color({ r: CYAN.r, g: CYAN.g, b: CYAN.b, + a: 0.55 * (1.0 - f) }); + cv.set_line_width(unit * 0.15); + cv.circle(n.x, n.y, r); + cv.stroke(); + } +} + +function packet_paint(cv, p, x, y, w, h) { + let c = p.colour; + let pt, f; + + for (let k = TRAIL; k >= 0; k--) { + pt = p.pts[k]; + + if (pt.x + glow_radius < x || pt.x - glow_radius > x + w) + continue; + if (pt.y + glow_radius < y || pt.y - glow_radius > y + h) + continue; + + if (k > 0) { + f = 1.0 - k / (TRAIL + 1.0); + cv.set_color({ r: c.r, g: c.g, b: c.b, a: 0.32 * f }); + cv.circle(pt.x, pt.y, packet_radius * f); + cv.fill(); + continue; + } + + cv.set_color({ r: c.r, g: c.g, b: c.b, a: 0.28 }); + cv.circle(pt.x, pt.y, glow_radius); + cv.fill(); + cv.set_color(c); + cv.circle(pt.x, pt.y, (p.kind == 'stream') + ? packet_radius * 0.7 : packet_radius); + cv.fill(); + } +} + +function stages_paint(cv, x, y, w, h) { + let b, on, doc; + + for (let i = 0; i < length(stage_boxes); i++) { + b = stage_boxes[i]; + on = stage_list[i].on; + + if (b.x + icon_size < x || b.x - icon_size > x + w) + continue; + if (b.y + icon_size < y || b.y - icon_size > y + h) + continue; + + if (on) { + cv.set_color({ r: CYAN.r, g: CYAN.g, b: CYAN.b, + a: 0.85 }); + cv.circle(b.x, b.y, icon_size * 0.72); + cv.fill(); + } + + doc = icons[stage_list[i].key]; + + if (doc) + icon_draw(cv, doc, b.x, b.y, on ? WHITE : ICON_DIM); + } +} + +function paint(cv, x, y, w, h) { + let b; + + if (!backdrop) + return; + + cv.set_texture(backdrop, 'plain', 1.0); + cv.fill_rect(x, y, w, h); + + pulses_paint(cv, x, y, w, h); + + for (let i = 0; i < length(packets); i++) { + b = packets[i].box; + + if (!packets[i].pts || !b) + continue; + if (b.x + b.w < x || b.x > x + w) + continue; + if (b.y + b.h < y || b.y > y + h) + continue; + + packet_paint(cv, packets[i], x, y, w, h); + } + + stages_paint(cv, x, y, w, h); +} + +function object_stage(path) { + if (path == 'service' || path == 'system' || path == 'container' || + substr(path, 0, 8) == 'hotplug.') + return 'procd'; + if (path == 'block') + return 'blockd'; + if (substr(path, 0, 7) == 'network') + return 'network'; + if (substr(path, 0, 7) == 'dnsmasq') + return 'dnsmasq'; + if (path == 'dhcp') + return 'odhcpd'; + if (substr(path, 0, 7) == 'hostapd' || path == 'wpa_supplicant' || + path == 'iwinfo') + return 'wpad'; + if (path == 'uhttpd' || substr(path, 0, 4) == 'luci') + return 'uhttpd'; + + return null; +} + +function endpoint_for(path) { + let idx; + + if (exists(endpoints, path)) + return endpoints[path]; + + if (!length(clients)) + return null; + + idx = clients[next_endpoint % length(clients)]; + next_endpoint++; + endpoints[path] = idx; + + return idx; +} + +function flow_endpoint(path) { + let idx = endpoint_for(path); + let p; + + if (idx == null || revealed < length(reveal)) + return; + + p = path_between(idx, core); + + if (p) + packet_add(p, 'request', 0.0, WHITE); +} + +function ubus_event(detail) { + let payload; + + if (detail.type == 'ubus.object.add') { + payload = detail.path; + + if (!payload) { + try { + payload = json(detail.data ?? '{}')?.path; + } catch (e) { + payload = null; + } + } + + if (payload) { + stage_reach(object_stage(payload) ?? payload); + flow_endpoint(payload); + } + + return; + } + + flow_endpoint(detail.path ?? detail.type ?? 'bus'); +} + +function event(name, detail) { + if (name == 'ubus-connected') { + on_bus = true; + + while (revealed < length(reveal)) + reveal_step(); + + services_scan(); + return; + } + + if (name == 'ubus' && type(detail) == 'object') + return ubus_event(detail); + + if (name == 'status' && substr(detail ?? '', 0, 8) == 'service:') + stage_reach(substr(detail, 8)); + +} + +return { + setup: setup, + frame: frame, + paint: paint, + event: event +}; diff --git a/utils/plymouth-theme-openwrt-demo/files/pictograms/blockd.svg b/utils/plymouth-theme-openwrt-demo/files/pictograms/blockd.svg new file mode 100644 index 0000000..6e0ab8c --- /dev/null +++ b/utils/plymouth-theme-openwrt-demo/files/pictograms/blockd.svg @@ -0,0 +1 @@ + diff --git a/utils/plymouth-theme-openwrt-demo/files/pictograms/dnsmasq.svg b/utils/plymouth-theme-openwrt-demo/files/pictograms/dnsmasq.svg new file mode 100644 index 0000000..79e182b --- /dev/null +++ b/utils/plymouth-theme-openwrt-demo/files/pictograms/dnsmasq.svg @@ -0,0 +1 @@ + diff --git a/utils/plymouth-theme-openwrt-demo/files/pictograms/network.svg b/utils/plymouth-theme-openwrt-demo/files/pictograms/network.svg new file mode 100644 index 0000000..905afb5 --- /dev/null +++ b/utils/plymouth-theme-openwrt-demo/files/pictograms/network.svg @@ -0,0 +1 @@ + diff --git a/utils/plymouth-theme-openwrt-demo/files/pictograms/sysntpd.svg b/utils/plymouth-theme-openwrt-demo/files/pictograms/sysntpd.svg new file mode 100644 index 0000000..8e832f7 --- /dev/null +++ b/utils/plymouth-theme-openwrt-demo/files/pictograms/sysntpd.svg @@ -0,0 +1 @@ + diff --git a/utils/plymouth-theme-openwrt-demo/files/pictograms/uhttpd.svg b/utils/plymouth-theme-openwrt-demo/files/pictograms/uhttpd.svg new file mode 100644 index 0000000..a4019a5 --- /dev/null +++ b/utils/plymouth-theme-openwrt-demo/files/pictograms/uhttpd.svg @@ -0,0 +1 @@ + diff --git a/utils/plymouth-theme-openwrt-demo/files/pictograms/wpad.svg b/utils/plymouth-theme-openwrt-demo/files/pictograms/wpad.svg new file mode 100644 index 0000000..5465427 --- /dev/null +++ b/utils/plymouth-theme-openwrt-demo/files/pictograms/wpad.svg @@ -0,0 +1 @@ +