From 9497add010df9d3ce751a35e4de8b60e0a341465 Mon Sep 17 00:00:00 2001 From: Ruediger Klaehn Date: Wed, 24 Jun 2026 11:41:50 +0300 Subject: [PATCH 1/2] Fix missing popup background in some animations needed for dark mode --- public/animations/publish-relay-dht.svg | 7 ++++++- public/animations/publish-relay.svg | 8 ++++++-- scripts/publish-relay-dht.gen.py | 10 +++++++++- scripts/publish-relay.gen.py | 11 +++++++++-- 4 files changed, 30 insertions(+), 6 deletions(-) diff --git a/public/animations/publish-relay-dht.svg b/public/animations/publish-relay-dht.svg index cdf67db1..7deb08e3 100644 --- a/public/animations/publish-relay-dht.svg +++ b/public/animations/publish-relay-dht.svg @@ -64,7 +64,12 @@ 1a9c… Alice - 8e2b… is at relay us-east + + + + + 8e2b… is at relay us-east + diff --git a/public/animations/publish-relay.svg b/public/animations/publish-relay.svg index ca079a79..c2c57a64 100644 --- a/public/animations/publish-relay.svg +++ b/public/animations/publish-relay.svg @@ -79,8 +79,12 @@ 1a9c… Alice - - 8e2b… is at relay us-east + + + + + 8e2b… is at relay us-east + diff --git a/scripts/publish-relay-dht.gen.py b/scripts/publish-relay-dht.gen.py index 8ae2c082..7998114b 100644 --- a/scripts/publish-relay-dht.gen.py +++ b/scripts/publish-relay-dht.gen.py @@ -222,13 +222,21 @@ def answer_dot(wire_id, color_down, launch, reach, back): alice_pts = [(0, 0), (ALICE_IN, 0), (ALICE_IN+0.6, 1), (OUT0, 1), (OUT1, 0), (CYCLE, 0)] result_pts = [(0, 0), (FIRST_VALID, 0), (FIRST_VALID+0.5, 1), (OUT0, 1), (OUT1, 0), (CYCLE, 0)] +RES_LABEL = "8e2b… is at relay us-east" +res_w = len(RES_LABEL) * 11 * 0.6 # Space Mono advance ≈ 0.6em at 11px +res_xr, res_yb = ax0 - 14, ay0 + 46 # right edge / baseline of the result text alice = f''' {anim_opacity(alice_pts)} {phone(ax0, ay0, "1a9c…", iphone=True)} Alice - 8e2b… is at relay us-east{anim_opacity(result_pts)}''' + + + {anim_opacity(result_pts)} + + {RES_LABEL} + ''' # ============================ wires + dots ============================ pub_wire_pts = [(0, 0), (0.6, 0), (1.0, 1), (PUB_DONE, 1), (PUB_DONE+0.5, 0), (CYCLE, 0)] diff --git a/scripts/publish-relay.gen.py b/scripts/publish-relay.gen.py index af52b000..01656fda 100644 --- a/scripts/publish-relay.gen.py +++ b/scripts/publish-relay.gen.py @@ -186,14 +186,21 @@ def doc_packet(wire_id, color, opa_pts, mot_pts): acx = ax0+24 alice_pts = [(0, 0), (3.8, 0), (4.4, 1), (13.8, 1), (14.2, 0), (CYCLE, 0)] result_pts = [(0, 0), (8.0, 0), (8.5, 1), (13.8, 1), (14.2, 0), (CYCLE, 0)] +RES_LABEL = "8e2b… is at relay us-east" +res_w = len(RES_LABEL) * 11 * 0.6 # Space Mono advance ≈ 0.6em at 11px +res_xr, res_yb = ax0 - 14, ay0 + 46 # right edge / baseline of the result text alice = f''' {anim_opacity(alice_pts)} {phone(ax0, ay0, "1a9c…", iphone=True)} Alice - - 8e2b… is at relay us-east{anim_opacity(result_pts)}''' + + + {anim_opacity(result_pts)} + + {RES_LABEL} + ''' # ============================ wires ============================ put_d = arc(bcx, by0, sx+18, sy+sh, k=0.18) # Bob -> server From d52bb00a06fc580412581590306325dec4825b34 Mon Sep 17 00:00:00 2001 From: Ruediger Klaehn Date: Wed, 24 Jun 2026 12:36:56 +0300 Subject: [PATCH 2/2] Add new svgs - layer stack and mdns address lookup --- public/animations/layer-stack.svg | 57 ++++ public/animations/mdns-address-lookup.svg | 144 +++++++++ scripts/layer-stack.gen.py | 160 ++++++++++ scripts/mdns-address-lookup.gen.py | 353 ++++++++++++++++++++++ 4 files changed, 714 insertions(+) create mode 100644 public/animations/layer-stack.svg create mode 100644 public/animations/mdns-address-lookup.svg create mode 100644 scripts/layer-stack.gen.py create mode 100644 scripts/mdns-address-lookup.gen.py diff --git a/public/animations/layer-stack.svg b/public/animations/layer-stack.svg new file mode 100644 index 00000000..8970f86e --- /dev/null +++ b/public/animations/layer-stack.svg @@ -0,0 +1,57 @@ + + + + + + Your application + iroh — embedded library + + + + Protocols + blobs · gossip · yours + + Router + dispatches connections by ALPN + + Endpoint + identity · address lookup · NAT · relay + + QUIC + TLS 1.3 + encryption · auth · stream mux + + Transport + UDP and relay by default · swappable + + + + + + + + + + + + Ethernet + LAN · UDP + + + + + + + + + + Wi-Fi + UDP + + + + + + + Tor + onion routing + diff --git a/public/animations/mdns-address-lookup.svg b/public/animations/mdns-address-lookup.svg new file mode 100644 index 00000000..90d882c5 --- /dev/null +++ b/public/animations/mdns-address-lookup.svg @@ -0,0 +1,144 @@ + + + + + mDNS multicast + 224.0.0.251:5353 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + iroh + a4f7… + 192.168.0.3 + + + + + + iroh + 8e2b… + 192.168.0.5 + + + + + + + + + + + + + + + + + + iroh + c1d9… + 192.168.0.7 + + + + + + local knowledge + 8e2b… 192.168.0.5 + c1d9… 192.168.0.7 + + + + local knowledge + a4f7… 192.168.0.3 + c1d9… 192.168.0.7 + + + + local knowledge + a4f7… 192.168.0.3 + 8e2b… 192.168.0.5 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + mDNS announce + a4f7… @ 192.168.0.3 + + + + + mDNS announce + 8e2b… @ 192.168.0.5 + + + + + mDNS announce + c1d9… @ 192.168.0.7 + + + + + + + diff --git a/scripts/layer-stack.gen.py b/scripts/layer-stack.gen.py new file mode 100644 index 00000000..1cda6315 --- /dev/null +++ b/scripts/layer-stack.gen.py @@ -0,0 +1,160 @@ +#!/usr/bin/env python3 +"""Generator for layer-stack.svg (the "How the pieces stack together" figure). + +A *static* architecture diagram in the same style as the animated how-iroh-works +SVGs (Space Mono, light boxes on a transparent canvas, mid-tone accents so it +reads in light and dark mode). + +The framing: iroh is a library *embedded in your application*. So the app is one +big container box, and the iroh components (Protocols, Router, Endpoint, QUIC, +Transport) are borderless boxes nested inside it. Below the app, the physical +media the transport runs over fan out: an RJ45/ethernet plug and a Wi-Fi router +(the two default UDP paths) plus a Tor onion (a swappable alternative). + +No animation: a layer stack reads best held still. All text is either dark ink on +a light box or a mid-tone accent, so nothing vanishes on a dark background. + +To change it, edit this script and run: python3 layer-stack.gen.py +It writes ../public/animations/layer-stack.svg. +The page embeds this via /; the +intrinsic aspect ratio comes from the SVG viewBox (VB_W x VB_H). +""" +import os + +HERE = os.path.dirname(os.path.abspath(__file__)) +OUT_PATH = os.path.normpath(os.path.join( + HERE, "..", "public", "animations", "layer-stack.svg")) + +MONO = "'Space Mono', monospace" +INDIGO, AMBER, GRAY, INK, GOLD = "#6366f1", "#d97706", "#888", "#111", "#eab308" +BLUE = "#2563eb" # connection-blue, matching the other diagrams +TOR = "#7e4798" +BOX_FILL, BOX_STROKE, CABLE = "#e5e7eb", "#9ca3af", "#cbd0d8" + +VB_W, VB_H = 680, 465 + +# ---- app container ---- +APP_X, APP_Y, APP_W = 70, 24, 540 +APP_CX = APP_X + APP_W / 2 # 340, the spine + +# ---- iroh box: one rounded box, layers touch with square internal dividers ---- +IX, IW = APP_X + 18, APP_W - 36 # 88, 504 +ITOP, IBH = APP_Y + 48, 46 +IROH = [ + ("Protocols", "blobs · gossip · yours"), + ("Router", "dispatches connections by ALPN"), + ("Endpoint", "identity · address lookup · NAT · relay"), + ("QUIC + TLS 1.3", "encryption · auth · stream mux"), + ("Transport", "UDP and relay by default · swappable"), +] +IROH_H = len(IROH) * IBH +TRANSPORT_BOTTOM = ITOP + IROH_H # bottom edge of the Transport layer (= iroh box bottom) +APP_BOTTOM = ITOP + IROH_H + 16 +APP_H = APP_BOTTOM - APP_Y + +# ---- transport media row ---- +LAN_CX, WIFI_CX, TOR_CX = 200, 340, 480 +EXIT_XS = (APP_CX - 28, APP_CX, APP_CX + 28) # 3 separate wire exits, close together at the Transport box +ICON_TOP = APP_BOTTOM + 52 +LABEL_Y = ICON_TOP + 64 + + +def iroh_box(): + """One rounded box; the layers touch, separated by thin square dividers.""" + parts = [f' '] + for i, (title, subtitle) in enumerate(IROH): + y = ITOP + i * IBH + if i: # divider above every layer but the first + parts.append(f' ') + parts.append(f' {title}') + parts.append(f' {subtitle}') + return "\n".join(parts) + + +# ============================ transport media icons ============================ + +def rj45(cx, top): + """RJ45 / CAT6 ethernet plug — 8 gold contacts, latch, boot, cable.""" + pins = "".join( + f'' + for j in range(8)) + return ( + f' \n' + f' \n' + f' \n' + f' \n' + f' \n' + f' {pins}\n' + f' Ethernet\n' + f' LAN · UDP' + ) + + +def router(cx, cy): + """Wi-Fi router — body with two L-shaped side antennas, matching the + home-router design used in the other how-iroh-works diagrams.""" + bw, bh = 46, 26 + bx, by = cx - bw / 2, cy - bh / 2 + return ( + f' \n' + f' \n' + f' \n' + f' \n' + f' \n' + f' \n' + f' \n' + f' \n' + f' \n' + f' Wi-Fi\n' + f' UDP' + ) + + +def tor(cx, cy): + """Tor onion: concentric layers + a little sprout.""" + return ( + f' \n' + f' \n' + f' \n' + f' \n' + f' \n' + f' \n' + f' Tor\n' + f' onion routing' + ) + + +# ============================ assemble ============================ +# Three separate wires: vertical out of the Transport box, rounded across, vertical into each device. +_sy, _ay = TRANSPORT_BOTTOM, ICON_TOP - 2 +_k = (_ay - _sy) * 0.55 # control-handle length → vertical tangents at both ends +fan = "\n".join( + f' ' + for ex, dx in zip(EXIT_XS, (LAN_CX, WIFI_CX, TOR_CX)) +) + +svg = f''' + + + + + Your application + iroh — embedded library + + +{iroh_box()} + + +{fan} +{rj45(LAN_CX, ICON_TOP)} +{router(WIFI_CX, ICON_TOP + 13)} +{tor(TOR_CX, ICON_TOP + 13)} + +''' + +open(OUT_PATH, "w").write(svg) +print(f"wrote {len(svg)} bytes -> {OUT_PATH}") diff --git a/scripts/mdns-address-lookup.gen.py b/scripts/mdns-address-lookup.gen.py new file mode 100644 index 00000000..c221b479 --- /dev/null +++ b/scripts/mdns-address-lookup.gen.py @@ -0,0 +1,353 @@ +#!/usr/bin/env python3 +"""Generator for mdns-address-lookup.svg ("Local address lookup over mDNS"). + +The purely-local discovery method: three devices on the *same* network (an +iPhone, an Android phone and an embedded device) behind one router. There is no +server and no relay — each device periodically multicasts an mDNS announcement +(its EndpointId + local address) to 224.0.0.251:5353. The router fans the packet +out to the other devices, and each device's "knows" panel grows as the +announcements arrive. After every device has announced once, all three know each +other and a direct LAN connection is possible. + +The devices are deliberately unnamed: they are identified only by their short +key id (gold key glyph) and their local address, the same things mDNS carries. + +One master SMIL loop (CYCLE seconds), constant packet speed (SPEED px/s) so the +multicast dots reach the nearer device first and the farther device a moment +later. SMIL only (no CSS keyframes) so the animation survives the production CSP +(`default-src 'none'`) and Mintlify's embedding on docs.iroh.computer. + +Edit this script and run: python3 mdns-address-lookup.gen.py +Writes ../public/animations/mdns-address-lookup.svg. +The page embeds this via /; the +intrinsic aspect ratio comes from the SVG viewBox (VB_W x VB_H). +""" + +import math +import os + +HERE = os.path.dirname(os.path.abspath(__file__)) +OUT_PATH = os.path.normpath(os.path.join( + HERE, "..", "public", "animations", "mdns-address-lookup.svg")) + +MONO = "'Space Mono', monospace" +INDIGO, AMBER, GRAY, INK, BLUE = "#6366f1", "#d97706", "#888", "#111", "#2563eb" +GOLD = "#eab308" + +# ============================ geometry ============================ +CX = [150, 410, 670] # device centers (iPhone, Android, embedded) +KIND = ["iphone", "android", "embedded"] +IDS = ["a4f7…", "8e2b…", "c1d9…"] +ADDRS = ["192.168.0.3", "192.168.0.5", "192.168.0.7"] +DEV_TOP = 250 # device body top +PH = 92 # device body height +JX, JY = 410, 115 # router box bottom center +SPEED = 200.0 # px/second — every multicast dot travels at this speed + +ENTRY = [JX - 16, JX, JX + 16] # distinct router entry x per device (3 separate wires) +DV, DV2 = 60, 72 # bezier control offsets: vertical at the device / at the router +JH = (JX, JY - 10) # hidden hand-off point inside the router box (where dots split) + + +def cubic_len(p0, p1, p2, p3, n=64): + """Numeric arc length of a cubic bezier (for constant-speed packet timing).""" + total, prev = 0.0, p0 + for i in range(1, n + 1): + t = i / n + u = 1 - t + x = u*u*u*p0[0] + 3*u*u*t*p1[0] + 3*u*t*t*p2[0] + t*t*t*p3[0] + y = u*u*u*p0[1] + 3*u*u*t*p1[1] + 3*u*t*t*p2[1] + t*t*t*p3[1] + total += math.hypot(x - prev[0], y - prev[1]) + prev = (x, y) + return total + + +def leg_pts(i): + """Control points for device i's curved leg: leaves the device vertically, + enters the router vertically at the device's own offset entry point.""" + cx, ex = CX[i], ENTRY[i] + return ((cx, DEV_TOP), (cx, DEV_TOP - DV), (ex, JY + DV2), (ex, JY)) + + +def leg_d(i): + p0, p1, p2, p3 = leg_pts(i) + return f"M {p0[0]} {p0[1]} C {p1[0]} {p1[1]} {p2[0]} {p2[1]} {p3[0]} {p3[1]}" + + +def packet_d(a, r): + """Multicast dot path: up the announcer's leg to its entry, across to the + hidden hand-off point, then down the receiver's leg.""" + pa, pr = leg_pts(a), leg_pts(r) + return (f"M {pa[0][0]} {pa[0][1]} C {pa[1][0]} {pa[1][1]} {pa[2][0]} {pa[2][1]} {pa[3][0]} {pa[3][1]} " + f"L {JH[0]} {JH[1]} L {pr[3][0]} {pr[3][1]} " + f"C {pr[2][0]} {pr[2][1]} {pr[1][0]} {pr[1][1]} {pr[0][0]} {pr[0][1]}") + + +def leglen(i): + return cubic_len(*leg_pts(i)) + + +def total_len(a, r): + return (leglen(a) + + math.hypot(ENTRY[a] - JH[0], JY - JH[1]) + + math.hypot(JH[0] - ENTRY[r], JH[1] - JY) + + leglen(r)) + + +# ============================ timeline ============================ +# Round-robin: each device announces once; the other two learn it. +rounds = [] # (announcer, launch, receivers, {receiver: arrival}) +launch = 1.8 +for ai in range(3): + recs = [i for i in range(3) if i != ai] + arrs = {ri: launch + total_len(ai, ri) / SPEED for ri in recs} + rounds.append((ai, launch, recs, arrs)) + launch = max(arrs.values()) + 1.0 + +LAST_ARR = max(max(a.values()) for *_, a in rounds) +HOLD = 4.0 +OUT0 = LAST_ARR + HOLD # persistent rows fade out before the loop restarts +OUT1 = OUT0 + 0.7 +CYCLE = OUT1 + 0.6 + +# What each device learns and when (sorted so its panel fills top-down). +learned = {i: [] for i in range(3)} +for ai, _, recs, arrs in rounds: + for ri in recs: + learned[ri].append((ai, arrs[ri] + 0.15)) +for i in learned: + learned[i].sort(key=lambda t: t[1]) + + +# ============================ SMIL helpers ============================ +def anim_opacity(points): + times = ";".join(f"{t/CYCLE:.4f}" for t, _ in points) + vals = ";".join(str(v) for _, v in points) + return (f'') + + +def anim_motion(wire_id, points): + times = ";".join(f"{t/CYCLE:.4f}" for t, _ in points) + kp = ";".join(str(k) for _, k in points) + return (f'') + + +# ============================ key glyphs ============================ +def _key_paths(xl, cy, s): + """The little gold key glyph (bow + shaft + teeth) starting at left edge xl.""" + rb, rh = s * 0.34, s * 0.15 + bx = xl + rb + hs = s * 0.18 + xr = xl + s * 1.3 + top, bot = cy - hs / 2, cy + hs / 2 + tw = s * 0.13 + return ( + f'' + f'' + f'' + f'' + ) + + +def key_label(cx, y, text, size, color): + """Centered: gold key glyph + monospace label (the iroh identity on a device).""" + s = size + w, gap = s * 1.3, s * 0.3 + xl = cx - (w + gap + s * 0.6 * len(text)) / 2 + return (_key_paths(xl, y - s * 0.30, s) + + f'{text}') + + +def key_row(x, y, text, size, color=INK): + """Left-aligned: gold key glyph at x + monospace label (a learned-peer row).""" + s = size + w, gap = s * 1.3, s * 0.3 + return (_key_paths(x, y - s * 0.30, s) + + f'{text}') + + +# ============================ device drawings ============================ +def iphone_screen(px0, py0, pw, ph): + sx0, sx1 = px0 + 3, px0 + pw - 3 + sy0, sy1 = py0 + 3, py0 + ph - 3 + cx = px0 + pw / 2 + no, ni, nd, r = 7, 4, 5, 6 + ny = sy0 + nd + return ( + f'' + ) + + +def phone(cx, keytext, iphone=False): + pw, ph = 50, PH + px0 = cx - pw / 2 + if iphone: + screen = iphone_screen(px0, DEV_TOP, pw, ph) + else: + screen = (f'' + f'') + return f''' + {screen} + + iroh + {key_label(cx, DEV_TOP+60, keytext, 8, AMBER)}''' + + +def embedded(cx, keytext): + bw, bh = 60, PH + bx0 = cx - bw / 2 + n = 6 + pins = [] + for i in range(n): + yy = DEV_TOP + 14 + i * (bh - 28) / (n - 1) + pins.append(f'') + pins.append(f'') + pins_s = "\n ".join(pins) + return f''' {pins_s} + + + + iroh + {key_label(cx, DEV_TOP+60, keytext, 8, AMBER)}''' + + +def device(i): + if KIND[i] == "iphone": + body = phone(CX[i], IDS[i], iphone=True) + elif KIND[i] == "android": + body = phone(CX[i], IDS[i], iphone=False) + else: + body = embedded(CX[i], IDS[i]) + return f''' +{body} + {ADDRS[i]} + ''' + + +def router(cx, cy): + """Home router with two side antennas — the exact design used in + hole-punching-lan.svg (router_lan), minus its gateway-IP label (the network + is already named by the multicast caption above).""" + bx, by, bw, bh = cx - 30, cy - 20, 60, 40 + return f''' + + + + + + + ''' + + +# ============================ knowledge panels ============================ +def panel(i): + cx = CX[i] + w, x, y, h = 168, CX[i] - 84, 364, 64 + rows = [f' local knowledge'] + for j, (ai, lt) in enumerate(learned[i]): + rowy = y + 38 + j * 20 + pts = [(0, 0), (lt, 0), (lt + 0.4, 1), (OUT0, 1), (OUT1, 0), (CYCLE, 0)] + rows.append( + f' {anim_opacity(pts)}' + f'{key_row(x + 14, rowy, f"{IDS[ai]} {ADDRS[ai]}", 10)}') + body = "\n".join(rows) + return f''' + +{body} + ''' + + +# ============================ multicast packets ============================ +wires, packets = [], [] +for ai, lnch, recs, arrs in rounds: + for ri in recs: + wid = f"mw{ai}{ri}" + wires.append(f' ') + arr = arrs[ri] + opa = [(0, 0), (lnch, 0), (lnch + 0.12, 1), (arr - 0.1, 1), (arr + 0.05, 0), (CYCLE, 0)] + mot = [(0, 0), (lnch, 0), (arr, 1), (CYCLE, 1)] + packets.append(f''' + + {anim_opacity(opa)} + {anim_motion(wid, mot)} + ''') +wires = "\n".join(wires) +packets = "\n".join(packets) + +# ============================ announce callouts ============================ +callouts = [] +for ai, lnch, recs, arrs in rounds: + t0, t1 = lnch - 0.3, max(arrs.values()) + 0.4 + pts = [(0, 0), (t0 - 0.3, 0), (t0, 1), (t1, 1), (t1 + 0.3, 0), (CYCLE, 0)] + w, h = 176, 44 + x, y = CX[ai] - w / 2, 198 + callouts.append(f''' + {anim_opacity(pts)} + + mDNS announce + {IDS[ai]} @ {ADDRS[ai]} + ''') +callouts = "\n".join(callouts) + +# ============================ static backdrop ============================ +legs = "\n".join( + f' ' + for i in range(3)) +devices = "\n".join(device(i) for i in range(3)) +panels = "\n".join(panel(i) for i in range(3)) + +VB_X, VB_Y, VB_W, VB_H = 0, 26, 820, 439 +svg = f''' + + + + mDNS multicast + 224.0.0.251:5353 + + +{legs} + + +{wires} + +{router(JX, 95)} + + +{devices} + + +{panels} + + +{packets} + + +{callouts} + + + + + + +''' + +open(OUT_PATH, "w").write(svg) +print(f"wrote {len(svg)} bytes -> {OUT_PATH} (CYCLE={CYCLE:.1f}s, last arrival at {LAST_ARR:.1f}s)")