(function () { const signalWord = document.getElementById("signalWord"); const palette = { text: "#f6f1e4", muted: "rgba(246, 241, 228, 0.52)", line: "rgba(246, 241, 228, 0.18)", teal: "#39d4c5", green: "#8fe388", amber: "#f0b84a", coral: "#f06f5f" }; const topologies = { vps: { caption: "A VPS shape starts with one source tree, renders transport outputs, then hands them to service-specific daemons on the host.", mobile: { source: [0.12, 0.46], build: [0.31, 0.46], outputs: [0.38, 0.25], state: [0.38, 0.68], vps: [0.55, 0.46], web: [0.72, 0.22], tor: [0.74, 0.4], i2p: [0.72, 0.6], gemini: [0.56, 0.8], reticulum: [0.84, 0.78], visitor: [0.9, 0.5] }, nodes: [ ["source", "source\nsite", 0.11, 0.48, "amber"], ["build", "AMPG\nbuild", 0.27, 0.48, "teal"], ["outputs", "rendered\noutputs", 0.42, 0.3, "green"], ["state", "deploy\nstate", 0.42, 0.62, "amber"], ["vps", "VPS\nhost", 0.56, 0.46, "green"], ["web", "HTTPS\nnginx", 0.73, 0.23, "teal"], ["tor", "Tor\nonion", 0.76, 0.42, "coral"], ["i2p", "I2P\ntunnel", 0.73, 0.61, "amber"], ["gemini", "Gemini\ncapsule", 0.6, 0.78, "green"], ["reticulum", "RNS\nnode", 0.86, 0.76, "coral"], ["visitor", "visitor", 0.93, 0.48, "green"] ], links: [ ["source", "build"], ["build", "outputs", -0.08], ["build", "state", 0.08], ["outputs", "vps"], ["state", "vps"], ["vps", "web", -0.08], ["vps", "tor"], ["vps", "i2p", 0.04], ["vps", "gemini", 0.1], ["vps", "reticulum", 0.12], ["web", "visitor", 0.08], ["tor", "visitor"], ["i2p", "visitor", -0.05], ["gemini", "visitor", -0.12], ["reticulum", "visitor", -0.08] ] }, router: { caption: "Behind a router, clearnet depends on DNS and a chosen inbound path; onion, I2P, and Reticulum routes can publish without exposing normal web ports.", mobile: { visitor: [0.1, 0.44], dns: [0.26, 0.24], router: [0.42, 0.43], host: [0.58, 0.43], ampg: [0.72, 0.43], clearnet: [0.56, 0.23], onion: [0.55, 0.67], i2p: [0.77, 0.67], rns: [0.86, 0.28] }, nodes: [ ["visitor", "visitor", 0.1, 0.42, "green"], ["dns", "DNS /\nDDNS", 0.25, 0.24, "amber"], ["router", "router\nNAT", 0.4, 0.42, "coral"], ["host", "home\nhost", 0.55, 0.42, "teal"], ["ampg", "AMPG", 0.7, 0.42, "teal"], ["clearnet", "port\nforward", 0.55, 0.22, "green"], ["onion", "Tor\nonion", 0.68, 0.69, "coral"], ["i2p", "I2P\nserver\ntunnel", 0.84, 0.61, "amber"], ["rns", "RNS\npeer", 0.88, 0.3, "green"] ], links: [ ["visitor", "dns"], ["dns", "router"], ["router", "host"], ["host", "ampg"], ["ampg", "clearnet", -0.04], ["clearnet", "router"], ["ampg", "onion", 0.05], ["ampg", "i2p", 0.04], ["ampg", "rns", -0.05], ["visitor", "onion", 0.16], ["visitor", "i2p", 0.12], ["visitor", "rns", -0.14] ] }, phone: { caption: "A spare Android phone can run Termux packages, local AMPG state, and user-space daemons for small always-on sites.", mobile: { phone: [0.13, 0.47], termux: [0.31, 0.26], state: [0.31, 0.67], ampg: [0.48, 0.47], daemons: [0.64, 0.47], network: [0.8, 0.27], routes: [0.82, 0.62], power: [0.5, 0.82], visitor: [0.92, 0.45] }, nodes: [ ["phone", "Android\nphone", 0.14, 0.46, "green"], ["termux", "Termux\npackages", 0.31, 0.28, "amber"], ["state", "AMPG\nstate", 0.31, 0.64, "coral"], ["ampg", "AMPG\nbuild", 0.48, 0.46, "teal"], ["daemons", "user-space\ndaemons", 0.65, 0.46, "green"], ["network", "Wi-Fi /\ncell", 0.79, 0.28, "amber"], ["routes", "published\nroutes", 0.86, 0.58, "teal"], ["power", "power /\nstorage", 0.5, 0.78, "coral"], ["visitor", "visitor", 0.94, 0.4, "green"] ], links: [ ["phone", "termux", -0.05], ["phone", "state"], ["phone", "power", 0.08], ["termux", "ampg"], ["state", "ampg"], ["ampg", "daemons"], ["daemons", "network", -0.05], ["daemons", "routes", 0.05], ["power", "routes", -0.1], ["network", "visitor", -0.04], ["routes", "visitor", 0.08] ] }, browser: { caption: "Browser validation consumes AMPG fixture manifests, chooses an adapter, then verifies each route through the intended transport profile.", mobile: { manifest: [0.12, 0.48], ampb: [0.3, 0.48], policy: [0.45, 0.25], adapters: [0.48, 0.57], existing: [0.66, 0.34], managed: [0.66, 0.7], context: [0.81, 0.5], result: [0.92, 0.5], visitor: [0.91, 0.74] }, nodes: [ ["manifest", "fixture\nmanifest", 0.12, 0.43, "amber"], ["ampb", "AMPB", 0.29, 0.43, "teal"], ["policy", "route\npolicy", 0.45, 0.25, "green"], ["adapters", "transport\nadapters", 0.5, 0.53, "coral"], ["existing", "existing\ndaemon", 0.67, 0.32, "green"], ["managed", "managed\ndaemon", 0.67, 0.64, "amber"], ["context", "browser\ncontext", 0.82, 0.47, "teal"], ["result", "health\nresult", 0.93, 0.47, "green"], ["visitor", "visitor", 0.93, 0.7, "green"] ], links: [ ["manifest", "ampb"], ["ampb", "policy", -0.08], ["ampb", "adapters", 0.06], ["policy", "adapters"], ["adapters", "existing", -0.08], ["adapters", "managed", 0.08], ["existing", "context"], ["managed", "context"], ["context", "result"], ["context", "visitor", 0.08], ["visitor", "result"], ["result", "ampb", 0.16] ] } }; const protocolTopologies = { clearnet: { motif: "clearnet", caption: "Clearnet splits into a DNS discovery path and an HTTPS request path before the web server reads AMPG output.", mobile: { client: [0.13, 0.54], resolver: [0.28, 0.34], auth: [0.47, 0.25], ip: [0.49, 0.46], tls: [0.32, 0.74], ingress: [0.55, 0.7], proxy: [0.7, 0.52], output: [0.84, 0.65], health: [0.82, 0.32] }, nodes: [ ["client", "visitor\nbrowser", 0.11, 0.48, "green"], ["resolver", "recursive\nDNS", 0.29, 0.25, "amber"], ["auth", "authoritative\nDNS", 0.5, 0.18, "amber"], ["ip", "public\naddress", 0.49, 0.42, "teal"], ["tls", "TLS\nhandshake", 0.34, 0.66, "green"], ["ingress", "HTTPS\ningress", 0.57, 0.62, "teal"], ["proxy", "nginx /\nproxy", 0.72, 0.42, "coral"], ["output", "AMPG\nclearnet\nfiles", 0.86, 0.56, "teal"], ["health", "transport\nhealth\ncheck", 0.82, 0.23, "green"] ], links: [ ["client", "resolver", -0.1], ["resolver", "auth", -0.06], ["auth", "ip"], ["resolver", "client", 0.1], ["client", "tls", 0.08], ["tls", "ingress"], ["ingress", "proxy"], ["proxy", "output", 0.05], ["health", "ingress"], ["health", "output", 0.05] ] }, tor: { motif: "onion", caption: "Tor onion traffic builds client and service circuits through relays; the rendezvous point joins them without revealing the origin.", mobile: { browser: [0.1, 0.52], hsdir: [0.28, 0.24], descriptor: [0.47, 0.21], guard: [0.28, 0.5], middle: [0.45, 0.66], rendezvous: [0.61, 0.5], intro: [0.68, 0.28], service: [0.78, 0.52], local: [0.88, 0.36], output: [0.88, 0.7], health: [0.62, 0.78] }, nodes: [ ["browser", "Tor\nBrowser", 0.09, 0.5, "green"], ["hsdir", "HSDir\nrelays", 0.28, 0.2, "amber"], ["descriptor", "onion\nservice\ndescriptor", 0.48, 0.2, "amber"], ["guard", "client\nguard", 0.28, 0.48, "teal"], ["middle", "middle\nrelay", 0.46, 0.6, "coral"], ["rendezvous", "rendezvous\nrelay", 0.62, 0.46, "teal"], ["intro", "intro\npoints", 0.68, 0.24, "green"], ["service", "onion\nservice", 0.78, 0.52, "coral"], ["local", "local\nHTTP", 0.88, 0.36, "teal"], ["output", "AMPG\nprivacy\nHTML", 0.88, 0.69, "teal"], ["health", "onion\nhealth\ncheck", 0.62, 0.78, "green"] ], links: [ ["browser", "hsdir", -0.1], ["hsdir", "descriptor"], ["descriptor", "intro"], ["browser", "guard"], ["guard", "middle", 0.05], ["middle", "rendezvous", 0.05], ["intro", "service", 0.08], ["service", "rendezvous", -0.1], ["service", "local"], ["service", "output"], ["health", "guard", 0.08], ["health", "rendezvous", -0.08], ["health", "output", 0.04] ] }, i2p: { motif: "garlic", caption: "I2P uses netDb LeaseSets and one-way tunnels; client outbound tunnels meet server inbound tunnels inside the I2P network.", mobile: { browser: [0.1, 0.52], address: [0.25, 0.25], floodfill: [0.46, 0.23], leaseset: [0.64, 0.25], outbound: [0.28, 0.62], routers: [0.5, 0.7], inbound: [0.7, 0.62], server: [0.84, 0.42], garlic: [0.5, 0.45], output: [0.88, 0.7], health: [0.72, 0.82] }, nodes: [ ["browser", "I2P\nbrowser", 0.09, 0.52, "green"], ["address", "b32 /\naddressbook", 0.25, 0.26, "amber"], ["floodfill", "floodfill\nnetDb", 0.46, 0.22, "amber"], ["leaseset", "LeaseSet", 0.64, 0.24, "green"], ["outbound", "client\noutbound\ntunnel", 0.3, 0.6, "teal"], ["routers", "I2P\ntransit\nrouters", 0.5, 0.66, "coral"], ["inbound", "server\ninbound\ntunnel", 0.7, 0.6, "teal"], ["server", "i2pd\nserver\ntunnel", 0.84, 0.42, "coral"], ["garlic", "garlic\nmessage\nbundle", 0.5, 0.43, "green"], ["output", "AMPG\nprivacy\nHTML", 0.9, 0.67, "teal"], ["health", "I2P\nhealth\ncheck", 0.76, 0.78, "green"] ], links: [ ["browser", "address", -0.08], ["address", "floodfill"], ["floodfill", "leaseset"], ["leaseset", "server", -0.06], ["browser", "outbound", 0.05], ["outbound", "garlic", -0.06], ["garlic", "routers", 0.04], ["outbound", "routers", 0.04], ["routers", "inbound", 0.04], ["inbound", "server"], ["server", "output", 0.05], ["health", "outbound", 0.08], ["health", "output", -0.06] ] }, gemini: { motif: "gemini", caption: "Gemini is intentionally simple: resolve the capsule, open TLS on 1965, and serve text-first AMPG Gemtext.", mobile: { client: [0.12, 0.54], name: [0.28, 0.3], cert: [0.5, 0.24], tcp: [0.42, 0.52], daemon: [0.62, 0.52], gemtext: [0.78, 0.31], output: [0.86, 0.58], links: [0.62, 0.76], health: [0.84, 0.8] }, nodes: [ ["client", "Gemini\nclient", 0.12, 0.5, "green"], ["name", "capsule\nname", 0.28, 0.24, "amber"], ["cert", "server\ncertificate", 0.48, 0.2, "green"], ["tcp", "TCP\n1965", 0.42, 0.5, "teal"], ["daemon", "Gemini\ndaemon", 0.62, 0.5, "coral"], ["gemtext", "Gemtext\ntree", 0.78, 0.3, "teal"], ["output", "AMPG\nGemtext", 0.86, 0.58, "green"], ["links", "capsule\nlinks", 0.62, 0.73, "amber"], ["health", "Gemini\nhealth\ncheck", 0.84, 0.78, "green"] ], links: [ ["client", "name", -0.07], ["name", "tcp"], ["client", "tcp"], ["cert", "tcp"], ["tcp", "daemon"], ["daemon", "gemtext", -0.08], ["daemon", "output"], ["output", "links", 0.07], ["links", "client", 0.16], ["health", "tcp", 0.1], ["health", "output", -0.06] ] }, reticulum: { motif: "reticulum", caption: "Reticulum routes to destination identities across known paths and peers; it is transport-flexible, not automatically anonymous.", mobile: { client: [0.11, 0.52], cache: [0.28, 0.27], request: [0.28, 0.66], mesh: [0.49, 0.46], transport: [0.62, 0.24], identity: [0.72, 0.46], nomad: [0.84, 0.34], output: [0.88, 0.64], health: [0.72, 0.8] }, nodes: [ ["client", "RNS\nclient", 0.1, 0.5, "green"], ["cache", "path\ncache", 0.28, 0.24, "amber"], ["request", "path\nrequest", 0.29, 0.61, "teal"], ["mesh", "Reticulum\nmesh\npeers", 0.49, 0.44, "coral"], ["transport", "transport\nnode", 0.62, 0.2, "green"], ["identity", "destination\nidentity", 0.72, 0.44, "amber"], ["nomad", "NomadNet\nnode", 0.84, 0.33, "teal"], ["output", "AMPG\nMicron", 0.88, 0.62, "green"], ["health", "RNS\nhealth\ncheck", 0.72, 0.76, "green"] ], links: [ ["client", "cache", -0.08], ["client", "request", 0.08], ["request", "mesh"], ["cache", "mesh"], ["mesh", "transport", -0.06], ["mesh", "identity"], ["transport", "identity", 0.06], ["identity", "nomad"], ["nomad", "output"], ["output", "client", 0.16], ["health", "mesh", 0.1], ["health", "output", -0.06] ] }, ipfs: { motif: "ipfs", caption: "IPFS-style publishing addresses AMPG output by content hash, then discovers providers and fetches blocks from peers or gateways.", mobile: { browser: [0.1, 0.54], name: [0.28, 0.27], dnslink: [0.27, 0.68], dht: [0.49, 0.46], routing: [0.63, 0.25], bitswap: [0.66, 0.62], peers: [0.8, 0.43], blocks: [0.89, 0.25], output: [0.9, 0.66], health: [0.72, 0.8] }, nodes: [ ["browser", "browser\nor gateway", 0.1, 0.5, "green"], ["name", "CID /\nIPNS", 0.28, 0.24, "amber"], ["dnslink", "DNSLink", 0.27, 0.65, "amber"], ["dht", "DHT\nprovider\nrecords", 0.49, 0.44, "coral"], ["routing", "peer\nrouting", 0.63, 0.22, "green"], ["bitswap", "Bitswap /\nGraphsync", 0.66, 0.58, "teal"], ["peers", "IPFS\npeer\ncluster", 0.8, 0.4, "coral"], ["blocks", "content\nblocks", 0.89, 0.23, "teal"], ["output", "AMPG\nstatic\ntree", 0.9, 0.65, "green"], ["health", "CID\nhealth\ncheck", 0.72, 0.78, "green"] ], links: [ ["browser", "name", -0.08], ["browser", "dnslink", 0.08], ["name", "dht"], ["dnslink", "dht"], ["dht", "routing", -0.05], ["dht", "bitswap", 0.05], ["routing", "peers"], ["bitswap", "peers"], ["peers", "blocks", -0.05], ["peers", "output", 0.05], ["output", "browser", 0.16], ["health", "dht", 0.1], ["health", "output", -0.05] ] } }; if (window.CanvasRenderingContext2D && !CanvasRenderingContext2D.prototype.roundRect) { CanvasRenderingContext2D.prototype.roundRect = function (x, y, w, h, r) { this.beginPath(); this.moveTo(x + r, y); this.lineTo(x + w - r, y); this.quadraticCurveTo(x + w, y, x + w, y + r); this.lineTo(x + w, y + h - r); this.quadraticCurveTo(x + w, y + h, x + w - r, y + h); this.lineTo(x + r, y + h); this.quadraticCurveTo(x, y + h, x, y + h - r); this.lineTo(x, y + r); this.quadraticCurveTo(x, y, x + r, y); this.closePath(); }; } function createTopologyMap(options) { const graphSet = options.graphSet || topologies; const canvas = document.getElementById(options.canvasId); const caption = options.captionId ? document.getElementById(options.captionId) : null; const buttons = options.buttonSelector ? Array.from(document.querySelectorAll(options.buttonSelector)) : []; if (!canvas) return; const ctx = canvas.getContext("2d"); if (!ctx) return; let active = options.initial || "vps"; let width = 0; let height = 0; const start = performance.now(); function resize() { const rect = canvas.getBoundingClientRect(); const ratio = Math.min(window.devicePixelRatio || 1, 2); width = Math.max(1, rect.width); height = Math.max(1, rect.height); canvas.width = Math.floor(width * ratio); canvas.height = Math.floor(height * ratio); ctx.setTransform(ratio, 0, 0, ratio, 0, 0); } function nodeMap(topology) { return new Map(topology.nodes.map((node) => [node[0], node])); } function draw(now) { const topology = graphSet[active]; const nodes = nodeMap(topology); const tick = (now - start) / 1000; ctx.clearRect(0, 0, width, height); drawField(tick); drawMotif(topology, tick); topology.links.forEach((link, index) => { drawLink(nodes.get(link[0]), nodes.get(link[1]), tick, index, link[2] || 0); }); topology.nodes.forEach((node, index) => drawNode(node, tick, index)); requestAnimationFrame(draw); } function drawField(tick) { ctx.save(); ctx.globalAlpha = options.fieldAlpha || 0.38; const dotCount = options.dotCount || 36; for (let i = 0; i < dotCount; i += 1) { const x = ((i * 137 + tick * 16) % (width + 120)) - 60; const y = ((i * 71 + tick * 9) % (height + 120)) - 60; ctx.fillStyle = i % 3 === 0 ? palette.teal : i % 3 === 1 ? palette.amber : palette.green; ctx.beginPath(); ctx.arc(x, y, i % 2 === 0 ? 1.6 : 1, 0, Math.PI * 2); ctx.fill(); } ctx.restore(); } function drawMotif(topology, tick) { if (topology.motif === "onion") { drawOnionMotif(tick); } if (topology.motif === "garlic") { drawGarlicMotif(tick); } } function drawOnionMotif(tick) { const centerX = width * 0.58; const centerY = height * 0.5; const maxRadius = Math.min(width, height) * 0.28; ctx.save(); ctx.globalAlpha = 0.34; for (let ring = 0; ring < 5; ring += 1) { const radius = maxRadius * (0.34 + ring * 0.15); ctx.strokeStyle = ring % 2 === 0 ? "rgba(240, 111, 95, 0.32)" : "rgba(57, 212, 197, 0.26)"; ctx.lineWidth = 1.5; ctx.beginPath(); ctx.ellipse(centerX, centerY, radius * 1.3, radius * 0.82, -0.1, 0, Math.PI * 2); ctx.stroke(); } for (let dot = 0; dot < 14; dot += 1) { const angle = tick * 0.42 + dot * 0.72; const radius = maxRadius * (0.42 + (dot % 4) * 0.12); const x = centerX + Math.cos(angle) * radius * 1.25; const y = centerY + Math.sin(angle) * radius * 0.78; ctx.fillStyle = dot % 2 === 0 ? palette.coral : palette.teal; ctx.beginPath(); ctx.arc(x, y, 2.1, 0, Math.PI * 2); ctx.fill(); } ctx.restore(); } function drawGarlicMotif(tick) { const centerX = width * 0.5; const centerY = height * 0.52; const radius = Math.min(width, height) * 0.13; const cloves = [ [-1.05, -0.12, 0.98], [-0.38, -0.42, 1.12], [0.34, -0.4, 1.1], [1.02, -0.1, 0.98], [-0.52, 0.34, 0.94], [0.48, 0.35, 0.94] ]; ctx.save(); ctx.globalAlpha = 0.32; cloves.forEach(([offsetX, offsetY, scale], index) => { const pulse = Math.sin(tick * 1.4 + index) * 4; ctx.fillStyle = index % 2 === 0 ? "rgba(240, 184, 74, 0.2)" : "rgba(143, 227, 136, 0.16)"; ctx.strokeStyle = index % 2 === 0 ? "rgba(240, 184, 74, 0.46)" : "rgba(143, 227, 136, 0.38)"; ctx.lineWidth = 1.6; ctx.beginPath(); ctx.ellipse( centerX + offsetX * radius, centerY + offsetY * radius, radius * 0.62 * scale + pulse, radius * 0.86 * scale, offsetX * 0.22, 0, Math.PI * 2 ); ctx.fill(); ctx.stroke(); }); ctx.strokeStyle = "rgba(57, 212, 197, 0.34)"; ctx.lineWidth = 2; ctx.beginPath(); ctx.arc(centerX, centerY, radius * 1.82, tick * 0.6, tick * 0.6 + Math.PI * 1.35); ctx.stroke(); ctx.restore(); } function drawLink(from, to, tick, index, curveOffset) { if (!from || !to) return; const fromPosition = nodePosition(from); const toPosition = nodePosition(to); const ax = fromPosition.x; const ay = fromPosition.y; const bx = toPosition.x; const by = toPosition.y; const curve = Number(curveOffset) || 0; const cx = (ax + bx) / 2 - (by - ay) * curve; const cy = (ay + by) / 2 + (bx - ax) * curve; ctx.save(); ctx.strokeStyle = palette.line; ctx.lineWidth = options.lineWidth || 1; ctx.beginPath(); ctx.moveTo(ax, ay); if (curve) { ctx.quadraticCurveTo(cx, cy, bx, by); } else { ctx.lineTo(bx, by); } ctx.stroke(); const progress = (tick * 0.22 + index * 0.13) % 1; const inverse = 1 - progress; const px = curve ? inverse * inverse * ax + 2 * inverse * progress * cx + progress * progress * bx : ax + (bx - ax) * progress; const py = curve ? inverse * inverse * ay + 2 * inverse * progress * cy + progress * progress * by : ay + (by - ay) * progress; ctx.fillStyle = index % 2 === 0 ? palette.teal : palette.amber; ctx.beginPath(); ctx.arc(px, py, responsivePacketRadius(options.packetRadius || 3.5), 0, Math.PI * 2); ctx.fill(); ctx.restore(); } function drawNode(node, tick, index) { const [, label, nx, ny, colorKey] = node; const position = nodePosition(node); const x = position.x; const y = position.y; const lines = String(label).split("\n"); const boxWidth = responsiveNodeWidth(options.nodeWidth || 116); const fontSize = responsiveFontSize(options.fontSize || 13); const lineHeight = responsiveLineHeight(options.lineHeight || 15); const boxHeight = Math.max(responsiveNodeHeight(options.nodeHeight || 46), lines.length * lineHeight + 18); const pulse = Math.sin(tick * 2.2 + index) * 2; const color = palette[colorKey] || palette.teal; ctx.save(); ctx.shadowColor = color; ctx.shadowBlur = responsiveShadowBlur(options.shadowBlur || 20); ctx.fillStyle = "rgba(16, 16, 15, 0.86)"; ctx.strokeStyle = color; ctx.lineWidth = 2; ctx.beginPath(); ctx.roundRect(x - boxWidth / 2, y - boxHeight / 2, boxWidth, boxHeight, 8); ctx.fill(); ctx.stroke(); ctx.shadowBlur = 0; ctx.fillStyle = palette.text; ctx.font = `${options.fontWeight || 700} ${fontSize}px system-ui, sans-serif`; ctx.textAlign = "center"; ctx.textBaseline = "middle"; const firstLineY = y - ((lines.length - 1) * lineHeight) / 2; lines.forEach((line, lineIndex) => { ctx.fillText(line, x, firstLineY + lineIndex * lineHeight + pulse * 0.25); }); ctx.restore(); } function responsiveNodeWidth(baseWidth) { if (width < 520) return Math.max(70, Math.min(baseWidth, width * 0.215)); if (width < 720) return Math.max(92, Math.min(baseWidth, width * 0.18)); if (width < 980) return Math.max(112, Math.min(baseWidth, width * 0.18)); return baseWidth; } function responsiveNodeHeight(baseHeight) { if (width < 520) return Math.min(baseHeight, 42); if (width < 720) return Math.min(baseHeight, 48); return baseHeight; } function responsiveFontSize(baseSize) { if (width < 520) return Math.min(baseSize, 11.5); if (width < 720) return Math.min(baseSize, 12.5); return baseSize; } function responsiveLineHeight(baseLineHeight) { if (width < 520) return Math.min(baseLineHeight, 12); if (width < 720) return Math.min(baseLineHeight, 13); return baseLineHeight; } function responsiveShadowBlur(baseBlur) { if (width < 520) return Math.min(baseBlur, 14); if (width < 720) return Math.min(baseBlur, 18); return baseBlur; } function responsivePacketRadius(baseRadius) { if (width < 520) return Math.min(baseRadius, 3.2); if (width < 720) return Math.min(baseRadius, 3.8); return baseRadius; } function nodePosition(node) { if (!options.wrapNodes || width >= 760) { return { x: node[2] * width, y: node[3] * height }; } const topology = graphSet[active]; const mobilePosition = topology.mobile && topology.mobile[node[0]]; if (mobilePosition) { const x = Math.min(0.88, Math.max(0.12, mobilePosition[0])); const y = Math.min(0.84, Math.max(0.26, mobilePosition[1])); return { x: x * width, y: y * height }; } const x = 0.12 + node[2] * 0.76; const y = 0.1 + node[3] * 0.68; return { x: x * width, y: y * height }; } function setTopology(name) { if (!graphSet[name]) return; active = name; if (caption) { caption.textContent = graphSet[name].caption; } buttons.forEach((button) => { const selected = button.dataset[options.dataKey] === name; button.classList.toggle("active", selected); button.setAttribute("aria-pressed", selected ? "true" : "false"); }); } buttons.forEach((button) => { button.addEventListener("click", () => setTopology(button.dataset[options.dataKey])); }); if (options.autoCycleMs) { const names = Object.keys(graphSet); window.setInterval(() => { const currentIndex = names.indexOf(active); setTopology(names[(currentIndex + 1) % names.length]); }, options.autoCycleMs); } window.addEventListener("resize", resize); resize(); setTopology(active); requestAnimationFrame(draw); } function createMapModeTabs() { const tabs = Array.from(document.querySelectorAll("[data-map-mode]")); const title = document.getElementById("map-stage-title"); const eyebrow = document.getElementById("map-stage-eyebrow"); const description = document.getElementById("map-stage-description"); if (tabs.length === 0) return; function setMode(mode) { tabs.forEach((tab) => { const selected = tab.dataset.mapMode === mode; tab.classList.toggle("active", selected); tab.setAttribute("aria-selected", selected ? "true" : "false"); if (selected) { title.textContent = tab.dataset.mapTitle; eyebrow.textContent = tab.dataset.mapEyebrow; description.textContent = tab.dataset.mapDescription; } }); document.querySelectorAll(".map-canvas").forEach((canvas) => { canvas.classList.toggle( "active", (mode === "deployment" && canvas.id === "cleanTopologyCanvas") || (mode === "protocol" && canvas.id === "protocolTopologyCanvas") ); }); document.querySelectorAll("[data-map-controls]").forEach((panel) => { const selected = panel.dataset.mapControls === mode; panel.hidden = !selected; panel.classList.toggle("active", selected); }); window.dispatchEvent(new Event("resize")); } tabs.forEach((tab) => { tab.addEventListener("click", () => setMode(tab.dataset.mapMode)); }); setMode("deployment"); } createTopologyMap({ canvasId: "topologyCanvas", initial: "vps" }); createTopologyMap({ canvasId: "cleanTopologyCanvas", captionId: "cleanTopologyCaption", buttonSelector: "[data-clean-topology]", dataKey: "cleanTopology", initial: "vps", dotCount: 70, fieldAlpha: 0.5, lineWidth: 1.4, packetRadius: 4.5, nodeWidth: 144, nodeHeight: 54, fontSize: 15, fontWeight: 800, shadowBlur: 26, wrapNodes: true }); createTopologyMap({ canvasId: "protocolTopologyCanvas", captionId: "protocolTopologyCaption", buttonSelector: "[data-protocol-topology]", dataKey: "protocolTopology", initial: "clearnet", graphSet: protocolTopologies, dotCount: 65, fieldAlpha: 0.48, lineWidth: 1.5, packetRadius: 4.5, nodeWidth: 138, nodeHeight: 58, fontSize: 14, lineHeight: 14, fontWeight: 800, shadowBlur: 24, wrapNodes: true }); createMapModeTabs(); if (signalWord) { const words = ["clearnet", "Tor", "I2P", "Gemini", "IPFS", "Reticulum"]; let wordIndex = 0; window.setInterval(() => { wordIndex = (wordIndex + 1) % words.length; signalWord.textContent = words[wordIndex]; signalWord.classList.remove("swap"); void signalWord.offsetWidth; signalWord.classList.add("swap"); }, 1800); } })();