diff --git a/public/animations/connect-by-key.svg b/public/animations/connect-by-key.svg new file mode 100644 index 00000000..5a5f4740 --- /dev/null +++ b/public/animations/connect-by-key.svg @@ -0,0 +1,80 @@ + + + + + + + + + + + 10.0.0.42 + + 192.168.1.7 + + 203.0.113.15 + + 172.20.4.88 + + 192.168.10.55 + + + + + + iroh + a4f7c0… + + + + + + + iroh + 8e2b1d… + + + Alice + Bob + + + + + + connect + + + + + + + + + + diff --git a/public/animations/embedding-phone.svg b/public/animations/embedding-phone.svg new file mode 100644 index 00000000..df4cb511 --- /dev/null +++ b/public/animations/embedding-phone.svg @@ -0,0 +1,121 @@ + + + + + iOS app (Swift) +iroh via iroh-ffi + + + + swift app + + iroh + + + + + Android app (Kotlin) +iroh via iroh-ffi + + + + + kotlin app + + iroh + + + + + Native desktop (C++) +iroh via iroh-c-ffi + + + + c++ app + + iroh + + + + + + + Web app (JavaScript) +iroh compiled to WASM + + + + + + + + + js app + + iroh + + + + + Server daemon (Rust) +iroh crate directly + + + + + + + + + + + + rust daemon + + iroh + + + + + Embedded firmware (Rust) +iroh crate + + + + + + + + + + + + + + + + + + firmware + + iroh + + diff --git a/public/animations/endpoint-startup.svg b/public/animations/endpoint-startup.svg new file mode 100644 index 00000000..480f0913 --- /dev/null +++ b/public/animations/endpoint-startup.svg @@ -0,0 +1,130 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + us-west + + + + + + + + + + + + 52.10.18.7 + + + + + us-east + + + + + + + + + + + + 44.208.61.5 + + + + + eu-west + + + + + + + + + + + + 18.196.142.9 + + + + + + + + + + + + + + + iroh + 8e2b… + + + + 73.118.42.9 + + + + + + + + + + + + us-east: 19 ms + us-west: 71 ms + eu-west: 102 ms + + + + + + + + + + diff --git a/public/animations/hole-punching-lan.svg b/public/animations/hole-punching-lan.svg new file mode 100644 index 00000000..c62dfaac --- /dev/null +++ b/public/animations/hole-punching-lan.svg @@ -0,0 +1,156 @@ + + + + + + + + + + + + + + + + + + + + + + relay + + + + + + + + + + 192.168.0.1 + + + + + + + + iroh + 1a9c… + Alice + + + EndpointId: 1a9c… + Addr: 192.168.0.3:2104 + Addr: 4.9.8.2:2104 + relay: us-east + + + + + + + + + iroh + 8e2b… + Bob + + + EndpointId: 8e2b… + Addr: 192.168.0.5:4153 + Addr: 4.9.8.2:4153 + relay: us-east + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + direct (LAN) + + + + + + + ADD_ADDRESS 192.168.0.5:4153 + ADD_ADDRESS 4.9.8.2:4153 + + + + + + + + Bob + Candidate: 192.168.0.5:4153 + Candidate: 4.9.8.2:4153 + + + + + REACH_OUT192.168.0.3:2104 + REACH_OUT4.9.8.2:2104 + + + + + + + + Alice + Candidate: 192.168.0.3:2104 + Candidate: 4.9.8.2:2104 + + + + + PATH_CHALLENGE + 192.168.0.5:4153 + + + + + PATH_RESPONSE + PATH_CHALLENGE + 192.168.0.3:2104 + + + + + PATH_RESPONSE + + + + + + + diff --git a/public/animations/hole-punching.svg b/public/animations/hole-punching.svg new file mode 100644 index 00000000..d4757f12 --- /dev/null +++ b/public/animations/hole-punching.svg @@ -0,0 +1,257 @@ + + + + + + + + + + + + + + + + + + + + + + + + + relay + + + + 8.3.1.9 + + + + + + + 10.0.0.1 + + + + 4.9.8.2 + + + + + + + 192.168.0.1 + + + + + + + + iroh + 1a9c… + Alice + + + EndpointId: 1a9c… + Addr: 10.0.0.3:2104 + Addr: 8.3.1.9:2104 + relay: us-west + + + + + + + + + iroh + 8e2b… + Bob + + + EndpointId: 8e2b… + Addr: 192.168.0.3:4153 + Addr: 4.9.8.2:4153 + relay: us-east + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + not routable + + + + + + not routable + + + + + + no mapping + + + + + + + + 4.9.8.2:4153 → + 10.0.0.3:2104 + + + + + + + + + + + + + + + + + + + 8.3.1.9:2104 → + 192.168.0.3:4153 + + + + + + + + + + + + + + + + + + ADD_ADDRESS 192.168.0.3:4153 + ADD_ADDRESS 4.9.8.2:4153 + + + + + + + + Bob + Candidate: 192.168.0.3:4153 + Candidate: 4.9.8.2:4153 + + + + + REACH_OUT10.0.0.3:2104 + REACH_OUT8.3.1.9:2104 + + + + + + + + Alice + Candidate: 10.0.0.3:2104 + Candidate: 8.3.1.9:2104 + I need to reach out + + + + + PATH_CHALLENGE + 192.168.0.3:4153 + + + + + PATH_CHALLENGE + 10.0.0.3:2104 + + + + + PATH_CHALLENGE + 4.9.8.2:4153 + + + + + PATH_CHALLENGE + 8.3.1.9:2104 + + + + + PATH_RESPONSE + PATH_CHALLENGE + + + + + PATH_RESPONSE + + + + + + + diff --git a/public/animations/publish-relay-dht.svg b/public/animations/publish-relay-dht.svg new file mode 100644 index 00000000..cdf67db1 --- /dev/null +++ b/public/animations/publish-relay-dht.svg @@ -0,0 +1,149 @@ + + + + + + + + + + + + + Mainline DHT + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + iroh + 8e2b… + Bob + + + NodeId: 8e2b… + Home relay: us-east + + + + + + + + + iroh + 1a9c… + Alice + + 8e2b… is at relay us-east + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + DHT put + 8e2b… + Relay: us-east + + + + + + + DHT get + 8e2b… + + + + + + + ;; ANSWER SECTION: + TXT "relay=https://us-east" + + + + + + + diff --git a/public/animations/publish-relay.svg b/public/animations/publish-relay.svg new file mode 100644 index 00000000..ca079a79 --- /dev/null +++ b/public/animations/publish-relay.svg @@ -0,0 +1,125 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + dns.iroh.link + + + + + + 1f3a… → eu-west + 7c0e… → us-west + b48d… → us-east + 2a91… → eu-west + 8e2b… → us-east + + + + + + + + + iroh + 8e2b… + Bob + + + NodeId: 8e2b… + Home relay: us-east + + + + + + + + + iroh + 1a9c… + Alice + + + 8e2b… is at relay us-east + + + + + + + + + + + + + + + + + HTTPS PUT + Relay: us-east + Signed by: 8e2b… + + + + + + + + + DNS LOOKUP + TXT _iroh.8e2b….dns.iroh.link + + + + ;; ANSWER SECTION: + TXT "relay=https://us-east" + + + + + + + + diff --git a/public/animations/routing-moves.svg b/public/animations/routing-moves.svg new file mode 100644 index 00000000..357ba067 --- /dev/null +++ b/public/animations/routing-moves.svg @@ -0,0 +1,148 @@ + + + + + + + + + + + 10.0.0.42 + + + + iroh + a4f7c0… + Alice + + + + + + 10.0.0.7 + 192.168.1.7 + 172.16.0.7 + + + + + iroh + 8e2b1d… + Bob + + + + + + + + + + + + + + + + + + + + + + + + home router + + 203.0.113.1 + + + + + + + 10.0.0.1 + + + + + + + mobile network + + 198.51.100.1 + + + + + + 192.168.1.1 + + + + + + + satellite internet + + 100.64.10.1 + + + 172.16.0.1 + + + + + + + + + + diff --git a/scripts/connect-by-key.gen.py b/scripts/connect-by-key.gen.py new file mode 100644 index 00000000..c91561a9 --- /dev/null +++ b/scripts/connect-by-key.gen.py @@ -0,0 +1,233 @@ +#!/usr/bin/env python3 +"""Generator for connect-by-key.svg. + +Alice and Bob each get an iroh endpoint identified by a stable cryptographic +key (gold key glyph + short hex prefix). Bob's IP changes 4 times during the +loop — strikethrough on the old IP as it rotates — while the connection +between the keys (unidirectional → bidirectional after the handshake) stays +up. The point: iroh dials keys, not addresses. + +14s loop. SMIL-driven (/), single clock — no CSS +keyframes. SMIL keeps everything (opacity here, motion-along-paths elsewhere) +on one clock, co-located with the geometry, so the file animates under any host +or embedding. These diagrams are also consumed cross-site by +docs.iroh.computer, where self-contained SMIL is what survives. + +To change it, edit this script and run: python3 connect-by-key.gen.py +It writes ../public/animations/connect-by-key.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", "connect-by-key.svg")) + +MONO = "'Space Mono', monospace" +INDIGO, AMBER, GRAY, RED = "#6366f1", "#d97706", "#888", "#dc2626" +GOLD = "#eab308" +CYCLE = 14 # seconds +VB_W, VB_H = 800, 320 + + +def fmt_kt(pct): + """Percentage (0-100) of the loop -> SMIL keyTimes fraction string.""" + if pct <= 0: + return "0" + if pct >= 100: + return "1" + return f"{pct / 100:.4f}".rstrip("0").rstrip(".") + + +def opacity_anim(stops): + """SMIL on opacity. `stops` is a list of (percent, opacity) + matching what a CSS @keyframes block would express; the first stop's value + is also the element's resting opacity (set it as the opacity attribute).""" + keytimes = ";".join(fmt_kt(p) for p, _ in stops) + values = ";".join(str(v) for _, v in stops) + return (f'') + + +def key_label(cx, y, text, size, color): + """Tiny drawn gold key glyph + monospace text, replacing the 🔑 emoji.""" + s = size + w = s * 1.3 + gap = s * 0.3 + xl = cx - (w + gap + s * 0.6 * len(text)) / 2 + cy = y - s * 0.30 + rb, rh = s * 0.34, s * 0.15 + bx = xl + rb + hs = s * 0.18 + xr = xl + w + top, bot = cy - hs / 2, cy + hs / 2 + tw = s * 0.13 + return ( + f'' + f'' + f'' + f'' + f'{text}' + ) + + +# Bob's IP rotation (text + strikethrough). Each (text, line_x0, line_x1). +BOB_IPS = [ + ("192.168.1.7", 560, 640), + ("203.0.113.15", 555, 645), + ("172.20.4.88", 560, 640), + ("192.168.10.55", None, None), # final IP — no strikethrough +] + +# Keyframes per IP slot (text shows; line strikes through about halfway). Times in %. +# 14s loop. Slot 1 ~ 0-2s, slot 2 ~ 2-4s, slot 3 ~ 4-8s (struck at 7s), slot 4 ~ 8-14s. +TEXT_KEYFRAMES = [ + # (visible-from%, visible-to%) for each IP + (0, 13), + (15, 28), + (30, 56), + (58, 100), +] +LINE_KEYFRAMES = [ + # (struck-from%, struck-to%) — None means no strike + (8, 13), + (22, 28), + (51, 56), + None, +] + + +def text_stops(i): + """Opacity stops for IP-text slot i (1-indexed), matching the old CSS.""" + vis_from, vis_to = TEXT_KEYFRAMES[i - 1] + if i == 1: + # Start visible, fade out at end of slot 1. + return [(0, 1), (vis_to, 1), (vis_to + 2, 0), (100, 0)] + if i < len(TEXT_KEYFRAMES): + return [(0, 0), (vis_from - 2, 0), (vis_from, 1), (vis_to, 1), (vis_to + 2, 0), (100, 0)] + # Final IP — stay visible to end. + return [(0, 0), (vis_from - 2, 0), (vis_from, 1), (100, 1)] + + +def line_stops(i): + """Opacity stops for the strikethrough line of slot i (1-indexed).""" + sf, st = LINE_KEYFRAMES[i - 1] + return [(0, 0), (sf - 2, 0), (sf, 1), (st, 1), (st + 2, 0), (100, 0)] + + +# Keys appear at ~5s and stay; "connect" arrow shows 5–6s; bidirectional from 6s. +KEYS_ANIM = opacity_anim([(0, 0), (35, 0), (37, 1), (100, 1)]) +UNI_ANIM = opacity_anim([(0, 0), (35, 0), (37, 1), (42, 1), (44, 0), (100, 0)]) +BI_ANIM = opacity_anim([(0, 0), (42, 0), (44, 1), (100, 1)]) + + +def alice_iphone(): + """Alice's iPhone (large, with notch).""" + return ( + f' \n' + f' \n' + f' \n' + f' iroh\n' + f' {KEYS_ANIM}{key_label(200, 208, "a4f7c0…", 11, AMBER)}' + ) + + +def bob_android(): + """Bob's Android phone (large).""" + return ( + f' \n' + f' \n' + f' \n' + f' \n' + f' iroh\n' + f' {KEYS_ANIM}{key_label(600, 208, "8e2b1d…", 11, AMBER)}' + ) + + +# ============================ assemble ============================ +ip_rows = [] +for i, (text, lx0, lx1) in enumerate(BOB_IPS, start=1): + t_stops = text_stops(i) + init = t_stops[0][1] + ip_rows.append( + f' {text}{opacity_anim(t_stops)}') + if lx0 is not None: + ip_rows.append( + f' {opacity_anim(line_stops(i))}') + +svg = f''' + + + + + + + + + + 10.0.0.42 + +{chr(10).join(ip_rows)} + + +{alice_iphone()} + + +{bob_android()} + + + Alice + Bob + + + + {UNI_ANIM} + + connect + + + + {BI_ANIM} + + + + + + +''' + +open(OUT_PATH, "w").write(svg) +print(f"wrote {len(svg)} bytes -> {OUT_PATH}") diff --git a/scripts/endpoint-startup.gen.py b/scripts/endpoint-startup.gen.py new file mode 100644 index 00000000..ed50c08f --- /dev/null +++ b/scripts/endpoint-startup.gen.py @@ -0,0 +1,273 @@ +#!/usr/bin/env python3 +"""Generator for endpoint-startup.svg (the "Endpoint startup" figure in how-iroh-works). + +This SVG is generated, not hand-edited. To change it, edit this script and run: + + python3 endpoint-startup.gen.py + +It reads the projected land outline from endpoint-startup.land.txt (Natural Earth +110m coastline, plate carrée with standard parallel 45°, clipped to a US + Europe +window) and writes ../public/animations/endpoint-startup.svg. + +NOTE: this lives in scripts/, not in src/app/, because anything under the Next.js +app router directory gets bundled by webpack (which chokes on .py files). + +The animation: the three relay wires fade in at 2s, then an equal-speed probe +packet round-trips Bob -> relay -> Bob on each wire. Because speed is constant and +distance differs, us-east returns first, then us-west, then eu-west. A readout to +the right of the phone gains a line each time a packet returns. +""" + +import math +import os + +CYCLE = 14.0 # one loop; everything resets afterwards + +HERE = os.path.dirname(os.path.abspath(__file__)) +LAND_PATH = os.path.join(HERE, "endpoint-startup.land.txt") +OUT_PATH = os.path.normpath(os.path.join( + HERE, "..", + "public", "animations", "endpoint-startup.svg")) + +def key_label(cx, y, text, size, color): + """A tiny drawn gold key (bow + shaft + teeth) + monospace label, centered on cx + (replaces the 🔑 emoji, which no pure-vector renderer can draw).""" + s = size + gold = "#eab308" + w = s * 1.3 # key glyph width + gap = s * 0.3 + xl = cx - (w + gap + s * 0.6 * len(text)) / 2 + cy = y - s * 0.30 + rb, rh = s * 0.34, s * 0.15 # bow outer / hole radius + bx = xl + rb # bow center x + hs = s * 0.18 # shaft thickness + xr = xl + w # right end + top, bot = cy - hs / 2, cy + hs / 2 + tw = s * 0.13 # tooth width + mono = "'Space Mono', monospace" + return ( + f'' + f'' + f'' + f'' + f'{text}' + ) + + +land = open(LAND_PATH).read() + +# ---- projection: plate carrée, standard parallel 45°, US + Europe window ---- +lon0, lon1 = -130, 42 +lat0, lat1 = 16, 72 +cosp = math.cos(math.radians(45)) +scale = 820.0 / ((lon1 - lon0) * cosp) +def proj(lon, lat): + return ((lon - lon0) * cosp * scale, (lat1 - lat) * scale) + +# city coordinates (lon, lat) +seattle = (-122.33, 47.61) +delaware = (-75.55, 39.16) # Dover/Wilmington area +frankfurt = (8.68, 50.11) +florida = (-80.2, 25.8) # Miami area + + +def relay(cx, cy, label, ip): + # pizzabox 1U server icon centered horizontally on cx, sitting above the location dot + bx, by, bw, bh = cx-23, cy-30, 46, 15 + s = [] + s.append(f' ') + s.append(f' ') + s.append(f' {label}') + # connector from box bottom to dot + s.append(f' ') + # location dot + s.append(f' ') + # pizzabox body + s.append(f' ') + # front bezel split line + s.append(f' ') + # LEDs + s.append(f' ') + s.append(f' ') + # vent slits on the right + for i in range(5): + vx = bx+22+i*4 + s.append(f' ') + # public IPv4 below the location dot + s.append(f' {ip}') + s.append(f' ') + return "\n".join(s) + + +def arc(x1, y1, x2, y2, k=0.16): + # great-circle-style curved wire: quadratic bezier bowing "up" (toward the top). + # Returns (path_d, arc_length). Larger k => more northern bow. + mx, my = (x1+x2)/2, (y1+y2)/2 + dx, dy = x2-x1, y2-y1 + L = math.hypot(dx, dy) + px, py = -dy/L, dx/L + if py > 0: # always bow upward + px, py = -px, -py + off = k*L + cx, cy = mx+px*off, my+py*off + d = f"M {x1:.0f} {y1:.0f} Q {cx:.0f} {cy:.0f} {x2:.0f} {y2:.0f}" + def q(t, a, b, c): return (1-t)**2*a + 2*(1-t)*t*b + t**2*c + N, length, prev = 60, 0.0, (x1, y1) + for i in range(1, N+1): + t = i/N + cur = (q(t, x1, cx, x2), q(t, y1, cy, y2)) + length += math.hypot(cur[0]-prev[0], cur[1]-prev[1]) + prev = cur + return d, length + + +# ---- Bob: small Android phone in the Atlantic, leader line to his FL location ---- +bx0, by0 = 300, 244 # phone top-left +bw, bh = 48, 88 +bcx, bcy = bx0+bw/2, by0+bh/2 +flx, fly = proj(*florida) +bob = f''' + + + + + + + + + + + + iroh + {key_label(bcx, by0+58, "8e2b…", 8, "#d97706")} + ''' + +uwx, uwy = round(proj(*seattle)[0]), round(proj(*seattle)[1]) +uex, uey = round(proj(*delaware)[0]), round(proj(*delaware)[1]) +ewx, ewy = round(proj(*frankfurt)[0]), round(proj(*frankfurt)[1]) +bx_, by_ = round(flx), round(fly) # Bob's location dot + +# made-up public IPv4 addresses for the relays +ip_uw, ip_ue, ip_ew = "52.10.18.7", "44.208.61.5", "18.196.142.9" +# Bob's public IP — reflected back by the first relay to respond +ip_bob = "73.118.42.9" + +# wire paths (us-east kept nearly straight); keep lengths for equal-speed packets +d_uw, L_uw = arc(bx_, by_, uwx, uwy, k=0.16) +d_ue, L_ue = arc(bx_, by_, uex, uey, k=0.05) +d_ew, L_ew = arc(bx_, by_, ewx, ewy, k=0.10) + +wires = f''' + + + + + + ''' + +# Equal-speed probe packets: round-trip Bob -> relay -> Bob. +# Duration is proportional to path length so speed is constant => us-east returns first. +SPEED = 120.0 # px per second (slow enough that the short us-east hop is still watchable) +START = 3.0 # packets launch after the wires have faded in +def packet(wid, length): + dur = 2*length/SPEED # there and back + t0 = START / CYCLE + t1 = (START + dur*0.04) / CYCLE + tmid = (START + dur*0.5) / CYCLE + t2 = (START + dur*0.96) / CYCLE + t3 = (START + dur) / CYCLE + opa = f'values="0;0;1;1;1;0;0" keyTimes="0;{t0:.4f};{t1:.4f};{tmid:.4f};{t2:.4f};{t3:.4f};1"' + mot = f'keyTimes="0;{t0:.4f};{tmid:.4f};{t3:.4f};1" keyPoints="0;0;1;0;0"' + return f''' + + + + + ''' + +packets = f''' + +{packet("w-uw", L_uw)} +{packet("w-ue", L_ue)} +{packet("w-ew", L_ew)} + ''' + +# Latency readout, right of the phone: one line appears each time a packet returns. +def ret_time(length): + return START + 2*length/SPEED # packet is back at Bob at START + round-trip dur +def readline(y, label, ms, begin): + t0 = begin / CYCLE + t1 = (begin + 0.4) / CYCLE + return (f' ' + f'{label}: {ms} ms' + f'') +readout = f''' + + + + +{readline(268, "us-east", 19, ret_time(L_ue))} +{readline(294, "us-west", 71, ret_time(L_uw))} +{readline(320, "eu-west", 102, ret_time(L_ew))} + + + + ''' + +# When the last response is in, us-east wins: overlay its wire in connection-blue, +# then keep it gently pulsing to show the persistent home-relay connection. +_rt = ret_time(L_ew) +home = f''' + + + ''' + +# The endpoint's public IP appears just below the phone on the first response. +_pt0 = ret_time(L_ue) / CYCLE +_pt1 = (ret_time(L_ue) + 0.4) / CYCLE +phone_ip = f''' + {ip_bob}''' + +# Cropped viewport: trim the empty Arctic (top) and eastern Europe (right) so the +# relevant US <-> western-Europe band fills the frame. The land path is unchanged; +# this just clips it. The page embeds this via /; +# the intrinsic aspect ratio comes from the SVG viewBox (VB_W x VB_H). +VB_X, VB_Y, VB_W, VB_H = 0, 92, 705, 280 +svg = f''' + + + + +{wires} + +{packets} + +{relay(uwx, uwy, "us-west", ip_uw)} + +{relay(uex, uey, "us-east", ip_ue)} + +{relay(ewx, ewy, "eu-west", ip_ew)} + +{bob} + +{phone_ip} + +{home} + +{readout} + + + + + + +''' + +open(OUT_PATH, "w").write(svg) +print(f"wrote {len(svg)} bytes -> {OUT_PATH}") diff --git a/scripts/endpoint-startup.land.txt b/scripts/endpoint-startup.land.txt new file mode 100644 index 00000000..a746ad2e --- /dev/null +++ b/scripts/endpoint-startup.land.txt @@ -0,0 +1 @@ +M 329,417 L 324,417 L 326,416 L 326,413 L 329,412 L 329,417 Z M 307,363 L 306,364 L 299,364 L 299,362 L 300,361 L 304,361 L 307,363 Z M 253,365 L 252,366 L 249,365 L 246,363 L 247,361 L 253,361 L 256,365 L 253,365 Z M 274,351 L 278,353 L 278,351 L 282,351 L 286,353 L 287,355 L 290,355 L 290,357 L 292,357 L 294,360 L 292,363 L 290,361 L 286,361 L 284,363 L 283,361 L 281,362 L 279,367 L 278,364 L 275,363 L 270,363 L 267,364 L 265,362 L 265,360 L 273,361 L 275,360 L 273,357 L 273,354 L 270,353 L 271,351 L 274,351 Z M 240,332 L 242,334 L 246,334 L 255,342 L 259,344 L 259,346 L 263,346 L 266,349 L 266,350 L 249,352 L 252,348 L 250,346 L 247,346 L 244,340 L 242,340 L 236,337 L 230,336 L 228,334 L 230,333 L 225,332 L 222,336 L 220,336 L 219,338 L 215,338 L 220,332 L 228,329 L 235,330 L 240,332 Z M 250,325 L 249,326 L 246,320 L 247,315 L 248,316 L 250,325 Z M 249,306 L 244,307 L 243,305 L 249,304 L 249,306 Z M 253,306 L 252,311 L 249,303 L 253,306 Z M 785,245 L 781,248 L 782,250 L 777,252 L 775,251 L 774,249 L 785,245 Z M 733,245 L 735,247 L 743,247 L 743,248 L 745,247 L 745,249 L 738,250 L 738,249 L 732,248 L 733,245 Z M 694,228 L 692,239 L 679,232 L 680,228 L 685,229 L 694,228 Z M 664,208 L 667,212 L 666,221 L 664,221 L 662,223 L 660,221 L 659,209 L 661,210 L 664,208 Z M 665,201 L 664,206 L 662,205 L 661,201 L 661,198 L 665,195 L 665,201 Z M 316,172 L 324,172 L 320,176 L 314,173 L 313,170 L 315,168 L 316,172 Z M 325,154 L 317,152 L 312,149 L 320,150 L 325,153 L 325,154 Z M 31,158 L 29,159 L 21,156 L 14,150 L 9,148 L 7,145 L 8,143 L 20,146 L 24,152 L 29,155 L 31,158 Z M 352,144 L 349,150 L 352,147 L 355,149 L 354,151 L 358,153 L 360,151 L 365,153 L 363,158 L 367,157 L 369,165 L 367,171 L 361,170 L 363,164 L 361,163 L 356,169 L 353,169 L 356,166 L 352,164 L 337,164 L 336,162 L 339,160 L 337,158 L 341,154 L 346,143 L 349,140 L 353,137 L 356,138 L 352,144 Z M -13,121 L -8,121 L -10,128 L -6,134 L -8,134 L -15,125 L -15,120 L -13,121 Z M 587,133 L 579,137 L 572,136 L 576,129 L 574,122 L 584,114 L 588,113 L 593,118 L 590,122 L 591,127 L 587,133 Z M 680,111 L 677,116 L 672,112 L 672,109 L 679,107 L 680,111 Z M 605,90 L 600,97 L 610,97 L 609,102 L 605,108 L 610,108 L 614,117 L 618,118 L 622,129 L 628,130 L 627,134 L 625,136 L 627,140 L 622,143 L 616,143 L 608,145 L 606,144 L 603,147 L 598,146 L 595,149 L 592,147 L 599,140 L 603,139 L 596,138 L 595,135 L 600,133 L 597,129 L 598,125 L 605,125 L 606,121 L 602,117 L 597,116 L 596,114 L 597,111 L 596,109 L 593,113 L 593,106 L 590,103 L 592,96 L 596,90 L 605,90 Z M 242,66 L 240,70 L 238,69 L 237,67 L 239,65 L 242,66 Z M 229,63 L 224,66 L 220,66 L 219,64 L 223,61 L 229,61 L 229,63 Z M 214,43 L 215,46 L 217,45 L 231,51 L 231,54 L 234,54 L 238,56 L 234,58 L 226,56 L 224,53 L 212,60 L 210,56 L 204,57 L 208,54 L 210,42 L 214,43 Z M 551,37 L 549,42 L 555,46 L 549,51 L 531,57 L 511,54 L 516,51 L 506,48 L 514,47 L 514,45 L 504,43 L 507,39 L 514,38 L 522,42 L 529,39 L 535,40 L 543,37 L 551,37 Z M 258,33 L 253,33 L 252,30 L 254,26 L 258,25 L 262,27 L 261,31 L 258,33 Z M 164,20 L 161,22 L 154,20 L 150,21 L 144,18 L 152,13 L 159,16 L 164,20 Z M 188,17 L 188,24 L 194,18 L 200,23 L 199,28 L 203,32 L 208,27 L 212,22 L 212,14 L 226,16 L 232,19 L 233,22 L 229,26 L 232,30 L 232,33 L 222,38 L 216,39 L 211,37 L 203,49 L 198,53 L 191,54 L 187,57 L 187,61 L 181,62 L 176,67 L 170,75 L 169,80 L 168,88 L 175,89 L 180,101 L 186,99 L 195,102 L 203,108 L 214,113 L 228,114 L 227,119 L 228,126 L 232,134 L 239,140 L 242,138 L 245,131 L 243,120 L 239,117 L 247,114 L 252,109 L 255,104 L 254,100 L 251,94 L 245,89 L 251,82 L 247,65 L 251,64 L 264,66 L 268,64 L 278,71 L 279,73 L 288,74 L 289,88 L 294,89 L 297,93 L 304,89 L 312,79 L 327,101 L 325,106 L 336,113 L 343,115 L 346,117 L 348,123 L 352,124 L 354,126 L 354,134 L 347,139 L 340,141 L 334,147 L 326,148 L 315,146 L 303,147 L 299,152 L 293,155 L 281,170 L 285,169 L 292,160 L 302,154 L 310,153 L 314,157 L 309,161 L 312,174 L 319,177 L 326,176 L 331,169 L 332,173 L 335,176 L 329,180 L 313,187 L 308,192 L 305,191 L 304,186 L 313,180 L 300,181 L 301,183 L 285,191 L 282,196 L 282,200 L 284,204 L 286,204 L 285,201 L 287,203 L 286,205 L 272,208 L 268,209 L 275,208 L 277,209 L 270,212 L 267,212 L 267,211 L 266,213 L 267,213 L 266,218 L 263,223 L 260,219 L 262,227 L 258,235 L 259,230 L 256,227 L 256,221 L 255,224 L 256,229 L 253,228 L 256,230 L 256,236 L 258,237 L 259,246 L 256,251 L 251,253 L 248,257 L 245,257 L 243,260 L 242,262 L 237,266 L 232,273 L 232,283 L 238,304 L 237,315 L 235,316 L 233,316 L 232,313 L 230,311 L 225,297 L 226,293 L 224,289 L 219,283 L 214,286 L 208,280 L 195,281 L 193,282 L 194,288 L 190,289 L 187,289 L 183,285 L 179,286 L 175,285 L 172,285 L 168,287 L 164,292 L 159,295 L 156,301 L 157,311 L 154,322 L 153,334 L 156,346 L 161,355 L 163,358 L 168,360 L 170,363 L 181,359 L 187,355 L 189,344 L 198,341 L 205,340 L 206,342 L 206,345 L 202,353 L 203,354 L 201,362 L 199,361 L 200,362 L 199,374 L 196,378 L 200,380 L 201,378 L 205,379 L 210,378 L 215,378 L 222,382 L 223,384 L 223,389 L 222,394 L 222,402 L 220,411 L 228,425 L 230,425 L 232,426 L 239,423 L 240,421 L 246,422 L 251,427 L 253,427 L 257,422 L 259,422 L 260,414 L 263,411 L 266,411 L 266,409 L 270,410 L 278,402 L 281,404 L 280,408 L 277,408 L 278,411 L 278,415 L 276,419 L 278,424 L 280,424 L 281,419 L 280,417 L 279,411 L 285,409 L 285,406 L 286,403 L 288,408 L 291,408 L 294,412 L 295,414 L 304,414 L 307,417 L 310,417 L 313,415 L 313,414 L 325,413 L 321,415 L 322,418 L 326,419 L 330,422 L 331,428 L 333,427 L 338,432 L 341,436 L 341,439 L 343,440 L 347,445 L 353,447 L 354,445 L 357,445 L 363,447 L 368,449 L 373,455 L 373,457 L 375,457 L 379,473 L 382,474 L 382,478 L 378,484 L 380,486 L 388,487 L 388,494 L 392,489 L 406,496 L 408,500 L 407,504 L 413,502 L 422,505 L 429,505 L 436,510 L 442,518 L 446,520 L 450,520 L 452,522 L 454,535 L 452,546 L 443,560 L 437,573 L 435,573 L 434,578 L 434,591 L 433,606 L 431,609 L 430,618 L 425,626 L 425,633 L 421,636 L 420,640 L 414,640 L 407,643 L 404,646 L 398,648 L 393,653 L 389,660 L 389,669 L 387,679 L 383,682 L 378,694 L 371,703 L 368,709 L 363,717 L 358,721 L 354,720 L 352,720 L 347,718 L 344,718 L 341,714 L 341,718 L 347,723 L 346,728 L 349,731 L 349,734 L 344,743 L 337,746 L 328,748 L 323,747 L 323,760 L 321,762 L 316,763 L 311,761 L 309,762 L 310,769 L 313,771 L 316,769 L 317,772 L 313,774 L 309,779 L 307,789 L 303,789 L 299,793 L 298,798 L 302,803 L 307,804 L 305,810 L 300,814 L 296,822 L 292,824 L 290,827 L 292,834 L 295,838 L 289,838 L 282,842 L 281,848 L 274,846 L 262,838 L 261,834 L 262,830 L 260,825 L 259,814 L 261,807 L 266,802 L 259,800 L 264,794 L 265,783 L 271,785 L 273,771 L 270,769 L 268,778 L 265,777 L 269,755 L 271,750 L 269,736 L 271,736 L 279,704 L 278,694 L 280,688 L 279,680 L 282,672 L 286,630 L 285,619 L 284,609 L 279,605 L 279,603 L 270,596 L 257,584 L 255,579 L 256,577 L 240,534 L 232,527 L 234,524 L 232,517 L 233,513 L 239,503 L 238,500 L 237,504 L 234,501 L 235,499 L 234,493 L 236,492 L 238,483 L 238,480 L 244,476 L 243,474 L 245,474 L 246,468 L 248,467 L 252,459 L 250,458 L 251,454 L 250,440 L 247,435 L 246,431 L 247,429 L 243,425 L 240,425 L 236,431 L 238,435 L 234,437 L 233,433 L 233,434 L 231,433 L 230,431 L 225,430 L 225,431 L 222,428 L 221,424 L 216,421 L 215,417 L 214,421 L 211,418 L 211,413 L 210,412 L 211,411 L 202,398 L 202,397 L 203,398 L 203,396 L 201,395 L 201,397 L 198,397 L 188,392 L 185,392 L 180,387 L 175,380 L 168,376 L 159,380 L 139,370 L 134,365 L 126,362 L 124,359 L 119,355 L 117,351 L 116,348 L 117,347 L 118,341 L 114,332 L 103,316 L 99,313 L 98,311 L 99,307 L 93,302 L 92,298 L 90,297 L 85,290 L 80,275 L 73,271 L 72,274 L 73,282 L 88,306 L 92,322 L 95,322 L 98,328 L 96,332 L 91,324 L 85,319 L 84,310 L 79,305 L 78,306 L 74,302 L 71,299 L 74,298 L 75,296 L 76,293 L 69,286 L 61,263 L 57,259 L 55,258 L 55,256 L 45,252 L 44,248 L 40,242 L 36,231 L 30,223 L 29,217 L 27,214 L 28,202 L 26,197 L 28,191 L 29,179 L 28,169 L 25,161 L 26,159 L 33,162 L 35,168 L 37,166 L 34,155 L 21,146 L 12,143 L 10,137 L 10,133 L 4,130 L 3,124 L -2,119 L -3,116 L -9,111 L -11,105 L -17,100 L -19,94 L -32,93 L -37,91 L -47,84 L -60,80 L -67,81 L -82,75 L -87,76 L -86,81 L -104,87 L -104,83 L -102,76 L -97,74 L -98,72 L -104,76 L -108,80 L -115,85 L -111,89 L -116,93 L -125,98 L -127,101 L -134,105 L -136,108 L -141,111 L -144,110 L -158,117 L -166,119 L -167,118 L -152,109 L -146,108 L -132,97 L -131,92 L -129,88 L -134,90 L -136,89 L -139,92 L -142,88 L -143,91 L -145,87 L -149,90 L -152,90 L -152,83 L -155,81 L -161,82 L -169,77 L -169,74 L -172,71 L -165,60 L -161,59 L -158,60 L -154,57 L -150,58 L -147,56 L -148,52 L -150,51 L -147,49 L -155,50 L -156,52 L -160,50 L -167,51 L -174,49 L -176,47 L -182,43 L -164,37 L -160,37 L -161,40 L -151,40 L -155,35 L -161,33 L -169,27 L -175,25 L -173,21 L -164,21 L -158,18 L -157,14 L -152,11 L -138,7 L -134,8 L -127,4 L -120,6 L -116,9 L -114,7 L -106,8 L -106,9 L -99,11 L -94,10 L -71,14 L -65,12 L -31,21 L -21,16 L -14,17 L 1,12 L 4,15 L 8,13 L 9,10 L 12,11 L 20,17 L 27,12 L 27,18 L 33,16 L 35,14 L 41,15 L 59,20 L 70,21 L 77,24 L 70,28 L 79,29 L 96,27 L 101,31 L 106,28 L 101,25 L 104,23 L 114,22 L 122,27 L 128,26 L 136,29 L 150,28 L 150,24 L 154,23 L 162,25 L 161,32 L 165,26 L 168,27 L 171,20 L 160,13 L 160,5 L 166,1 L 172,2 L 177,5 L 183,12 L 179,16 L 188,17 Z M 75,-8 L 73,-4 L 84,-6 L 90,-3 L 96,-6 L 100,-4 L 104,2 L 106,-0 L 103,-7 L 107,-8 L 112,-7 L 117,-5 L 122,7 L 138,13 L 138,16 L 130,17 L 133,19 L 131,22 L 115,19 L 100,22 L 80,23 L 77,20 L 70,18 L 66,19 L 60,14 L 84,11 L 75,9 L 58,10 L 55,7 L 66,5 L 59,5 L 51,3 L 58,-5 L 71,-9 L 75,-8 Z M 122,-10 L 117,-5 L 110,-10 L 118,-11 L 122,-10 Z M 256,-7 L 256,-6 L 241,-5 L 234,-9 L 234,-11 L 248,-11 L 256,-7 Z M 207,-8 L 211,-4 L 215,-9 L 227,-12 L 236,-5 L 235,-0 L 244,-2 L 249,-5 L 266,2 L 267,5 L 275,3 L 280,7 L 292,10 L 296,13 L 300,19 L 292,22 L 303,27 L 311,28 L 317,34 L 325,35 L 323,39 L 315,47 L 309,44 L 302,38 L 296,39 L 295,43 L 308,51 L 311,58 L 310,63 L 292,56 L 304,66 L 304,68 L 291,65 L 281,61 L 275,58 L 277,56 L 263,49 L 263,51 L 249,52 L 245,50 L 248,45 L 267,44 L 266,42 L 273,32 L 272,29 L 270,27 L 263,23 L 253,21 L 256,19 L 251,15 L 247,15 L 243,12 L 241,14 L 232,15 L 197,11 L 193,8 L 198,5 L 191,5 L 190,-2 L 193,-8 L 198,-10 L 211,-12 L 207,-8 Z M 141,-12 L 147,-11 L 156,-12 L 157,-10 L 152,-7 L 160,-4 L 159,2 L 151,5 L 146,4 L 131,-3 L 131,-6 L 141,-5 L 136,-9 L 141,-12 Z M 175,-5 L 170,-0 L 165,-0 L 162,-6 L 162,-10 L 164,-13 L 169,-14 L 188,-13 L 181,-7 L 175,-5 Z M 45,4 L 33,7 L 30,4 L 19,1 L 29,-11 L 24,-15 L 40,-17 L 59,-15 L 69,-10 L 51,-4 L 45,1 L 45,4 Z M 173,-20 L 171,-17 L 158,-20 L 161,-23 L 168,-25 L 173,-20 Z M 150,-32 L 154,-29 L 154,-25 L 152,-20 L 144,-20 L 139,-21 L 139,-25 L 131,-24 L 131,-29 L 136,-29 L 143,-31 L 150,-31 L 150,-32 Z M 104,-28 L 106,-26 L 115,-27 L 116,-23 L 113,-20 L 97,-19 L 85,-16 L 78,-16 L 77,-18 L 87,-21 L 65,-21 L 59,-22 L 65,-28 L 70,-30 L 83,-28 L 91,-24 L 100,-23 L 93,-30 L 97,-32 L 102,-32 L 104,-28 Z M 168,-34 L 174,-32 L 183,-32 L 187,-30 L 186,-27 L 195,-24 L 208,-23 L 216,-25 L 233,-25 L 238,-22 L 239,-20 L 229,-16 L 223,-17 L 200,-16 L 179,-19 L 177,-26 L 172,-29 L 162,-30 L 157,-32 L 159,-35 L 168,-34 Z M 66,-38 L 65,-33 L 61,-31 L 41,-26 L 34,-28 L 52,-37 L 66,-38 Z M 1130,-34 L 1131,-30 L 1135,-32 L 1149,-32 L 1160,-28 L 1164,-26 L 1163,-22 L 1141,-15 L 1154,-12 L 1159,-13 L 1161,-9 L 1163,-11 L 1171,-12 L 1186,-11 L 1187,-8 L 1207,-7 L 1207,-12 L 1225,-11 L 1233,-7 L 1235,-3 L 1232,0 L 1238,5 L 1246,8 L 1250,1 L 1258,4 L 1266,2 L 1275,4 L 1279,3 L 1287,3 L 1283,-3 L 1289,-6 L 1333,-1 L 1337,3 L 1349,8 L 1368,7 L 1378,8 L 1382,10 L 1381,15 L 1387,17 L 1393,16 L 1411,17 L 1420,16 L 1428,22 L 1434,20 L 1430,16 L 1432,13 L 1448,15 L 1458,14 L 1471,18 L 1478,20 L 1478,47 L 1472,50 L 1466,50 L 1470,53 L 1475,61 L 1475,64 L 1474,65 L 1465,64 L 1448,70 L 1434,79 L 1432,82 L 1425,77 L 1413,82 L 1410,80 L 1406,83 L 1399,82 L 1398,86 L 1392,93 L 1392,95 L 1398,97 L 1397,107 L 1393,107 L 1391,113 L 1393,116 L 1384,119 L 1383,127 L 1376,128 L 1374,135 L 1367,142 L 1361,112 L 1363,103 L 1367,99 L 1367,96 L 1375,94 L 1391,79 L 1400,73 L 1404,64 L 1398,64 L 1395,70 L 1383,77 L 1379,69 L 1367,71 L 1355,83 L 1359,87 L 1341,89 L 1341,84 L 1334,83 L 1328,87 L 1313,85 L 1298,87 L 1264,116 L 1271,117 L 1274,122 L 1278,123 L 1282,120 L 1287,120 L 1294,127 L 1294,133 L 1290,140 L 1288,159 L 1280,169 L 1279,173 L 1263,193 L 1256,197 L 1253,197 L 1250,194 L 1244,199 L 1243,201 L 1241,200 L 1239,203 L 1238,205 L 1238,210 L 1228,217 L 1227,221 L 1232,225 L 1237,237 L 1237,245 L 1235,249 L 1227,253 L 1223,254 L 1223,245 L 1221,238 L 1225,237 L 1221,231 L 1219,230 L 1217,231 L 1214,229 L 1217,225 L 1217,219 L 1212,216 L 1206,218 L 1202,221 L 1197,223 L 1199,220 L 1198,217 L 1202,213 L 1200,209 L 1190,216 L 1187,221 L 1183,221 L 1180,224 L 1183,229 L 1187,230 L 1187,233 L 1190,235 L 1196,230 L 1200,233 L 1203,233 L 1204,236 L 1197,238 L 1195,242 L 1190,245 L 1188,250 L 1193,254 L 1195,260 L 1201,272 L 1201,277 L 1198,279 L 1199,282 L 1202,284 L 1200,295 L 1197,296 L 1185,320 L 1172,332 L 1167,333 L 1164,336 L 1162,333 L 1160,337 L 1153,340 L 1148,341 L 1146,348 L 1144,349 L 1142,344 L 1144,341 L 1137,339 L 1129,346 L 1125,352 L 1124,357 L 1132,373 L 1136,377 L 1139,382 L 1141,395 L 1140,407 L 1131,416 L 1121,427 L 1119,423 L 1121,419 L 1117,415 L 1113,414 L 1109,403 L 1105,400 L 1100,400 L 1101,395 L 1097,395 L 1097,402 L 1092,418 L 1093,423 L 1096,423 L 1099,435 L 1101,439 L 1104,440 L 1111,448 L 1113,453 L 1113,467 L 1115,468 L 1117,477 L 1113,477 L 1103,467 L 1097,450 L 1098,445 L 1097,442 L 1089,429 L 1089,433 L 1088,429 L 1091,408 L 1089,404 L 1089,397 L 1087,393 L 1085,377 L 1083,371 L 1074,379 L 1069,377 L 1070,369 L 1069,363 L 1066,355 L 1066,352 L 1064,352 L 1060,346 L 1056,332 L 1051,332 L 1052,334 L 1050,338 L 1048,337 L 1047,338 L 1044,337 L 1044,339 L 1034,341 L 1035,346 L 1032,350 L 1025,354 L 1020,362 L 1012,371 L 1012,374 L 1003,378 L 1001,383 L 1003,398 L 1001,404 L 1000,416 L 998,416 L 996,421 L 997,423 L 993,425 L 989,432 L 985,425 L 981,409 L 977,400 L 975,387 L 970,378 L 967,356 L 966,341 L 959,345 L 956,345 L 950,337 L 952,334 L 950,331 L 945,326 L 941,324 L 940,319 L 936,314 L 913,316 L 893,312 L 891,304 L 889,302 L 881,307 L 875,305 L 870,299 L 865,298 L 859,282 L 856,283 L 853,281 L 851,284 L 848,283 L 852,299 L 859,305 L 859,311 L 862,319 L 862,314 L 863,310 L 864,309 L 866,311 L 865,319 L 867,323 L 870,322 L 877,323 L 888,307 L 889,317 L 891,322 L 893,324 L 900,327 L 905,335 L 899,348 L 896,347 L 895,349 L 895,358 L 893,358 L 890,360 L 888,365 L 885,365 L 883,367 L 883,369 L 881,371 L 878,371 L 870,375 L 868,380 L 856,386 L 852,391 L 848,391 L 846,394 L 837,396 L 834,400 L 827,400 L 823,383 L 824,383 L 823,372 L 816,360 L 815,354 L 810,348 L 806,342 L 806,333 L 803,326 L 798,322 L 796,313 L 787,296 L 785,296 L 786,287 L 781,299 L 778,294 L 774,284 L 776,292 L 782,309 L 790,324 L 789,330 L 796,337 L 798,360 L 803,364 L 807,378 L 816,388 L 826,402 L 826,405 L 823,406 L 827,409 L 830,415 L 832,415 L 842,413 L 850,409 L 855,408 L 863,404 L 863,414 L 856,440 L 847,457 L 842,466 L 825,483 L 818,497 L 815,499 L 814,502 L 812,503 L 809,515 L 807,517 L 804,525 L 805,529 L 808,532 L 807,543 L 813,558 L 814,584 L 811,594 L 808,598 L 798,604 L 786,619 L 785,624 L 788,635 L 789,634 L 789,648 L 775,659 L 775,662 L 777,662 L 773,679 L 769,684 L 763,695 L 754,706 L 751,709 L 743,712 L 743,714 L 740,713 L 737,715 L 732,713 L 727,714 L 713,720 L 708,715 L 707,716 L 707,714 L 705,705 L 707,704 L 707,699 L 692,668 L 688,646 L 688,635 L 683,626 L 680,614 L 676,607 L 676,592 L 679,577 L 685,567 L 685,558 L 681,547 L 683,543 L 677,519 L 665,500 L 662,493 L 665,479 L 664,478 L 666,465 L 665,460 L 662,459 L 660,453 L 652,457 L 648,457 L 644,448 L 640,443 L 629,444 L 610,454 L 606,452 L 598,451 L 592,452 L 584,456 L 582,456 L 577,453 L 565,440 L 561,436 L 558,433 L 557,425 L 550,417 L 549,412 L 545,408 L 543,408 L 541,403 L 540,394 L 538,389 L 536,386 L 538,385 L 541,377 L 541,373 L 543,363 L 542,350 L 538,344 L 539,338 L 542,333 L 544,325 L 548,320 L 551,308 L 554,306 L 557,299 L 560,296 L 564,296 L 574,284 L 573,275 L 575,266 L 578,261 L 587,255 L 591,244 L 595,244 L 598,247 L 602,247 L 609,248 L 614,245 L 619,243 L 627,239 L 643,237 L 645,238 L 650,235 L 660,236 L 665,234 L 668,234 L 668,238 L 672,235 L 673,237 L 670,240 L 670,243 L 672,245 L 671,251 L 668,254 L 669,258 L 672,258 L 673,261 L 675,262 L 680,264 L 686,265 L 692,268 L 695,274 L 706,278 L 711,281 L 715,277 L 714,271 L 716,268 L 719,265 L 722,264 L 729,265 L 731,268 L 739,270 L 740,273 L 746,272 L 758,277 L 763,273 L 767,273 L 771,274 L 772,277 L 773,275 L 781,277 L 785,273 L 786,264 L 791,252 L 791,247 L 792,244 L 790,241 L 792,238 L 789,239 L 785,237 L 782,241 L 775,242 L 771,238 L 766,238 L 765,241 L 761,242 L 757,238 L 752,238 L 749,232 L 745,228 L 748,223 L 745,219 L 750,213 L 757,213 L 759,208 L 768,208 L 774,204 L 780,202 L 787,202 L 803,209 L 812,209 L 818,205 L 819,203 L 817,198 L 795,180 L 798,179 L 802,174 L 799,171 L 806,168 L 806,167 L 791,171 L 786,173 L 787,178 L 789,179 L 794,179 L 793,181 L 788,182 L 781,186 L 779,185 L 780,182 L 774,180 L 780,176 L 779,175 L 771,173 L 771,171 L 766,171 L 761,180 L 761,182 L 759,183 L 757,183 L 756,191 L 753,194 L 752,198 L 754,205 L 758,207 L 757,209 L 751,209 L 745,215 L 744,210 L 739,209 L 733,211 L 736,215 L 734,216 L 731,216 L 729,213 L 728,214 L 731,221 L 729,223 L 734,228 L 734,232 L 730,230 L 731,233 L 728,234 L 730,240 L 727,240 L 723,237 L 720,227 L 715,216 L 712,214 L 713,204 L 710,200 L 696,192 L 692,187 L 693,187 L 691,184 L 691,182 L 688,180 L 686,183 L 685,181 L 685,179 L 686,178 L 682,177 L 679,179 L 678,185 L 680,188 L 684,192 L 687,197 L 692,203 L 696,203 L 697,204 L 696,205 L 703,210 L 708,215 L 707,217 L 704,214 L 700,213 L 698,217 L 702,220 L 701,223 L 699,224 L 697,229 L 695,230 L 697,223 L 693,215 L 685,208 L 681,207 L 677,204 L 670,196 L 668,189 L 662,186 L 651,195 L 641,193 L 635,195 L 634,203 L 630,207 L 624,209 L 618,220 L 620,224 L 618,227 L 617,232 L 613,233 L 610,238 L 599,238 L 594,243 L 592,243 L 589,236 L 584,235 L 582,237 L 580,236 L 577,237 L 578,227 L 575,227 L 574,224 L 578,211 L 577,198 L 575,195 L 582,190 L 599,193 L 611,193 L 613,189 L 614,175 L 606,165 L 598,162 L 598,157 L 604,156 L 612,157 L 611,150 L 615,153 L 626,147 L 628,142 L 638,137 L 642,127 L 649,125 L 653,125 L 654,123 L 658,123 L 658,125 L 662,121 L 658,111 L 658,104 L 660,100 L 665,100 L 670,96 L 669,102 L 669,104 L 672,105 L 671,107 L 669,107 L 666,111 L 667,117 L 672,119 L 672,121 L 679,118 L 687,123 L 704,116 L 709,117 L 709,118 L 713,118 L 715,116 L 721,113 L 720,103 L 723,98 L 727,96 L 731,101 L 735,101 L 736,92 L 734,93 L 731,90 L 731,86 L 743,84 L 753,84 L 759,81 L 754,78 L 745,78 L 729,82 L 726,78 L 721,76 L 722,69 L 720,63 L 722,59 L 727,55 L 741,46 L 740,44 L 734,40 L 726,42 L 721,47 L 722,51 L 705,62 L 701,72 L 709,80 L 705,88 L 700,90 L 698,101 L 695,107 L 690,107 L 687,112 L 681,112 L 680,106 L 672,89 L 669,84 L 660,92 L 653,94 L 647,90 L 644,68 L 648,63 L 661,58 L 670,51 L 690,28 L 711,15 L 722,12 L 730,12 L 737,7 L 745,7 L 754,5 L 769,10 L 763,12 L 768,16 L 773,14 L 781,18 L 794,20 L 812,27 L 816,31 L 816,35 L 811,39 L 803,40 L 781,35 L 778,36 L 786,41 L 786,51 L 796,55 L 797,52 L 794,49 L 797,46 L 809,50 L 813,49 L 809,44 L 820,37 L 825,38 L 829,40 L 832,35 L 828,31 L 830,27 L 827,23 L 840,25 L 843,29 L 837,30 L 837,34 L 841,36 L 848,34 L 849,30 L 876,21 L 879,22 L 875,26 L 881,26 L 884,24 L 893,24 L 900,21 L 906,25 L 911,21 L 906,17 L 908,14 L 923,17 L 946,26 L 950,23 L 945,19 L 945,18 L 939,17 L 940,14 L 938,7 L 947,0 L 950,-6 L 953,-7 L 966,-5 L 967,-1 L 962,4 L 965,6 L 967,11 L 966,20 L 971,24 L 969,29 L 960,38 L 965,39 L 967,37 L 972,35 L 973,32 L 978,29 L 975,25 L 977,20 L 972,20 L 971,16 L 974,9 L 968,4 L 977,-1 L 976,-6 L 978,-6 L 981,-2 L 979,4 L 984,6 L 982,1 L 990,-2 L 1000,-2 L 1008,2 L 1004,-4 L 1004,-11 L 1012,-12 L 1034,-13 L 1030,-17 L 1035,-21 L 1041,-21 L 1050,-25 L 1063,-25 L 1064,-27 L 1077,-28 L 1081,-26 L 1091,-30 L 1100,-30 L 1101,-33 L 1106,-36 L 1117,-38 L 1125,-36 L 1119,-35 L 1130,-34 Z M 854,207 L 856,212 L 859,212 L 860,214 L 856,215 L 854,222 L 853,224 L 854,232 L 859,233 L 862,237 L 869,238 L 876,236 L 877,223 L 873,221 L 874,216 L 871,216 L 872,210 L 877,211 L 881,209 L 877,205 L 876,201 L 872,203 L 872,208 L 870,202 L 871,199 L 870,197 L 865,195 L 862,189 L 860,187 L 860,185 L 864,185 L 864,180 L 868,179 L 873,180 L 873,174 L 873,170 L 868,170 L 864,168 L 854,173 L 852,177 L 847,178 L 842,185 L 847,191 L 846,196 L 854,207 Z M 172,-37 L 161,-37 L 160,-39 L 170,-39 L 173,-38 L 172,-37 Z M 94,-38 L 86,-36 L 78,-39 L 82,-41 L 89,-41 L 96,-40 L 94,-38 Z M 738,-39 L 727,-37 L 719,-38 L 722,-40 L 719,-42 L 729,-44 L 731,-41 L 738,-39 Z M 163,-41 L 156,-39 L 152,-41 L 150,-44 L 150,-46 L 158,-46 L 164,-43 L 163,-41 Z M 143,-43 L 145,-40 L 129,-43 L 118,-43 L 123,-45 L 117,-47 L 117,-49 L 139,-46 L 143,-43 Z M 707,-52 L 722,-47 L 710,-44 L 708,-39 L 704,-38 L 701,-32 L 696,-32 L 685,-36 L 690,-39 L 683,-41 L 673,-46 L 670,-52 L 683,-54 L 685,-52 L 692,-52 L 694,-54 L 701,-54 L 707,-52 Z M 293,-75 L 316,-73 L 325,-72 L 325,-70 L 297,-64 L 308,-64 L 289,-58 L 280,-53 L 268,-50 L 253,-49 L 260,-49 L 256,-47 L 260,-44 L 240,-35 L 240,-34 L 248,-34 L 248,-32 L 236,-28 L 223,-30 L 209,-29 L 193,-30 L 193,-33 L 201,-35 L 199,-40 L 202,-40 L 215,-37 L 208,-42 L 200,-43 L 204,-46 L 213,-47 L 214,-50 L 207,-52 L 205,-56 L 222,-55 L 230,-57 L 202,-57 L 194,-60 L 184,-64 L 183,-67 L 205,-69 L 212,-72 L 218,-71 L 223,-70 L 227,-73 L 242,-75 L 293,-75 Z M 491,-78 L 520,-72 L 512,-70 L 468,-69 L 470,-68 L 487,-68 L 501,-66 L 511,-68 L 515,-66 L 509,-62 L 545,-67 L 559,-66 L 562,-63 L 539,-56 L 524,-55 L 535,-55 L 526,-46 L 526,-38 L 532,-34 L 524,-33 L 516,-31 L 525,-28 L 526,-22 L 521,-21 L 527,-15 L 517,-15 L 522,-12 L 521,-10 L 507,-9 L 513,-4 L 513,-1 L 504,-4 L 502,-2 L 508,-1 L 514,4 L 516,9 L 508,10 L 498,4 L 500,8 L 494,12 L 513,13 L 487,24 L 468,26 L 463,29 L 457,36 L 446,41 L 430,44 L 426,48 L 426,53 L 423,57 L 416,63 L 418,68 L 413,80 L 406,81 L 399,75 L 390,75 L 385,71 L 382,65 L 374,56 L 371,52 L 371,46 L 364,40 L 366,35 L 362,32 L 367,25 L 374,22 L 376,19 L 377,14 L 365,18 L 359,16 L 359,12 L 361,8 L 375,10 L 362,3 L 358,4 L 354,2 L 359,-4 L 346,-18 L 340,-21 L 340,-24 L 328,-28 L 293,-27 L 279,-34 L 301,-36 L 281,-38 L 270,-41 L 271,-43 L 306,-50 L 308,-52 L 295,-55 L 300,-57 L 316,-62 L 323,-63 L 321,-66 L 347,-69 L 362,-69 L 367,-67 L 380,-70 L 408,-65 L 396,-69 L 397,-72 L 413,-76 L 430,-75 L 436,-78 L 491,-78 Z \ No newline at end of file diff --git a/scripts/hole-punching-lan.gen.py b/scripts/hole-punching-lan.gen.py new file mode 100644 index 00000000..1f27ddaa --- /dev/null +++ b/scripts/hole-punching-lan.gen.py @@ -0,0 +1,366 @@ +#!/usr/bin/env python3 +"""Generator for hole-punching-lan.svg ("Direct connections on the same network"). + +The easy, very common case: Alice and Bob are two devices on the *same* home +network, behind the *same* router. They still discover each other over the relay +(ADD_ADDRESS / REACH_OUT), but then the *local* candidate addresses are directly +reachable across the LAN — so the very first PATH_CHALLENGE to the local address +succeeds. No NAT hole to punch, no sacrificial probe, no public-address probes. + +Layout is a "Y": the relay sits on top of the stem (the router's uplink), the +router is the junction, Alice and Bob hang off the two legs. Data first flows up +the stem to the relay and back; once the LAN path validates, the stem fades to +gray and the two legs (the direct LAN path) carry everything. + +Same look as the other how-iroh-works figures. + +Edit this script and run: python3 hole-punching-lan.gen.py +Writes ../public/animations/hole-punching-lan.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", "hole-punching-lan.svg")) + +MONO = "'Space Mono', monospace" +INDIGO, AMBER, GRAY, INK, BLUE, RED, GREEN = "#6366f1", "#d97706", "#888", "#111", "#2563eb", "#dc2626", "#15803d" +FADED = "#aab2bd" # inactive/secondary connection — clearly lighter than the structure gray +CYCLE = 27.0 +T_POPUP_FADE = 23.5 # popups fade a little after the LAN path is validated + + +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'') + + +def dot(color, opa_pts, mot_pts, wire): + return f''' + + {anim_opacity(opa_pts)} + {anim_motion(wire, mot_pts)} + ''' + + +def key_label(cx, y, text, size, color): + """A tiny drawn gold key (bow + shaft + teeth) + monospace label, centered on cx + (replaces the 🔑 emoji, which no pure-vector renderer can draw).""" + s = size + gold = "#eab308" + w = s * 1.3 # key glyph width + gap = s * 0.3 + xl = cx - (w + gap + s * 0.6 * len(text)) / 2 + cy = y - s * 0.30 + rb, rh = s * 0.34, s * 0.15 # bow outer / hole radius + bx = xl + rb # bow center x + hs = s * 0.18 # shaft thickness + xr = xl + w # right end + top, bot = cy - hs / 2, cy + hs / 2 + tw = s * 0.13 # tooth width + mono = "'Space Mono', monospace" + return ( + f'' + f'' + f'' + f'' + f'{text}' + ) + + +def iphone_screen(px0, py0, pw, ph, indigo): + """iPhone-style screen outline path (with notch) sized to fit inside an outer body of pw x ph.""" + sx0, sx1 = px0+3, px0+pw-3 + sy0, sy1 = py0+3, py0+ph-3 + cx = px0 + pw/2 + no = 7 # outer notch half-width + ni = 4 # inner notch half-width + nd = 5 # notch depth + ny = sy0 + nd + r = 6 # screen corner radius + return ( + f'' + ) + +def phone(px0, py0, keytext, iphone=False): + pw, ph = 50, 92 + cx = px0+pw/2 + if iphone: + screen = iphone_screen(px0, py0, pw, ph, INDIGO) + else: + screen = f'\n ' + return f''' + {screen} + + iroh + {key_label(cx, py0+60, keytext, 8, AMBER)}''' + + +def router_lan(cx, cy, lan_ip): + # single home router; the stem (uplink to the relay) leaves the top center, + # so the LAN gateway IP goes below and there is no label above. + bx, by, bw, bh = cx-30, cy-20, 60, 40 + return f''' + + + + + + + {lan_ip} + ''' + + +def relay(cx, cy, label): + bx, by, bw, bh = cx-32, cy-9, 64, 18 + s = [' ', + f' ', + f' ', + f' ', + f' '] + for i in range(6): + vx = bx+24+i*5 + s.append(f' ') + s.append(f' {label}') + s.append(' ') + return "\n".join(s) + + +def facts(x, lines, anchor="start"): + out = [f' '] + for y, label, val in lines: + out.append(f' {label} {val}') + out.append(' ') + return "\n".join(out) + + +# ---- positions (a "Y": relay on top, router at the junction, phones on the legs) ---- +RELAY = (440, 66) +ROUTER = (440, 200) +A_PH = (175, 312) # Alice phone top-left +B_PH = (655, 312) # Bob phone top-left +acx, bcx = 200, 680 +PH = 92 +JX, JY = 440, 220 # junction (router bottom center) where the legs meet the stem +RELAY_B = 78 # relay box bottom + +# The legs curve: they leave each phone vertically and arrive at the router vertically +# (from below). They enter the router box at two *separate* points tucked inside it, so +# the legs never cross; the vertical stem leaves the box top center up to the relay. +DV, DV2 = 64, 88 # control-point offsets: vertical out of the phone / into the router +OJ = 16 # legs enter the box this far either side of center +JYH = 213 # internal junction, hidden inside the router box +JXA, JXB = JX-OJ, JX+OJ +_legA = f"C {acx} {A_PH[1]-DV} {JXA} {JYH+DV2} {JXA} {JYH}" +_legB = f"C {bcx} {B_PH[1]-DV} {JXB} {JYH+DV2} {JXB} {JYH}" +_legA_r = f"C {JXA} {JYH+DV2} {acx} {A_PH[1]-DV} {acx} {A_PH[1]}" +_legB_r = f"C {JXB} {JYH+DV2} {bcx} {B_PH[1]-DV} {bcx} {B_PH[1]}" + +# stem (uplink to the relay); lan path (Alice -> router -> Bob) +stem_d = f"M {JX} {JYH} L {JX} {RELAY_B}" +lan_d = f"M {acx} {A_PH[1]} {_legA} L {JXB} {JYH} {_legB_r}" +# relay round-trips: up the stem to the relay and back down (the latency made visible) +relay_b2a = f"M {bcx} {B_PH[1]} {_legB} L {JX} {JYH} L {JX} {RELAY_B} L {JX} {JYH} L {JXA} {JYH} {_legA_r}" +relay_a2b = f"M {acx} {A_PH[1]} {_legA} L {JX} {JYH} L {JX} {RELAY_B} L {JX} {JYH} L {JXB} {JYH} {_legB_r}" + +alice_facts = facts(A_PH[0]+62, [ + (A_PH[1]+18, "EndpointId:", "1a9c…"), + (A_PH[1]+36, "Addr:", "192.168.0.3:2104"), + (A_PH[1]+54, "Addr:", "4.9.8.2:2104"), + (A_PH[1]+72, "relay:", "us-east"), +]) +bob_facts = facts(B_PH[0]+62, [ + (B_PH[1]+18, "EndpointId:", "8e2b…"), + (B_PH[1]+36, "Addr:", "192.168.0.5:4153"), + (B_PH[1]+54, "Addr:", "4.9.8.2:4153"), + (B_PH[1]+72, "relay:", "us-east"), +]) + +# ---- beat 1: ADD_ADDRESS (Bob advertises his candidates to Alice, over the relay) ---- +addaddr_pkt = dot(BLUE, + [(0, 0), (1.0, 0), (1.3, 1), (4.3, 1), (4.6, 0), (CYCLE, 0)], + [(0, 1), (1.0, 1), (4.5, 0), (CYCLE, 0)], "relay-b2a") +addaddr_co_pts = [(0, 0), (1.0, 0), (1.4, 1), (4.4, 1), (4.8, 0), (CYCLE, 0)] +addaddr_co = f''' + {anim_opacity(addaddr_co_pts)} + + ADD_ADDRESS 192.168.0.5:4153 + ADD_ADDRESS 4.9.8.2:4153 + ''' + +# Alice now knows Bob's candidates — ~2s popup beside Alice +ak_pts = [(0, 0), (4.4, 0), (4.7, 1), (6.6, 1), (6.9, 0), (CYCLE, 0)] +alice_knows = f''' + {anim_opacity(ak_pts)} + + + + + Bob + Candidate: 192.168.0.5:4153 + Candidate: 4.9.8.2:4153 + ''' + +# ---- beat 2: REACH_OUT (Alice -> Bob over the relay), carries Alice's candidates ---- +reachout_pkt = dot(BLUE, + [(0, 0), (7.5, 0), (7.8, 1), (10.8, 1), (11.1, 0), (CYCLE, 0)], + [(0, 0), (7.5, 0), (11.0, 1), (CYCLE, 1)], "relay-a2b") +reachout_co_pts = [(0, 0), (7.5, 0), (7.9, 1), (10.9, 1), (11.2, 0), (CYCLE, 0)] +reachout_co = f''' + {anim_opacity(reachout_co_pts)} + + REACH_OUT192.168.0.3:2104 + REACH_OUT4.9.8.2:2104 + ''' + +# Bob now knows Alice's candidates — ~2s popup beside Bob +bk_pts = [(0, 0), (11.0, 0), (11.3, 1), (13.2, 1), (13.5, 0), (CYCLE, 0)] +bob_knows = f''' + {anim_opacity(bk_pts)} + + + + + Alice + Candidate: 192.168.0.3:2104 + Candidate: 4.9.8.2:2104 + ''' + +# ============================ LAN hole punch (no NAT in the way) ============================ +# Same subnet, so the local candidate is directly reachable: the first PATH_CHALLENGE to the +# local address gets through. No sacrificial probe, no public probes, no drops. +# Alice is the client → she probes first. Each step gets a clear window. +T_C1 = 14.2 # Alice PATH_CHALLENGE -> Bob (local) — gets through +T_C2 = 16.8 # Bob PATH_CHALLENGE + PATH_RESPONSE -> Alice +T_R = 19.4 # Alice PATH_RESPONSE -> Bob — both directions validated +T_BLUE = T_R + 2.2 + + +def hp_pkt(t0, dur, kp0, kp1, color=BLUE): + arr = t0 + dur + return f''' + + {anim_opacity([(0, 0), (t0, 0), (t0+0.15, 1), (arr-0.12, 1), (arr+0.06, 0), (CYCLE, 0)])} + {anim_motion("lan", [(0, kp0), (t0, kp0), (arr, kp1), (CYCLE, kp1)])} + ''' + + +def callout(side, lines, t0, t1, w=170): + x = 232 if side == "L" else (648 - w) + h = 18 + len(lines)*19 + pts = [(0, 0), (t0-0.3, 0), (t0, 1), (t1, 1), (t1+0.3, 0), (CYCLE, 0)] + rows = "\n".join( + f' {txt}' + for i, (txt, bold) in enumerate(lines)) + return f''' + {anim_opacity(pts)} + +{rows} + ''' + + +# beat 3 — Alice's PATH_CHALLENGE to Bob's local address: directly reachable, gets through +c1_pkt = hp_pkt(T_C1, 2.0, 0, 1) +c1_co = callout("L", [("PATH_CHALLENGE", True), ("192.168.0.5:4153", False)], T_C1-0.3, T_C1+2.1) + +# beat 4 — Bob answers: PATH_RESPONSE (to Alice's challenge) + his own PATH_CHALLENGE +c2_pkt = hp_pkt(T_C2, 2.0, 1, 0) +c2_co = callout("R", [("PATH_RESPONSE", True), ("PATH_CHALLENGE", True), ("192.168.0.3:2104", False)], T_C2-0.3, T_C2+2.1) + +# beat 5 — Alice's PATH_RESPONSE to Bob — both directions validated +r_pkt = hp_pkt(T_R, 2.0, 0, 1) +r_co = callout("L", [("PATH_RESPONSE", True)], T_R-0.3, T_R+2.1, w=150) + +# the stem (relay uplink) is blue/active until the LAN path validates, then fades to gray +stem_stroke = (f'') + +# "direct" confirmation appears when the path is validated +direct_pts = [(0, 0), (T_BLUE, 0), (T_BLUE+0.4, 1), (CYCLE, 1)] +direct_badge = f''' + {anim_opacity(direct_pts)} + + direct (LAN) + ''' + +VB_X, VB_Y, VB_W, VB_H = 0, 30, 940, 430 +svg = f''' + + + {stem_stroke} + + + + + + +{relay(*RELAY, "relay")} + +{router_lan(*ROUTER, "192.168.0.1")} + + + +{phone(*A_PH, "1a9c…", iphone=True)} + Alice + +{alice_facts} + + + +{phone(*B_PH, "8e2b…")} + Bob + +{bob_facts} + + +{addaddr_pkt} +{reachout_pkt} +{c1_pkt} +{c2_pkt} +{r_pkt} +{direct_badge} + + +{addaddr_co} +{alice_knows} +{reachout_co} +{bob_knows} +{c1_co} +{c2_co} +{r_co} + + + + + + +''' + +open(OUT_PATH, "w").write(svg) +print(f"wrote {len(svg)} bytes -> {OUT_PATH}") diff --git a/scripts/hole-punching.gen.py b/scripts/hole-punching.gen.py new file mode 100644 index 00000000..9912fb86 --- /dev/null +++ b/scripts/hole-punching.gen.py @@ -0,0 +1,469 @@ +#!/usr/bin/env python3 +"""Generator for hole-punching.svg ("Establishing direct connections"). + +Initial state of the hole-punching story: Alice and Bob, each behind a normal +home router, are connected *through the relay* (us-east) — all data flows up to +the relay and back down. Later steps (CallMeMaybe, address discovery, the ping, +and the direct path) build on this. + +Continues the discovery story: Bob = 8e2b…, home relay us-east; Alice = 1a9c…. +Same look as the other how-iroh-works figures; all connections blue. + +Edit this script and run: python3 hole-punching.gen.py +Writes ../public/animations/hole-punching.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", "hole-punching.svg")) + +MONO = "'Space Mono', monospace" +INDIGO, AMBER, GRAY, INK, BLUE, RED, GREEN = "#6366f1", "#d97706", "#888", "#111", "#2563eb", "#dc2626", "#15803d" +FADED = "#aab2bd" # inactive/secondary connection — clearly lighter than the structure gray +CYCLE = 39.0 # one loop; popups fade after both checks are green, then the direct path stands alone +T_POPUP_FADE = 29.5 # NAT bubbles fade out here (a few seconds after both mappings are validated) + + +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'') + + +def dot(color, opa_pts, mot_pts, wire="relay-path"): + return f''' + + {anim_opacity(opa_pts)} + {anim_motion(wire, mot_pts)} + ''' + + +def key_label(cx, y, text, size, color): + """A tiny drawn gold key (bow + shaft + teeth) + monospace label, centered on cx + (replaces the 🔑 emoji, which no pure-vector renderer can draw).""" + s = size + gold = "#eab308" + w = s * 1.3 # key glyph width + gap = s * 0.3 + xl = cx - (w + gap + s * 0.6 * len(text)) / 2 + cy = y - s * 0.30 + rb, rh = s * 0.34, s * 0.15 # bow outer / hole radius + bx = xl + rb # bow center x + hs = s * 0.18 # shaft thickness + xr = xl + w # right end + top, bot = cy - hs / 2, cy + hs / 2 + tw = s * 0.13 # tooth width + mono = "'Space Mono', monospace" + return ( + f'' + f'' + f'' + f'' + f'{text}' + ) + + +def iphone_screen(px0, py0, pw, ph, indigo): + """iPhone-style screen outline path (with notch) sized to fit inside an outer body of pw x ph.""" + sx0, sx1 = px0+3, px0+pw-3 + sy0, sy1 = py0+3, py0+ph-3 + cx = px0 + pw/2 + no = 7 # outer notch half-width + ni = 4 # inner notch half-width + nd = 5 # notch depth + ny = sy0 + nd + r = 6 # screen corner radius + return ( + f'' + ) + +def phone(px0, py0, keytext, iphone=False): + pw, ph = 50, 92 + cx = px0+pw/2 + if iphone: + screen = iphone_screen(px0, py0, pw, ph, INDIGO) + else: + screen = f'\n ' + return f''' + {screen} + + iroh + {key_label(cx, py0+60, keytext, 8, AMBER)}''' + + +def router(cx, cy, pub_ip, lan_ip): + # same design as the "Guaranteed connections" diagram: public IP above, + # L-shaped antennas, body, LAN IP below + bx, by, bw, bh = cx-30, cy-20, 60, 40 + return f''' + {pub_ip} + + + + + + + {lan_ip} + ''' + + +def relay(cx, cy, label): + bx, by, bw, bh = cx-32, cy-9, 64, 18 + s = [' ', + f' ', + f' ', + f' ', + f' '] + for i in range(6): + vx = bx+24+i*5 + s.append(f' ') + s.append(f' {label}') + s.append(' ') + return "\n".join(s) + + +def facts(x, lines, anchor="start"): + out = [f' '] + for y, label, val in lines: + out.append(f' {label} {val}') + out.append(' ') + return "\n".join(out) + + +# ---- positions ---- +RELAY = (440, 66) +R1 = (200, 250) # Alice's router +R2 = (680, 250) # Bob's router +A_PH = (175, 312) # Alice phone top-left +B_PH = (655, 312) # Bob phone top-left +acx, bcx = 200, 680 +PH = 92 + +# relay legs: Alice <-> relay (attach left of box) and relay <-> Bob (attach right of box), +# so the "relay" label below the box stays clear +# one continuous relay pipe: rises through each router, flattens and passes +# horizontally *through* the relay (in the left edge, out the right), then drops. +# The relay clearly sits on the pipe; the "relay" label stays clear below it. +rx, ry = RELAY +relay_d = (f"M {acx} {A_PH[1]} L {acx} {R1[1]-20} " + f"C {acx} 120 {rx-160} {ry} {rx-32} {ry} " + f"L {rx+32} {ry} " + f"C {rx+160} {ry} {bcx} 120 {bcx} {R2[1]-20} " + f"L {bcx} {B_PH[1]}") + +alice_facts = facts(A_PH[0]+62, [ + (A_PH[1]+18, "EndpointId:", "1a9c…"), + (A_PH[1]+36, "Addr:", "10.0.0.3:2104"), + (A_PH[1]+54, "Addr:", "8.3.1.9:2104"), + (A_PH[1]+72, "relay:", "us-west"), +]) +bob_facts = facts(B_PH[0]+62, [ + (B_PH[1]+18, "EndpointId:", "8e2b…"), + (B_PH[1]+36, "Addr:", "192.168.0.3:4153"), + (B_PH[1]+54, "Addr:", "4.9.8.2:4153"), + (B_PH[1]+72, "relay:", "us-east"), +]) + +# ---- beat 1: ADD_ADDRESS (Bob advertises his candidate addresses to Alice, over the relay) ---- +# one packet carrying both ADD_ADDRESS frames travels Bob -> relay -> Alice. Not time-critical, +# and it does nothing to the routers' forwarding tables. +addaddr_pkt = dot(BLUE, + [(0, 0), (1.0, 0), (1.3, 1), (3.9, 1), (4.2, 0), (CYCLE, 0)], + [(0, 1), (1.0, 1), (4.0, 0), (CYCLE, 0)]) +addaddr_co_pts = [(0, 0), (1.0, 0), (1.4, 1), (4.0, 1), (4.4, 0), (CYCLE, 0)] +addaddr_co = f''' + {anim_opacity(addaddr_co_pts)} + + ADD_ADDRESS 192.168.0.3:4153 + ADD_ADDRESS 4.9.8.2:4153 + ''' + +# when the ADD_ADDRESS packet lands, Alice now knows Bob's candidates — ~2s popup +# beside Alice (briefly covering her own info panel) +ak_pts = [(0, 0), (4.0, 0), (4.3, 1), (6.2, 1), (6.5, 0), (CYCLE, 0)] +alice_knows = f''' + {anim_opacity(ak_pts)} + + + + + Bob + Candidate: 192.168.0.3:4153 + Candidate: 4.9.8.2:4153 + ''' + +# ---- beat 2: REACH_OUT (Alice -> Bob over the relay) — carries Alice's candidate addresses +# and is the trigger that starts probing; shown here decoupled from the probes for clarity ---- +reachout_pkt = dot(BLUE, + [(0, 0), (6.8, 0), (7.1, 1), (9.2, 1), (9.4, 0), (CYCLE, 0)], + [(0, 0), (6.8, 0), (9.3, 1), (CYCLE, 1)]) +reachout_co_pts = [(0, 0), (6.8, 0), (7.1, 1), (9.3, 1), (9.6, 0), (CYCLE, 0)] +reachout_co = f''' + {anim_opacity(reachout_co_pts)} + + REACH_OUT10.0.0.3:2104 + REACH_OUT8.3.1.9:2104 + ''' + +# Bob now knows Alice's candidates — ~2s popup beside Bob (toward center) +bk_pts = [(0, 0), (9.3, 0), (9.6, 1), (11.6, 1), (11.9, 0), (CYCLE, 0)] +bob_knows = f''' + {anim_opacity(bk_pts)} + + + + + Alice + Candidate: 10.0.0.3:2104 + Candidate: 8.3.1.9:2104 + I need to reach out + ''' + +# ============================ hole punch (Alice is the client → she probes first) ============================ +# Locals fail (not routable). Then the public chain on ONE shared direct route: Alice's sacrificial +# probe opens her hole but is rejected at Bob's NAT; Bob's probe opens his hole and gets through; +# Alice replies PATH_RESPONSE+PATH_CHALLENGE; Bob replies PATH_RESPONSE → both paths validated. +# No timeouts. Each step gets a clear window before the next. +T_AL, T_BL = 12.4, 14.6 # local probes (fail) +T_AP = 16.9 # Alice public — sacrificial +T_BP = 19.9 # Bob public — succeeds +T_AR = 22.9 # Alice reply: PATH_RESPONSE + PATH_CHALLENGE +T_BR = 25.7 # Bob reply: PATH_RESPONSE → validated +T_BLUE = T_BR + 2.4 # direct path turns blue once the response validates it + +DROP_A, DROP_B = (acx, 165), (bcx, 165) # local drops — straight up above each router (no bend toward the other side, which read as if the packet was trying to reach the peer). Kept high enough that the "not routable" label clears the router's public IP below. +probe_local_a_d = f"M {acx} {A_PH[1]} L {acx} {DROP_A[1]}" +probe_local_b_d = f"M {bcx} {B_PH[1]} L {bcx} {DROP_B[1]}" + +# the one direct (internet) route, Alice(0) -> Bob(1). Every public packet rides it. +ARC = 158 +direct_d = f"M {acx} {A_PH[1]} L {acx} {R1[1]-20} C {acx} {ARC} {bcx} {ARC} {bcx} {R2[1]-20} L {bcx} {B_PH[1]}" + + +def cubic_len(p0, p1, p2, p3, n=120): + def b(t, a, bb, c, d): return (1-t)**3*a + 3*(1-t)**2*t*bb + 3*(1-t)*t*t*c + t**3*d + prev, L = p0, 0.0 + for i in range(1, n+1): + t = i/n + cur = (b(t, p0[0], p1[0], p2[0], p3[0]), b(t, p0[1], p1[1], p2[1], p3[1])) + L += ((cur[0]-prev[0])**2 + (cur[1]-prev[1])**2)**0.5 + prev = cur + return L + + +_seg = A_PH[1] - (R1[1]-20) +_arc = cubic_len((acx, R1[1]-20), (acx, ARC), (bcx, ARC), (bcx, R2[1]-20)) +F_BR = (_seg + _arc) / (_seg + _arc + _seg) # fraction at Bob's router — where the sacrificial bounces +REJECT = (bcx, R2[1]-20) # Bob's router WAN — sacrificial probe rejected here + + +def hp_pkt(wire, t0, dur, kp0=0, kp1=1, color=BLUE): + arr = t0 + dur + return f''' + + {anim_opacity([(0, 0), (t0, 0), (t0+0.15, 1), (arr-0.12, 1), (arr+0.06, 0), (CYCLE, 0)])} + {anim_motion(wire, [(0, kp0), (t0, kp0), (arr, kp1), (CYCLE, kp1)])} + ''' + + +def drop_x(p, t_arr, label, hold=1.9, dx=0): + pts = [(0, 0), (t_arr-0.05, 0), (t_arr+0.15, 1), (t_arr+hold, 1), (t_arr+hold+0.3, 0), (CYCLE, 0)] + if dx: # label to the side instead of below + lbl = f'{label}' + else: + lbl = f'{label}' + return f''' + {anim_opacity(pts)} + + + {lbl} + ''' + + +def callout(side, lines, t0, t1, w=164): + x = 240 if side == "L" else (656 - w) + h = 18 + len(lines)*19 + pts = [(0, 0), (t0-0.3, 0), (t0, 1), (t1, 1), (t1+0.3, 0), (CYCLE, 0)] + rows = "\n".join( + f' {txt}' + for i, (txt, bold) in enumerate(lines)) + return f''' + {anim_opacity(pts)} + +{rows} + ''' + + +def pc_co(side, addr, t0, t1): + return callout(side, [("PATH_CHALLENGE", True), (addr, False)], t0, t1) + + +def nat_bubble(side, l1, l2, t_on, t_used): + # thinking bubble on a router: the NAT mapping (remote public -> local private). A green + # check appears next to the mapping the moment a packet actually uses it. + out0, out1 = T_POPUP_FADE, T_POPUP_FADE+1.0 + pts = [(0, 0), (t_on, 0), (t_on+0.4, 1), (out0, 1), (out1, 0), (CYCLE, 0)] + hg_pts = [(0, 0), (t_on+0.4, 0), (t_on+0.7, 1), (t_used, 1), (t_used+0.3, 0), (CYCLE, 0)] # hourglass: created, temporary + chk_pts = [(0, 0), (t_used, 0), (t_used+0.3, 1), (CYCLE, 1)] # check: used / validated + if side == "a": # to the right of Alice's router, trail back to it + bx, trail = 240, [(236, 251, 3), (231, 250, 2.5), (227, 249, 2)] + else: # to the right of Bob's router + bx, trail = 720, [(716, 251, 3), (711, 250, 2.5), (707, 249, 2)] + dots = "\n".join(f' ' for c in trail) + cx = bx+138 + return f''' + {anim_opacity(pts)} +{dots} + + {l1} + {l2} + + + + + + {anim_opacity(hg_pts)} + + + + {anim_opacity(chk_pts)} + + ''' + + +# beat 3 — sequential locals (Alice first), not routable +loc_a_pkt = hp_pkt("probe-local-a", T_AL, 1.4) +loc_b_pkt = hp_pkt("probe-local-b", T_BL, 1.4) +loc_a_drop = drop_x(DROP_A, T_AL+1.4, "not routable", hold=0.5) +loc_b_drop = drop_x(DROP_B, T_BL+1.4, "not routable", hold=0.5) +loc_a_co = pc_co("L", "192.168.0.3:4153", T_AL-0.3, T_AL+1.5) +loc_b_co = pc_co("R", "10.0.0.3:2104", T_BL-0.3, T_BL+1.5) + +# beat 4 — Alice's sacrificial public probe: opens her hole, rejected at Bob's NAT (bounces) +pub_a_pkt = hp_pkt("direct", T_AP, 2.0, kp0=0, kp1=F_BR) +pub_a_drop = drop_x(REJECT, T_AP+2.0, "no mapping", hold=0.6, dx=16) +pub_a_co = pc_co("L", "4.9.8.2:4153", T_AP-0.3, T_AP+2.1) +nat_a = nat_bubble("a", "4.9.8.2:4153 →", "10.0.0.3:2104", T_AP+0.6, t_used=21.9) + +# beat 5 — Bob's public probe: opens his hole, through Alice's hole, reaches Alice (success) +pub_b_pkt = hp_pkt("direct", T_BP, 2.2, kp0=1, kp1=0) +pub_b_co = pc_co("R", "8.3.1.9:2104", T_BP-0.3, T_BP+2.3) +nat_b = nat_bubble("b", "8.3.1.9:2104 →", "192.168.0.3:4153", T_BP+0.6, t_used=24.9) + +# beat 6 — Alice's bundled reply (PATH_RESPONSE + PATH_CHALLENGE) → Bob +reply_pkt = hp_pkt("direct", T_AR, 2.2, kp0=0, kp1=1) +reply_co = callout("L", [("PATH_RESPONSE", True), ("PATH_CHALLENGE", True)], T_AR-0.3, T_AR+2.3, w=164) + +# beat 7 — Bob's PATH_RESPONSE → Alice; both directions validated +resp_pkt = hp_pkt("direct", T_BR, 2.2, kp0=1, kp1=0) +resp_co = callout("R", [("PATH_RESPONSE", True)], T_BR-0.3, T_BR+2.3, w=150) + +# the direct path itself: appears faded-gray as Bob's packet goes through, turns blue once validated +direct_opa = [(0, 0), (T_BP, 0), (T_BP+0.3, 1), (CYCLE-0.5, 1), (CYCLE-0.2, 0), (CYCLE, 0)] +direct_stroke = (f'') +direct_path = (f' ' + f'{anim_opacity(direct_opa)}{direct_stroke}') + +# the relay stays blue/active (user data flows over it during hole punching) and only fades to +# gray at the moment the direct path becomes blue — i.e. when the handover happens +relay_stroke = (f'') + +VB_X, VB_Y, VB_W, VB_H = 0, 30, 940, 430 +svg = f''' + + + {relay_stroke} + +{direct_path} + + + + + + + +{relay(*RELAY, "relay")} + +{router(*R1, "8.3.1.9", "10.0.0.1")} + +{router(*R2, "4.9.8.2", "192.168.0.1")} + + + +{phone(*A_PH, "1a9c…", iphone=True)} + Alice + +{alice_facts} + + + +{phone(*B_PH, "8e2b…")} + Bob + +{bob_facts} + + +{addaddr_pkt} +{reachout_pkt} +{loc_a_pkt} +{loc_b_pkt} +{pub_a_pkt} +{pub_b_pkt} +{reply_pkt} +{resp_pkt} +{loc_a_drop} +{loc_b_drop} +{pub_a_drop} +{nat_a} +{nat_b} + + +{addaddr_co} +{alice_knows} +{reachout_co} +{bob_knows} +{loc_a_co} +{loc_b_co} +{pub_a_co} +{pub_b_co} +{reply_co} +{resp_co} + + + + + + +''' + +open(OUT_PATH, "w").write(svg) +print(f"wrote {len(svg)} bytes -> {OUT_PATH}") diff --git a/scripts/publish-relay-dht.gen.py b/scripts/publish-relay-dht.gen.py new file mode 100644 index 00000000..8ae2c082 --- /dev/null +++ b/scripts/publish-relay-dht.gen.py @@ -0,0 +1,308 @@ +#!/usr/bin/env python3 +"""Generator for publish-relay-dht.svg ("Mainline DHT based address lookup"). + +The fully peer-to-peer variant of the DNS discovery figure. Instead of one +server (dns.iroh.link) there is a cloud (the Mainline DHT) holding many nodes. +Bob publishes his signed record to several random DHT nodes (green); Alice +resolves it by querying several random nodes (blue). + +All dots travel at the same speed, so they reach their nodes (and return) at +different times depending on distance. No arrowheads — the moving dots show the +direction. The answer is revealed as soon as the FIRST query response returns. + +One master SMIL loop (CYCLE seconds): publish -> Alice resolves -> arrows fade +out -> final state holds ~10s -> loop. + +Edit this script and run: python3 publish-relay-dht.gen.py +Writes ../public/animations/publish-relay-dht.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", "publish-relay-dht.svg")) + +MONO = "'Space Mono', monospace" +INDIGO, AMBER, GRAY, INK = "#6366f1", "#d97706", "#888", "#111" +GREEN, BLUE, RED = "#15803d", "#2563eb", "#dc2626" +SPEED = 150.0 # px/second — same for every dot + + +def arc(x1, y1, x2, y2, k=0.16): + mx, my = (x1+x2)/2, (y1+y2)/2 + dx, dy = x2-x1, y2-y1 + L = math.hypot(dx, dy) + px, py = -dy/L, dx/L + if py > 0: + px, py = -px, -py + cx, cy = mx+px*k*L, my+py*k*L + d = f"M {x1:.0f} {y1:.0f} Q {cx:.0f} {cy:.0f} {x2:.0f} {y2:.0f}" + def q(t, a, b, c): return (1-t)**2*a + 2*(1-t)*t*b + t**2*c + N, length, prev = 50, 0.0, (x1, y1) + for i in range(1, N+1): + t = i/N + cur = (q(t, x1, cx, x2), q(t, y1, cy, y2)) + length += math.hypot(cur[0]-prev[0], cur[1]-prev[1]) + prev = cur + return d, length + + +def key_label(cx, y, text, size, color): + """A tiny drawn gold key (bow + shaft + teeth) + monospace label, centered on cx + (replaces the 🔑 emoji, which no pure-vector renderer can draw).""" + s = size + gold = "#eab308" + w = s * 1.3 # key glyph width + gap = s * 0.3 + xl = cx - (w + gap + s * 0.6 * len(text)) / 2 + cy = y - s * 0.30 + rb, rh = s * 0.34, s * 0.15 # bow outer / hole radius + bx = xl + rb # bow center x + hs = s * 0.18 # shaft thickness + xr = xl + w # right end + top, bot = cy - hs / 2, cy + hs / 2 + tw = s * 0.13 # tooth width + mono = "'Space Mono', monospace" + return ( + f'' + f'' + f'' + f'' + f'{text}' + ) + + +def iphone_screen(px0, py0, pw, ph, indigo): + """iPhone-style screen outline path (with notch) sized to fit inside an outer body of pw x ph.""" + sx0, sx1 = px0+3, px0+pw-3 + sy0, sy1 = py0+3, py0+ph-3 + cx = px0 + pw/2 + no = 7 # outer notch half-width + ni = 4 # inner notch half-width + nd = 5 # notch depth + ny = sy0 + nd + r = 6 # screen corner radius + return ( + f'' + ) + +def phone(px0, py0, keytext, iphone=False): + pw, ph = 48, 88 + cx = px0+pw/2 + if iphone: + screen = iphone_screen(px0, py0, pw, ph, INDIGO) + else: + screen = f'\n ' + return f''' + {screen} + + iroh + {key_label(cx, py0+58, keytext, 8, AMBER)}''' + + +# ============================ geometry ============================ +# DHT cloud (soft, fill-only overlapping circles) +cloud_circles = [(320, 150, 56), (392, 124, 62), (462, 130, 58), (524, 156, 50), + (360, 172, 56), (442, 178, 56), (292, 174, 40), (548, 182, 38)] +nodes = [(320, 162), (376, 138), (432, 172), (484, 142), (528, 168), (402, 196)] +pub_nodes = nodes[0:5] # Bob publishes to these +look_nodes = nodes[1:6] # Alice queries these + +bx0, by0 = 96, 296 +bcx = bx0+24 +PH = 88 +bob_top = (bcx, by0) + +ax0, ay0 = 664, 296 +acx = ax0+24 +alice_top = (acx, ay0) + +# ============================ timeline (constant speed) ============================ +PUB_LAUNCH = 1.5 +pub = [] # (path_d, arrival_time) +for n in pub_nodes: + d, L = arc(bob_top[0], bob_top[1], n[0], n[1], k=0.16) + pub.append((d, PUB_LAUNCH + L/SPEED)) +PUB_DONE = max(t for _, t in pub) + +ALICE_IN = PUB_DONE + 0.3 +LOOK_LAUNCH = ALICE_IN + 1.2 +look = [] # (path_d, reach_node_time, back_at_alice_time, has_data) +for n in look_nodes: + d, L = arc(n[0], n[1], alice_top[0], alice_top[1], k=0.16) + half = L/SPEED + look.append((d, LOOK_LAUNCH + half, LOOK_LAUNCH + 2*half, n in pub_nodes)) +FIRST_VALID = min(b for _, _, b, ok in look if ok) # first node that actually has the data +LAST_RETURN = max(b for _, _, b, _ in look) + +HOLD = 5.0 +ARROWS_GONE = LAST_RETURN + 0.6 +CYCLE = ARROWS_GONE + HOLD + 0.6 +OUT0, OUT1 = CYCLE - 0.6, CYCLE - 0.2 # persistent elements fade out before loop + + +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'') + + +def dot(wire_id, color, opa_pts, mot_pts): + return f''' + + {anim_opacity(opa_pts)} + {anim_motion(wire_id, mot_pts)} + ''' + + +def answer_dot(wire_id, color_down, launch, reach, back): + # query goes up blue, answer comes back coloured (green=valid, red=no data) + up = [(0, 0), (launch, 0), (launch+0.15, 1), (reach-0.05, 1), (reach, 0), (CYCLE, 0)] + down = [(0, 0), (reach, 0), (reach+0.05, 1), (back-0.05, 1), (back+0.1, 0), (CYCLE, 0)] + mot = [(0, 1), (launch, 1), (reach, 0), (back, 1), (CYCLE, 1)] + return f''' + {anim_motion(wire_id, mot)} + {anim_opacity(up)} + {anim_opacity(down)} + ''' + + +# ============================ static backdrop ============================ +cloud = [' ', ' '] +for cx, cy, r in cloud_circles: + cloud.append(f' ') +cloud.append(f' Mainline DHT') +cloud.append(' ') +cloud = "\n".join(cloud) + +# nodes are black; a published node turns green when its message arrives (one is never reached) +nd = [' ', ' '] +for x, y in nodes: + nd.append(f' ') +for i, (x, y) in enumerate(pub_nodes): + arr = pub[i][1] + g_pts = [(0, 0), (arr, 0), (arr+0.3, 1), (OUT0, 1), (OUT1, 0), (CYCLE, 0)] + nd.append(f' {anim_opacity(g_pts)}') +nd.append(' ') +node_dots = "\n".join(nd) + +bob = f''' + +{phone(bx0, by0, "8e2b…")} + Bob + + + NodeId: 8e2b… + Home relay: us-east + ''' + +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)] +alice = f''' + + {anim_opacity(alice_pts)} +{phone(ax0, ay0, "1a9c…", iphone=True)} + Alice + + 8e2b… is at relay us-east{anim_opacity(result_pts)}''' + +# ============================ wires + dots ============================ +pub_wire_pts = [(0, 0), (0.6, 0), (1.0, 1), (PUB_DONE, 1), (PUB_DONE+0.5, 0), (CYCLE, 0)] +look_wire_pts = [(0, 0), (LOOK_LAUNCH-0.6, 0), (LOOK_LAUNCH, 1), (LAST_RETURN, 1), (LAST_RETURN+0.6, 0), (CYCLE, 0)] +wires, packets = [], [] +for i, (d, arr) in enumerate(pub): + wires.append(f' {anim_opacity(pub_wire_pts)}') + opa = [(0, 0), (PUB_LAUNCH, 0), (PUB_LAUNCH+0.15, 1), (arr-0.05, 1), (arr+0.1, 0), (CYCLE, 0)] + mot = [(0, 0), (PUB_LAUNCH, 0), (arr, 1), (CYCLE, 1)] + packets.append(dot(f"pw{i}", BLUE, opa, mot)) +for i, (d, reach, back, ok) in enumerate(look): + wires.append(f' {anim_opacity(look_wire_pts)}') + packets.append(answer_dot(f"lw{i}", GREEN if ok else RED, LOOK_LAUNCH, reach, back)) +wires = "\n".join(wires) +packets = "\n".join(packets) + +# ============================ callouts ============================ +pub_co_pts = [(0, 0), (1.6, 0), (2.0, 1), (PUB_DONE, 1), (PUB_DONE+0.5, 0), (CYCLE, 0)] +pub_co = f''' + + {anim_opacity(pub_co_pts)} + + DHT put + 8e2b… + Relay: us-east + ''' + +# DHT get callout (read request) — same box as the answer, shown until the answer arrives +read_pts = [(0, 0), (LOOK_LAUNCH-0.3, 0), (LOOK_LAUNCH+0.1, 1), (FIRST_VALID-0.1, 1), (FIRST_VALID+0.3, 0), (CYCLE, 0)] +read_co = f''' + + {anim_opacity(read_pts)} + + DHT get + 8e2b… + ''' + +# answer popup is visible while the dots are still travelling, then vanishes +ans_pts = [(0, 0), (FIRST_VALID, 0), (FIRST_VALID+0.4, 1), (LAST_RETURN, 1), (LAST_RETURN+0.6, 0), (CYCLE, 0)] +ans_co = f''' + + {anim_opacity(ans_pts)} + + ;; ANSWER SECTION: + TXT "relay=https://us-east" + ''' + +VB_X, VB_Y, VB_W, VB_H = 0, 44, 820, 376 +svg = f''' + +{cloud} + +{wires} + +{node_dots} + +{bob} + +{alice} + +{packets} + +{pub_co} + +{read_co} + +{ans_co} + + + + + + +''' + +open(OUT_PATH, "w").write(svg) +print(f"wrote {len(svg)} bytes -> {OUT_PATH} (CYCLE={CYCLE:.1f}s, first valid answer at {FIRST_VALID:.1f}s)") diff --git a/scripts/publish-relay.gen.py b/scripts/publish-relay.gen.py new file mode 100644 index 00000000..af52b000 --- /dev/null +++ b/scripts/publish-relay.gen.py @@ -0,0 +1,284 @@ +#!/usr/bin/env python3 +"""Generator for publish-relay.svg (the "Publishing the home relay" figure). + +Adapted from rklaehn's "DNS discovery" slide, restyled to match the other +how-iroh-works figures. No map. Our device "Bob" (left) publishes its current +home relay to the vanilla DNS server dns.iroh.link with a signed DNS packet sent +as an HTTPS PUT (green). Alice (right) resolves it with a DNS lookup (blue). +Continues the endpoint-startup story (Bob's home relay = us-east, key 8e2b...). + +One master loop (CYCLE seconds), driven entirely by SMIL so every element stays +in sync: publish once -> record added -> Alice resolves once -> arrows fade out +-> final state holds ~10s -> loop. + +To change it, edit this script and run: python3 publish-relay.gen.py +It writes ../public/animations/publish-relay.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", "publish-relay.svg")) + +MONO = "'Space Mono', monospace" +INDIGO, AMBER, GRAY, INK = "#6366f1", "#d97706", "#888", "#111" +GREEN, BLUE = "#15803d", "#2563eb" + +CYCLE = 14.5 # seconds for one full loop (~5s static hold) + + +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'') + + +def arc(x1, y1, x2, y2, k=0.16): + mx, my = (x1+x2)/2, (y1+y2)/2 + dx, dy = x2-x1, y2-y1 + L = math.hypot(dx, dy) + px, py = -dy/L, dx/L + if py > 0: + px, py = -px, -py + cx, cy = mx+px*k*L, my+py*k*L + return f"M {x1:.0f} {y1:.0f} Q {cx:.0f} {cy:.0f} {x2:.0f} {y2:.0f}" + + +def key_label(cx, y, text, size, color): + """A tiny drawn gold key (bow + shaft + teeth) + monospace label, centered on cx + (replaces the 🔑 emoji, which no pure-vector renderer can draw).""" + s = size + gold = "#eab308" + w = s * 1.3 # key glyph width + gap = s * 0.3 + xl = cx - (w + gap + s * 0.6 * len(text)) / 2 + cy = y - s * 0.30 + rb, rh = s * 0.34, s * 0.15 # bow outer / hole radius + bx = xl + rb # bow center x + hs = s * 0.18 # shaft thickness + xr = xl + w # right end + top, bot = cy - hs / 2, cy + hs / 2 + tw = s * 0.13 # tooth width + mono = "'Space Mono', monospace" + return ( + f'' + f'' + f'' + f'' + f'{text}' + ) + + +def iphone_screen(px0, py0, pw, ph, indigo): + """iPhone-style screen outline path (with notch) sized to fit inside an outer body of pw x ph.""" + sx0, sx1 = px0+3, px0+pw-3 + sy0, sy1 = py0+3, py0+ph-3 + cx = px0 + pw/2 + no = 7 # outer notch half-width + ni = 4 # inner notch half-width + nd = 5 # notch depth + ny = sy0 + nd + r = 6 # screen corner radius + return ( + f'' + ) + +def phone(px0, py0, keytext, iphone=False): + pw, ph = 48, 88 + cx = px0+pw/2 + if iphone: + screen = iphone_screen(px0, py0, pw, ph, INDIGO) + else: + screen = f'\n ' + return f''' + {screen} + + iroh + {key_label(cx, py0+58, keytext, 8, AMBER)}''' + + +def doc_packet(wire_id, color, opa_pts, mot_pts): + # a simple dot riding a wire (the popup explains what the request is) + return f''' + + {anim_opacity(opa_pts)} + {anim_motion(wire_id, mot_pts)} + ''' + + +# ============================ static backdrop ============================ + +# dns.iroh.link server, top-center (vanilla 3U rack, gray LEDs) +sx, sy, sw, sh = 378, 56, 64, 66 +scx = sx+sw/2 +srv = [' ', ' ', + f' '] +for i in range(3): + ry = sy + 4 + i*21 + srv.append(f' ') + srv.append(f' ') + for j in range(6): + vx = sx+24+j*5 + srv.append(f' ') + if i < 2: + yy = sy+4+(i+1)*21-2 + srv.append(f' ') +srv.append(f' dns.iroh.link') +srv.append(' ') +server = "\n".join(srv) + +# Bob (left, publisher) — always present +bx0, by0 = 96, 296 +bcx = bx0+24 +PH = 88 +bob = f''' + +{phone(bx0, by0, "8e2b…")} + Bob + + + NodeId: 8e2b… + Home relay: us-east + ''' + +# ============================ records on the server ============================ +rec_x = sx+sw+30 +existing = [("1f3a…", "eu-west"), ("7c0e…", "us-west"), + ("b48d…", "us-east"), ("2a91…", "eu-west")] +rlh, ry0 = 16, 60 +rec_lines = [f' {k} → {r}' + for i, (k, r) in enumerate(existing)] +ynew = ry0 + len(existing)*rlh +rec_pts = [(0, 0), (3.5, 0), (4.0, 1), (13.8, 1), (14.2, 0), (CYCLE, 0)] +record = f''' + + +{chr(10).join(rec_lines)} + 8e2b… → us-east{anim_opacity(rec_pts)} + ''' + +# ============================ Alice (right, resolver) ============================ +ax0, ay0 = 664, 296 +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)] +alice = f''' + + {anim_opacity(alice_pts)} +{phone(ax0, ay0, "1a9c…", iphone=True)} + Alice + + + 8e2b… is at relay us-east{anim_opacity(result_pts)}''' + +# ============================ wires ============================ +put_d = arc(bcx, by0, sx+18, sy+sh, k=0.18) # Bob -> server +lookup_d = arc(sx+sw-18, sy+sh, acx, ay0, k=0.18) # server -> Alice (answer direction) +put_wire_pts = [(0, 0), (0.6, 0), (1.0, 1), (4.0, 1), (4.5, 0), (CYCLE, 0)] +lookup_wire_pts = [(0, 0), (4.4, 0), (5.0, 1), (8.3, 1), (8.8, 0), (CYCLE, 0)] +wires = f''' {anim_opacity(put_wire_pts)} + {anim_opacity(lookup_wire_pts)}''' + +# ============================ packets ============================ +# PUT: Bob(0) -> server(1), fades out as it lands (~3.5s) +put_pkt = doc_packet("put-wire", BLUE, + [(0, 0), (1.5, 0), (1.7, 1), (3.4, 1), (3.6, 0), (CYCLE, 0)], + [(0, 0), (1.5, 0), (3.5, 1), (CYCLE, 1)]) +# LOOKUP: on lookup-wire 0=server,1=Alice. Query Alice(1)->server(0), answer server(0)->Alice(1). +lookup_pkt = doc_packet("lookup-wire", BLUE, + [(0, 0), (5.0, 0), (5.2, 1), (7.9, 1), (8.1, 0), (CYCLE, 0)], + [(0, 1), (5.0, 1), (6.5, 0), (8.0, 1), (CYCLE, 1)]) + +# ============================ callouts ============================ +put_co_pts = [(0, 0), (1.6, 0), (2.0, 1), (4.0, 1), (4.5, 0), (CYCLE, 0)] +put_co = f''' + + {anim_opacity(put_co_pts)} + + HTTPS PUT + Relay: us-east + Signed by: 8e2b… + ''' + +lookup_co_pts = [(0, 0), (4.6, 0), (5.0, 1), (8.3, 1), (8.8, 0), (CYCLE, 0)] +# the query content is shown on the way up, then replaced by the answer as the dot heads back (~6.5s) +query_pts = [(0, 0), (4.6, 0), (5.0, 1), (6.3, 1), (6.6, 0), (CYCLE, 0)] +answer_pts = [(0, 0), (6.4, 0), (6.7, 1), (8.3, 1), (8.7, 0), (CYCLE, 0)] +lookup_co = f''' + + {anim_opacity(lookup_co_pts)} + + + {anim_opacity(query_pts)} + DNS LOOKUP + TXT _iroh.8e2b….dns.iroh.link + + + {anim_opacity(answer_pts)} + ;; ANSWER SECTION: + TXT "relay=https://us-east" + + ''' + +VB_X, VB_Y, VB_W, VB_H = 0, 44, 820, 376 +svg = f''' + + + + + + + + + + +{wires} + +{server} + +{record} + +{bob} + +{alice} + +{put_pkt} +{lookup_pkt} + +{put_co} + +{lookup_co} + + + + + + +''' + +open(OUT_PATH, "w").write(svg) +print(f"wrote {len(svg)} bytes -> {OUT_PATH}") diff --git a/scripts/routing-moves.gen.py b/scripts/routing-moves.gen.py new file mode 100644 index 00000000..fe36911d --- /dev/null +++ b/scripts/routing-moves.gen.py @@ -0,0 +1,316 @@ +#!/usr/bin/env python3 +"""Generator for routing-moves.svg. + +Bob's commute story: home wifi (R1) → cellular (R2, cell tower) → in-flight +(R3, Starlink ground station). Alice stays put; the connection follows Bob. +20s loop, all SMIL so every element shares one timeline (no CSS/SMIL drift). + +To change it, edit this script and run: python3 routing-moves.gen.py +It writes ../public/animations/routing-moves.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", "routing-moves.svg")) + +MONO = "'Space Mono', monospace" +INDIGO, AMBER, GRAY = "#6366f1", "#d97706", "#888" +CYCLE = 20 # seconds + +VB_W, VB_H = 800, 380 + +# Animation thresholds (fractions of the cycle): +# 0.25 — Bob starts moving (and R2 fades in) +# 0.30 — single-bend path swaps to multi R1→R2 +# 0.55 — R2 + multi R1→R2 fade out +# 0.57 — R3 + multi R1→R3 fade in +# 0.75 — Bob reaches final position + + +def key_label(cx, y, text, size, color): + """Tiny drawn key glyph + monospace text, replacing the 🔑 emoji.""" + s = size + gold = "#eab308" + w = s * 1.3 + gap = s * 0.3 + xl = cx - (w + gap + s * 0.6 * len(text)) / 2 + cy = y - s * 0.30 + rb, rh = s * 0.34, s * 0.15 + bx = xl + rb + hs = s * 0.18 + xr = xl + w + top, bot = cy - hs / 2, cy + hs / 2 + tw = s * 0.13 + return ( + f'' + f'' + f'' + f'' + f'{text}' + ) + + +def iphone_outline(cx, top, bottom): + """iPhone outer body + screen path with notch in the top. Centered at cx, body height = bottom-top.""" + bx0 = cx - 45 # body 90 wide + sx0, sx1 = cx - 42, cx + 42 # screen 84 wide + sy0, sy1 = top + 3, bottom - 3 # screen y span + # Notch geometry (matches the original hand-tuned shape): + # outer corners at (cx-15, sy0) and (cx+15, sy0) — 30px wide + # inner notch bottom at (cx-9 .. cx+9) at y = sy0 + 9 + n_outer_l, n_outer_r = cx - 15, cx + 15 + n_inner_l, n_inner_r = cx - 9, cx + 9 + n_bottom = sy0 + 9 + body = f'' + path = ( + f'' + ) + return body + "\n " + path + + +def phone_iphone(cx, top, key, label, *, ip_text=None, animated_ips=None): + """Static iPhone — used for Alice.""" + lines = [f' '] + if ip_text is not None: + lines.append(f' {ip_text}') + if animated_ips: + for ip in animated_ips: + lines.append(ip) + lines += [ + f' {iphone_outline(cx, top, top + 150)}', + f' ', + f' iroh', + f' {key_label(cx, top + 95, key, 10, AMBER)}', + f' {label}', + f' ', + ] + return "\n".join(lines) + + +def phone_android(cx, top, key, label, *, animated_ips=None): + """Android phone — used for Bob (with movement and IP changes).""" + bx0 = cx - 45 + sx0 = cx - 42 + lines = [' '] + lines.append(f' ') + if animated_ips: + for ip in animated_ips: + lines.append(' ' + ip) + lines += [ + f' ', + f' ', + f' ', + f' ', + f' iroh', + f' {key_label(cx, top + 95, key, 10, AMBER)}', + f' {label}', + ' ', + ] + return "\n".join(lines) + + +def bob_ip(text, key_times, vals, *, initial_opacity=None): + extra = '' if initial_opacity is None else f' opacity="{initial_opacity}"' + return ( + f'' + f'{text}' + f'' + ) + + +def morph_path(d0, mid_d, k_mid_start, k_mid_end, stroke_attrs): + """Path with d-morph animation between two shapes.""" + return ( + f' \n' + f' \n' + ) + + +# ============================ Alice (static iPhone) ============================ +alice_block = phone_iphone(120, 190, "a4f7c0…", "Alice", ip_text="10.0.0.42") + +# ============================ Bob (Android, moves) ============================ +bob_ips = [ + bob_ip("10.0.0.7", "0;0.29;0.31;1", "1;1;0;0"), + bob_ip("192.168.1.7", "0;0.29;0.31;0.55;0.57;1", "0;0;1;1;0;0", initial_opacity="0"), + bob_ip("172.16.0.7", "0;0.55;0.57;1", "0;0;1;1", initial_opacity="0"), +] +bob_block = phone_android(240, 190, "8e2b1d…", "Bob", animated_ips=bob_ips) + +# ============================ Connection paths ============================ +single_d = "M 120 240 Q 120 161 180 161 Q 240 161 240 240" +single_d2 = "M 120 240 Q 120 161 180 161 Q 285 161 285 240" +single_path = ( + f' \n' + f' \n' + f' \n' + f' \n' + f' \n' +) + +multi_r1r2_d = "M 120 240 C 120 170 180 200 180 130 L 180 90 C 180 30 420 30 420 90 L 420 130 C 420 200 285 170 285 240" +multi_r1r2_d2 = "M 120 240 C 120 170 180 200 180 130 L 180 90 C 180 30 420 30 420 90 L 420 130 C 420 200 528 170 528 240" +multi_r1r2_path = ( + f' \n' + f' \n' + f' \n' + f' \n' + f' \n' +) + +multi_r1r3_d = "M 120 240 C 120 170 180 200 180 130 L 180 90 C 180 10 660 10 660 90 L 660 130 C 660 200 552 170 552 240" +multi_r1r3_d2 = "M 120 240 C 120 170 180 200 180 130 L 180 90 C 180 10 660 10 660 90 L 660 130 C 660 200 720 170 720 240" +multi_r1r3_path = ( + f' \n' + f' \n' + f' \n' + f' \n' + f' \n' +) + +# ============================ Routers ============================ +# R1 — home wifi (always visible). Public IP fades in once multi-routing kicks in. +r1 = f''' + + home router + + 203.0.113.1 + + + + + + + 10.0.0.1 + ''' + +# R2 — cell tower (appears 5s, fades out at 11s). 3 panels on a mast with crossbar. +r2 = f''' + + + + mobile network + + 198.51.100.1 + + + + + + 192.168.1.1 + ''' + +# R3 — Starlink ground station (fades in at 11s). Flat tilted parallelogram + kickstand. +r3 = f''' + + + + satellite internet + + 100.64.10.1 + + + 172.16.0.1 + ''' + +# ============================ Timer bar ============================ +timer = f''' + + + + + ''' + +# ============================ Assemble ============================ +svg = f''' + + + + + + + + +{alice_block} + + +{bob_block} + +{single_path} +{multi_r1r2_path} +{multi_r1r3_path} + +{r1} + +{r2} + +{r3} + + +{timer} + +''' + +open(OUT_PATH, "w").write(svg) +print(f"wrote {len(svg)} bytes -> {OUT_PATH}")