mirror of
https://github.com/Such-Software/adaptive-multi-protocol-gateway.git
synced 2026-10-17 07:31:32 +02:00
Refine AMPGateway topology theater
This commit is contained in:
+278
-136
@@ -13,186 +13,264 @@
|
||||
|
||||
const topologies = {
|
||||
vps: {
|
||||
caption: "A VPS can publish clearnet, Tor, I2P, Gemini, and Reticulum outputs from one AMPG plan.",
|
||||
caption: "A VPS shape starts with one source tree, renders transport outputs, then hands them to service-specific daemons on the host.",
|
||||
nodes: [
|
||||
["source", "source site", 0.16, 0.42, "amber"],
|
||||
["ampg", "AMPG", 0.34, 0.42, "teal"],
|
||||
["vps", "VPS", 0.52, 0.42, "green"],
|
||||
["web", "clearnet", 0.72, 0.22, "teal"],
|
||||
["tor", "Tor", 0.78, 0.39, "coral"],
|
||||
["i2p", "I2P", 0.76, 0.56, "amber"],
|
||||
["gemini", "Gemini", 0.66, 0.72, "green"],
|
||||
["reticulum", "Reticulum", 0.84, 0.72, "coral"]
|
||||
["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.88, 0.73, "coral"]
|
||||
],
|
||||
links: [
|
||||
["source", "ampg"],
|
||||
["ampg", "vps"],
|
||||
["vps", "web"],
|
||||
["source", "build"],
|
||||
["build", "outputs", -0.08],
|
||||
["build", "state", 0.08],
|
||||
["outputs", "vps"],
|
||||
["state", "vps"],
|
||||
["vps", "web", -0.08],
|
||||
["vps", "tor"],
|
||||
["vps", "i2p"],
|
||||
["vps", "gemini"],
|
||||
["vps", "reticulum"]
|
||||
["vps", "i2p", 0.04],
|
||||
["vps", "gemini", 0.1],
|
||||
["vps", "reticulum", 0.12]
|
||||
]
|
||||
},
|
||||
router: {
|
||||
caption: "Behind a router, clearnet may need DNS and port choices; Tor, I2P, and Reticulum can avoid depending on inbound web ports.",
|
||||
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.",
|
||||
nodes: [
|
||||
["visitor", "visitor", 0.13, 0.33, "green"],
|
||||
["dns", "DNS/DDNS", 0.3, 0.24, "amber"],
|
||||
["router", "router", 0.45, 0.38, "coral"],
|
||||
["host", "home host", 0.61, 0.38, "teal"],
|
||||
["ampg", "AMPG", 0.76, 0.38, "teal"],
|
||||
["tor", "Tor", 0.64, 0.66, "coral"],
|
||||
["i2p", "I2P", 0.78, 0.66, "amber"],
|
||||
["reticulum", "Reticulum", 0.9, 0.54, "green"]
|
||||
["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", "tor"],
|
||||
["ampg", "i2p"],
|
||||
["ampg", "reticulum"],
|
||||
["visitor", "tor"],
|
||||
["visitor", "i2p"],
|
||||
["visitor", "reticulum"]
|
||||
["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, AMPG state, and user-space transport services.",
|
||||
caption: "A spare Android phone can run Termux packages, local AMPG state, and user-space daemons for small always-on sites.",
|
||||
nodes: [
|
||||
["phone", "Android", 0.24, 0.45, "green"],
|
||||
["pkg", "pkg", 0.43, 0.26, "amber"],
|
||||
["ampg", "AMPG", 0.45, 0.45, "teal"],
|
||||
["state", "state", 0.43, 0.64, "coral"],
|
||||
["services", "services", 0.64, 0.45, "green"],
|
||||
["routes", "routes", 0.82, 0.45, "teal"]
|
||||
["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"]
|
||||
],
|
||||
links: [
|
||||
["phone", "pkg"],
|
||||
["phone", "ampg"],
|
||||
["phone", "termux", -0.05],
|
||||
["phone", "state"],
|
||||
["pkg", "services"],
|
||||
["ampg", "services"],
|
||||
["state", "services"],
|
||||
["services", "routes"]
|
||||
["phone", "power", 0.08],
|
||||
["termux", "ampg"],
|
||||
["state", "ampg"],
|
||||
["ampg", "daemons"],
|
||||
["daemons", "network", -0.05],
|
||||
["daemons", "routes", 0.05],
|
||||
["power", "routes", -0.1]
|
||||
]
|
||||
},
|
||||
browser: {
|
||||
caption: "Fixture manifests let AMPB or another checker verify every route through the expected transport profile.",
|
||||
caption: "Browser validation consumes AMPG fixture manifests, chooses an adapter, then verifies each route through the intended transport profile.",
|
||||
nodes: [
|
||||
["site", "site", 0.17, 0.5, "amber"],
|
||||
["ampg", "AMPG", 0.33, 0.5, "teal"],
|
||||
["manifest", "manifest", 0.5, 0.5, "green"],
|
||||
["ampb", "AMPB", 0.66, 0.5, "coral"],
|
||||
["transport", "transport", 0.82, 0.5, "teal"]
|
||||
["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"]
|
||||
],
|
||||
links: [
|
||||
["site", "ampg"],
|
||||
["ampg", "manifest"],
|
||||
["manifest", "ampb"],
|
||||
["ampb", "transport"],
|
||||
["transport", "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"],
|
||||
["result", "ampb", 0.16]
|
||||
]
|
||||
}
|
||||
};
|
||||
|
||||
const protocolTopologies = {
|
||||
clearnet: {
|
||||
caption: "Clearnet requests resolve DNS, reach HTTPS ingress, and read AMPG clearnet output.",
|
||||
caption: "Clearnet splits into a DNS discovery path and an HTTPS request path before the web server reads AMPG output.",
|
||||
nodes: [
|
||||
["browser", "Visitor\nbrowser", 0.12, 0.44, "green"],
|
||||
["dns", "DNS", 0.31, 0.32, "amber"],
|
||||
["ingress", "HTTPS\ningress", 0.5, 0.44, "teal"],
|
||||
["server", "web\nserver", 0.69, 0.32, "coral"],
|
||||
["output", "AMPG\nclearnet", 0.88, 0.44, "teal"]
|
||||
["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", "public\nhealth\ncheck", 0.82, 0.23, "green"]
|
||||
],
|
||||
links: [
|
||||
["browser", "dns"],
|
||||
["dns", "ingress"],
|
||||
["ingress", "server"],
|
||||
["server", "output"]
|
||||
["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: {
|
||||
caption: "Tor clients use onion-service descriptors and circuits before the local hidden service reaches AMPG privacy HTML.",
|
||||
caption: "Tor onion traffic builds client and service circuits through relays; the rendezvous point joins them without revealing the origin.",
|
||||
nodes: [
|
||||
["browser", "Tor\nBrowser", 0.12, 0.44, "green"],
|
||||
["descriptor", "onion\nservice\ndescriptor", 0.31, 0.32, "amber"],
|
||||
["circuits", "Tor\ncircuits", 0.5, 0.44, "coral"],
|
||||
["service", "onion\nservice", 0.69, 0.32, "teal"],
|
||||
["output", "AMPG\nprivacy\nHTML", 0.88, 0.44, "teal"]
|
||||
["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"]
|
||||
],
|
||||
links: [
|
||||
["browser", "descriptor"],
|
||||
["descriptor", "circuits"],
|
||||
["circuits", "service"],
|
||||
["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"]
|
||||
]
|
||||
},
|
||||
i2p: {
|
||||
caption: "I2P clients resolve a b32 destination, fetch routing data, and traverse tunnels to the server tunnel.",
|
||||
caption: "I2P uses netDb LeaseSets and one-way tunnels; client outbound tunnels meet server inbound tunnels inside the I2P network.",
|
||||
nodes: [
|
||||
["browser", "I2P\nbrowser", 0.12, 0.44, "green"],
|
||||
["b32", "b32\ndestination", 0.31, 0.32, "amber"],
|
||||
["leaseset", "LeaseSet\nlookup", 0.5, 0.44, "coral"],
|
||||
["tunnel", "I2P\nserver\ntunnel", 0.69, 0.32, "teal"],
|
||||
["output", "AMPG\nprivacy\nHTML", 0.88, 0.44, "teal"]
|
||||
["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"],
|
||||
["output", "AMPG\nprivacy\nHTML", 0.9, 0.67, "teal"]
|
||||
],
|
||||
links: [
|
||||
["browser", "b32"],
|
||||
["b32", "leaseset"],
|
||||
["leaseset", "tunnel"],
|
||||
["tunnel", "output"]
|
||||
["browser", "address", -0.08],
|
||||
["address", "floodfill"],
|
||||
["floodfill", "leaseset"],
|
||||
["leaseset", "server", -0.06],
|
||||
["browser", "outbound", 0.05],
|
||||
["outbound", "routers", 0.04],
|
||||
["routers", "inbound", 0.04],
|
||||
["inbound", "server"],
|
||||
["server", "output", 0.05]
|
||||
]
|
||||
},
|
||||
gemini: {
|
||||
caption: "Gemini clients open a TLS capsule request and receive AMPG-rendered Gemtext.",
|
||||
caption: "Gemini is intentionally simple: resolve the capsule, open TLS on 1965, and serve text-first AMPG Gemtext.",
|
||||
nodes: [
|
||||
["client", "Gemini\nclient", 0.12, 0.44, "green"],
|
||||
["address", "capsule\naddress", 0.31, 0.32, "amber"],
|
||||
["tls", "TLS\nrequest", 0.5, 0.44, "teal"],
|
||||
["daemon", "Gemini\ndaemon", 0.69, 0.32, "coral"],
|
||||
["output", "AMPG\nGemtext", 0.88, 0.44, "teal"]
|
||||
["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"]
|
||||
],
|
||||
links: [
|
||||
["client", "address"],
|
||||
["address", "tls"],
|
||||
["tls", "daemon"],
|
||||
["daemon", "output"]
|
||||
["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]
|
||||
]
|
||||
},
|
||||
reticulum: {
|
||||
caption: "Reticulum clients route to a destination hash; NomadNet-style nodes can serve compact AMPG Micron output.",
|
||||
caption: "Reticulum routes to destination identities across known paths and peers; it is transport-flexible, not automatically anonymous.",
|
||||
nodes: [
|
||||
["client", "RNS\nclient", 0.12, 0.44, "green"],
|
||||
["path", "path\ndiscovery", 0.31, 0.32, "amber"],
|
||||
["mesh", "Reticulum\nmesh", 0.5, 0.44, "coral"],
|
||||
["node", "NomadNet\nnode", 0.69, 0.32, "teal"],
|
||||
["output", "AMPG\nMicron", 0.88, 0.44, "teal"]
|
||||
["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"]
|
||||
],
|
||||
links: [
|
||||
["client", "path"],
|
||||
["path", "mesh"],
|
||||
["mesh", "node"],
|
||||
["node", "output"]
|
||||
["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]
|
||||
]
|
||||
},
|
||||
ipfs: {
|
||||
caption: "IPFS-style publishing addresses content by CID or IPNS and distributes AMPG static output through peers or gateways.",
|
||||
caption: "IPFS-style publishing addresses AMPG output by content hash, then discovers providers and fetches blocks from peers or gateways.",
|
||||
nodes: [
|
||||
["browser", "browser\nor gateway", 0.12, 0.44, "green"],
|
||||
["name", "CID\nor IPNS", 0.31, 0.32, "amber"],
|
||||
["routing", "DHT\nproviders", 0.5, 0.44, "coral"],
|
||||
["peers", "IPFS\npeers", 0.69, 0.32, "teal"],
|
||||
["output", "AMPG\nstatic\ntree", 0.88, 0.44, "teal"]
|
||||
["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"]
|
||||
],
|
||||
links: [
|
||||
["browser", "name"],
|
||||
["name", "routing"],
|
||||
["browser", "name", -0.08],
|
||||
["browser", "dnslink", 0.08],
|
||||
["name", "dht"],
|
||||
["dnslink", "dht"],
|
||||
["dht", "routing", -0.05],
|
||||
["dht", "bitswap", 0.05],
|
||||
["routing", "peers"],
|
||||
["peers", "output"]
|
||||
["bitswap", "peers"],
|
||||
["peers", "blocks", -0.05],
|
||||
["peers", "output", 0.05],
|
||||
["output", "browser", 0.16]
|
||||
]
|
||||
}
|
||||
};
|
||||
@@ -216,9 +294,9 @@
|
||||
function createTopologyMap(options) {
|
||||
const graphSet = options.graphSet || topologies;
|
||||
const canvas = document.getElementById(options.canvasId);
|
||||
const caption = document.getElementById(options.captionId);
|
||||
const buttons = Array.from(document.querySelectorAll(options.buttonSelector));
|
||||
if (!canvas || !caption || buttons.length === 0) return;
|
||||
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;
|
||||
@@ -249,7 +327,7 @@
|
||||
ctx.clearRect(0, 0, width, height);
|
||||
drawField(tick);
|
||||
topology.links.forEach((link, index) => {
|
||||
drawLink(nodes.get(link[0]), nodes.get(link[1]), tick, 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);
|
||||
@@ -270,7 +348,7 @@
|
||||
ctx.restore();
|
||||
}
|
||||
|
||||
function drawLink(from, to, tick, index) {
|
||||
function drawLink(from, to, tick, index, curveOffset) {
|
||||
if (!from || !to) return;
|
||||
const fromPosition = nodePosition(from);
|
||||
const toPosition = nodePosition(to);
|
||||
@@ -278,17 +356,29 @@
|
||||
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);
|
||||
ctx.lineTo(bx, by);
|
||||
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 px = ax + (bx - ax) * progress;
|
||||
const py = ay + (by - ay) * progress;
|
||||
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, options.packetRadius || 3.5, 0, Math.PI * 2);
|
||||
@@ -330,8 +420,9 @@
|
||||
}
|
||||
|
||||
function responsiveNodeWidth(baseWidth) {
|
||||
if (width < 720) return Math.max(86, Math.min(baseWidth, width * 0.24));
|
||||
if (width < 980) return Math.max(112, Math.min(baseWidth, width * 0.17));
|
||||
if (width < 520) return Math.max(104, Math.min(baseWidth, width * 0.38));
|
||||
if (width < 720) return Math.max(104, Math.min(baseWidth, width * 0.28));
|
||||
if (width < 980) return Math.max(112, Math.min(baseWidth, width * 0.18));
|
||||
return baseWidth;
|
||||
}
|
||||
|
||||
@@ -341,21 +432,21 @@
|
||||
}
|
||||
const topology = graphSet[active];
|
||||
const index = topology.nodes.findIndex((candidate) => candidate[0] === node[0]);
|
||||
const wrapped = [
|
||||
[0.18, 0.28],
|
||||
[0.5, 0.28],
|
||||
[0.82, 0.28],
|
||||
[0.34, 0.55],
|
||||
[0.66, 0.55],
|
||||
[0.5, 0.76]
|
||||
][index] || [node[2], node[3]];
|
||||
return { x: wrapped[0] * width, y: wrapped[1] * height };
|
||||
const columns = width < 520 ? 2 : 3;
|
||||
const rows = Math.max(1, Math.ceil(topology.nodes.length / columns));
|
||||
const col = index % columns;
|
||||
const row = Math.floor(index / columns);
|
||||
const x = (col + 0.5) / columns;
|
||||
const y = rows === 1 ? 0.34 : 0.15 + (row / (rows - 1)) * 0.48;
|
||||
return { x: x * width, y: y * height };
|
||||
}
|
||||
|
||||
function setTopology(name) {
|
||||
if (!graphSet[name]) return;
|
||||
active = name;
|
||||
caption.textContent = graphSet[name].caption;
|
||||
if (caption) {
|
||||
caption.textContent = graphSet[name].caption;
|
||||
}
|
||||
buttons.forEach((button) => {
|
||||
const selected = button.dataset[options.dataKey] === name;
|
||||
button.classList.toggle("active", selected);
|
||||
@@ -367,18 +458,66 @@
|
||||
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",
|
||||
captionId: "topologyCaption",
|
||||
buttonSelector: "[data-topology]",
|
||||
dataKey: "topology",
|
||||
initial: "vps"
|
||||
initial: "vps",
|
||||
autoCycleMs: 4600
|
||||
});
|
||||
|
||||
createTopologyMap({
|
||||
@@ -395,7 +534,8 @@
|
||||
nodeHeight: 54,
|
||||
fontSize: 15,
|
||||
fontWeight: 800,
|
||||
shadowBlur: 26
|
||||
shadowBlur: 26,
|
||||
wrapNodes: true
|
||||
});
|
||||
|
||||
createTopologyMap({
|
||||
@@ -418,6 +558,8 @@
|
||||
wrapNodes: true
|
||||
});
|
||||
|
||||
createMapModeTabs();
|
||||
|
||||
if (signalWord) {
|
||||
const words = ["clearnet", "Tor", "I2P", "Gemini", "IPFS", "Reticulum"];
|
||||
let wordIndex = 0;
|
||||
|
||||
@@ -49,18 +49,6 @@
|
||||
<a class="button secondary" href="#domains">See example domains</a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="topology-panel" aria-label="Topology modes">
|
||||
<div class="mode-label">Topology</div>
|
||||
<div class="segmented" role="group" aria-label="Topology selection">
|
||||
<button class="mode-button active" type="button" data-topology="vps">VPS</button>
|
||||
<button class="mode-button" type="button" data-topology="router">Router</button>
|
||||
<button class="mode-button" type="button" data-topology="phone">Phone</button>
|
||||
<button class="mode-button" type="button" data-topology="browser">Browser</button>
|
||||
</div>
|
||||
<p id="topologyCaption" class="topology-caption" aria-live="polite">
|
||||
A VPS can publish clearnet, Tor, I2P, Gemini, and Reticulum outputs from one AMPG plan.
|
||||
</p>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section id="live-map" class="section live-map-section" aria-labelledby="live-map-title">
|
||||
@@ -72,18 +60,57 @@
|
||||
clear nodes, and transport-specific paths.
|
||||
</p>
|
||||
</div>
|
||||
<div class="map-stack">
|
||||
<article class="map-stage" aria-labelledby="deployment-map-title">
|
||||
<div class="map-stage-heading">
|
||||
<div>
|
||||
<p class="eyebrow">View</p>
|
||||
<h3 id="deployment-map-title">Deployment shape</h3>
|
||||
</div>
|
||||
<p>VPS, router, phone, and browser validation views show where AMPG sits in the stack.</p>
|
||||
<article class="map-stage" aria-labelledby="map-stage-title">
|
||||
<div class="map-stage-heading">
|
||||
<div>
|
||||
<p id="map-stage-eyebrow" class="eyebrow">View</p>
|
||||
<h3 id="map-stage-title">Deployment shape</h3>
|
||||
</div>
|
||||
<div class="map-theater" aria-label="Interactive deployment topology map">
|
||||
<canvas id="cleanTopologyCanvas" aria-hidden="true"></canvas>
|
||||
<div class="map-theater-controls">
|
||||
<p id="map-stage-description">
|
||||
VPS, router, phone, and browser validation views show where AMPG sits in the stack.
|
||||
</p>
|
||||
</div>
|
||||
<div class="map-theater" aria-label="Interactive topology and protocol map">
|
||||
<canvas id="cleanTopologyCanvas" class="map-canvas active" aria-hidden="true"></canvas>
|
||||
<canvas id="protocolTopologyCanvas" class="map-canvas" aria-hidden="true"></canvas>
|
||||
<div class="map-tabs" role="tablist" aria-label="Map mode">
|
||||
<button
|
||||
id="deployment-map-tab"
|
||||
class="map-tab active"
|
||||
type="button"
|
||||
role="tab"
|
||||
aria-selected="true"
|
||||
aria-controls="deployment-map-controls"
|
||||
data-map-mode="deployment"
|
||||
data-map-title="Deployment shape"
|
||||
data-map-eyebrow="View"
|
||||
data-map-description="VPS, router, phone, and browser validation views show where AMPG sits in the stack."
|
||||
>
|
||||
Deployment shape
|
||||
</button>
|
||||
<button
|
||||
id="protocol-map-tab"
|
||||
class="map-tab"
|
||||
type="button"
|
||||
role="tab"
|
||||
aria-selected="false"
|
||||
aria-controls="protocol-map-controls"
|
||||
data-map-mode="protocol"
|
||||
data-map-title="Protocol flow"
|
||||
data-map-eyebrow="Transport"
|
||||
data-map-description="Transport maps show the real network shape: discovery, routing, service endpoints, and AMPG output."
|
||||
>
|
||||
Protocol flow
|
||||
</button>
|
||||
</div>
|
||||
<div class="map-theater-controls">
|
||||
<div
|
||||
id="deployment-map-controls"
|
||||
class="map-control-panel active"
|
||||
role="tabpanel"
|
||||
aria-labelledby="deployment-map-tab"
|
||||
data-map-controls="deployment"
|
||||
>
|
||||
<div class="mode-label">View</div>
|
||||
<div class="segmented" role="group" aria-label="Deployment topology selection">
|
||||
<button class="mode-button active" type="button" data-clean-topology="vps">VPS</button>
|
||||
@@ -92,22 +119,18 @@
|
||||
<button class="mode-button" type="button" data-clean-topology="browser">Browser</button>
|
||||
</div>
|
||||
<p id="cleanTopologyCaption" class="topology-caption" aria-live="polite">
|
||||
A VPS can publish clearnet, Tor, I2P, Gemini, and Reticulum outputs from one AMPG plan.
|
||||
A VPS shape starts with one source tree, renders transport outputs, then hands them
|
||||
to service-specific daemons on the host.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</article>
|
||||
<article class="map-stage" aria-labelledby="protocol-map-title">
|
||||
<div class="map-stage-heading">
|
||||
<div>
|
||||
<p class="eyebrow">Transport</p>
|
||||
<h3 id="protocol-map-title">Protocol flow</h3>
|
||||
</div>
|
||||
<p>Each transport gets its own animated path instead of a static comparison table.</p>
|
||||
</div>
|
||||
<div class="map-theater" aria-label="Interactive protocol flow map">
|
||||
<canvas id="protocolTopologyCanvas" aria-hidden="true"></canvas>
|
||||
<div class="map-theater-controls protocol-controls">
|
||||
<div
|
||||
id="protocol-map-controls"
|
||||
class="map-control-panel"
|
||||
role="tabpanel"
|
||||
aria-labelledby="protocol-map-tab"
|
||||
data-map-controls="protocol"
|
||||
hidden
|
||||
>
|
||||
<div class="mode-label">Transport</div>
|
||||
<div class="segmented protocol-segmented" role="group" aria-label="Protocol flow selection">
|
||||
<button class="mode-button active" type="button" data-protocol-topology="clearnet">Clearnet</button>
|
||||
@@ -118,12 +141,13 @@
|
||||
<button class="mode-button" type="button" data-protocol-topology="ipfs">IPFS</button>
|
||||
</div>
|
||||
<p id="protocolTopologyCaption" class="topology-caption" aria-live="polite">
|
||||
Clearnet requests resolve DNS, reach HTTPS ingress, and read AMPG clearnet output.
|
||||
Clearnet splits into a DNS discovery path and an HTTPS request path before the web
|
||||
server reads AMPG output.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</article>
|
||||
</div>
|
||||
</div>
|
||||
</article>
|
||||
</section>
|
||||
|
||||
<section id="topologies" class="section split" aria-labelledby="topologies-title">
|
||||
|
||||
@@ -114,13 +114,27 @@ a {
|
||||
|
||||
#topologyCanvas,
|
||||
#cleanTopologyCanvas,
|
||||
#protocolTopologyCanvas {
|
||||
#protocolTopologyCanvas,
|
||||
.map-canvas {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
#topologyCanvas {
|
||||
opacity: 0.64;
|
||||
}
|
||||
|
||||
.map-canvas {
|
||||
opacity: 0;
|
||||
transition: opacity 260ms ease;
|
||||
}
|
||||
|
||||
.map-canvas.active {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.hero::after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
@@ -134,7 +148,7 @@ a {
|
||||
z-index: 1;
|
||||
max-width: var(--max);
|
||||
margin: 0 auto;
|
||||
padding: clamp(118px, 16vh, 168px) clamp(18px, 4vw, 48px) 160px;
|
||||
padding: clamp(118px, 16vh, 168px) clamp(18px, 4vw, 48px) 130px;
|
||||
}
|
||||
|
||||
.eyebrow,
|
||||
@@ -241,19 +255,6 @@ h3 {
|
||||
background: rgba(246, 241, 228, 0.06);
|
||||
}
|
||||
|
||||
.topology-panel {
|
||||
position: absolute;
|
||||
right: clamp(18px, 4vw, 48px);
|
||||
bottom: 30px;
|
||||
z-index: 2;
|
||||
width: min(440px, calc(100% - 36px));
|
||||
padding: 16px;
|
||||
border: 1px solid var(--line);
|
||||
border-radius: 8px;
|
||||
background: rgba(27, 28, 26, 0.82);
|
||||
backdrop-filter: blur(18px);
|
||||
}
|
||||
|
||||
.segmented {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(4, minmax(0, 1fr));
|
||||
@@ -290,6 +291,7 @@ h3 {
|
||||
width: min(var(--max), calc(100% - 36px));
|
||||
margin: 0 auto;
|
||||
padding: clamp(64px, 10vw, 112px) 0;
|
||||
scroll-margin-top: 82px;
|
||||
}
|
||||
|
||||
.split {
|
||||
@@ -333,12 +335,7 @@ h3 {
|
||||
}
|
||||
|
||||
.live-map-section {
|
||||
width: min(1440px, calc(100% - 36px));
|
||||
}
|
||||
|
||||
.map-stack {
|
||||
display: grid;
|
||||
gap: clamp(28px, 5vw, 58px);
|
||||
width: min(1520px, calc(100% - 36px));
|
||||
}
|
||||
|
||||
.map-stage {
|
||||
@@ -366,7 +363,7 @@ h3 {
|
||||
|
||||
.map-theater {
|
||||
position: relative;
|
||||
min-height: clamp(540px, 72vw, 820px);
|
||||
min-height: clamp(640px, 74vw, 880px);
|
||||
overflow: hidden;
|
||||
border: 1px solid var(--line);
|
||||
border-radius: 8px;
|
||||
@@ -383,16 +380,47 @@ h3 {
|
||||
radial-gradient(circle at 50% 50%, transparent 0 56%, rgba(16, 16, 15, 0.42) 100%);
|
||||
}
|
||||
|
||||
.map-tabs {
|
||||
position: absolute;
|
||||
top: 18px;
|
||||
left: 18px;
|
||||
z-index: 2;
|
||||
display: inline-grid;
|
||||
grid-template-columns: repeat(2, minmax(160px, 1fr));
|
||||
gap: 8px;
|
||||
padding: 8px;
|
||||
border: 1px solid var(--line);
|
||||
border-radius: 8px;
|
||||
background: rgba(16, 16, 15, 0.72);
|
||||
backdrop-filter: blur(18px);
|
||||
}
|
||||
|
||||
.map-tab {
|
||||
min-height: 42px;
|
||||
padding: 8px 12px;
|
||||
border: 1px solid transparent;
|
||||
border-radius: 6px;
|
||||
background: transparent;
|
||||
color: var(--muted);
|
||||
font: inherit;
|
||||
font-size: 0.92rem;
|
||||
font-weight: 900;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.map-tab.active,
|
||||
.map-tab:focus-visible {
|
||||
border-color: var(--teal);
|
||||
background: rgba(57, 212, 197, 0.12);
|
||||
color: var(--text);
|
||||
}
|
||||
|
||||
.map-theater-controls {
|
||||
position: absolute;
|
||||
left: 18px;
|
||||
right: 18px;
|
||||
bottom: 18px;
|
||||
z-index: 2;
|
||||
display: grid;
|
||||
grid-template-columns: 84px minmax(0, 520px) minmax(260px, 1fr);
|
||||
gap: 12px;
|
||||
align-items: center;
|
||||
padding: 14px;
|
||||
border: 1px solid var(--line);
|
||||
border-radius: 8px;
|
||||
@@ -400,8 +428,19 @@ h3 {
|
||||
backdrop-filter: blur(18px);
|
||||
}
|
||||
|
||||
.map-theater-controls .mode-label,
|
||||
.map-theater-controls .topology-caption {
|
||||
.map-control-panel {
|
||||
display: grid;
|
||||
grid-template-columns: 84px minmax(0, 650px) minmax(280px, 1fr);
|
||||
gap: 12px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.map-control-panel[hidden] {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.map-control-panel .mode-label,
|
||||
.map-control-panel .topology-caption {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
@@ -517,7 +556,7 @@ h3 {
|
||||
}
|
||||
|
||||
.hero {
|
||||
min-height: 860px;
|
||||
min-height: 780px;
|
||||
}
|
||||
|
||||
.hero::after {
|
||||
@@ -525,7 +564,7 @@ h3 {
|
||||
}
|
||||
|
||||
.hero-inner {
|
||||
padding-bottom: 280px;
|
||||
padding-bottom: 150px;
|
||||
}
|
||||
|
||||
.split,
|
||||
@@ -539,12 +578,12 @@ h3 {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.map-theater-controls {
|
||||
.map-control-panel {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.map-theater {
|
||||
min-height: 680px;
|
||||
min-height: 740px;
|
||||
}
|
||||
|
||||
.flow-list {
|
||||
@@ -572,17 +611,19 @@ h3 {
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.topology-panel {
|
||||
bottom: 18px;
|
||||
}
|
||||
|
||||
.segmented,
|
||||
.flow-list {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.map-theater {
|
||||
min-height: 760px;
|
||||
min-height: 820px;
|
||||
}
|
||||
|
||||
.map-tabs {
|
||||
left: 12px;
|
||||
right: 12px;
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.map-theater-controls {
|
||||
|
||||
Reference in New Issue
Block a user