Animate AMPGateway protocol maps

This commit is contained in:
jwinterm
2026-07-07 13:21:17 -04:00
parent 34434d082a
commit cfffb4c37c
3 changed files with 270 additions and 216 deletions
+163 -12
View File
@@ -98,6 +98,105 @@
} }
}; };
const protocolTopologies = {
clearnet: {
caption: "Clearnet requests resolve DNS, reach HTTPS ingress, and read AMPG clearnet 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"]
],
links: [
["browser", "dns"],
["dns", "ingress"],
["ingress", "server"],
["server", "output"]
]
},
tor: {
caption: "Tor clients use onion-service descriptors and circuits before the local hidden service reaches AMPG privacy HTML.",
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"]
],
links: [
["browser", "descriptor"],
["descriptor", "circuits"],
["circuits", "service"],
["service", "output"]
]
},
i2p: {
caption: "I2P clients resolve a b32 destination, fetch routing data, and traverse tunnels to the server tunnel.",
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"]
],
links: [
["browser", "b32"],
["b32", "leaseset"],
["leaseset", "tunnel"],
["tunnel", "output"]
]
},
gemini: {
caption: "Gemini clients open a TLS capsule request and receive AMPG-rendered 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"]
],
links: [
["client", "address"],
["address", "tls"],
["tls", "daemon"],
["daemon", "output"]
]
},
reticulum: {
caption: "Reticulum clients route to a destination hash; NomadNet-style nodes can serve compact AMPG Micron output.",
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"]
],
links: [
["client", "path"],
["path", "mesh"],
["mesh", "node"],
["node", "output"]
]
},
ipfs: {
caption: "IPFS-style publishing addresses content by CID or IPNS and distributes AMPG static output through 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"]
],
links: [
["browser", "name"],
["name", "routing"],
["routing", "peers"],
["peers", "output"]
]
}
};
if (window.CanvasRenderingContext2D && !CanvasRenderingContext2D.prototype.roundRect) { if (window.CanvasRenderingContext2D && !CanvasRenderingContext2D.prototype.roundRect) {
CanvasRenderingContext2D.prototype.roundRect = function (x, y, w, h, r) { CanvasRenderingContext2D.prototype.roundRect = function (x, y, w, h, r) {
this.beginPath(); this.beginPath();
@@ -115,6 +214,7 @@
} }
function createTopologyMap(options) { function createTopologyMap(options) {
const graphSet = options.graphSet || topologies;
const canvas = document.getElementById(options.canvasId); const canvas = document.getElementById(options.canvasId);
const caption = document.getElementById(options.captionId); const caption = document.getElementById(options.captionId);
const buttons = Array.from(document.querySelectorAll(options.buttonSelector)); const buttons = Array.from(document.querySelectorAll(options.buttonSelector));
@@ -143,7 +243,7 @@
} }
function draw(now) { function draw(now) {
const topology = topologies[active]; const topology = graphSet[active];
const nodes = nodeMap(topology); const nodes = nodeMap(topology);
const tick = (now - start) / 1000; const tick = (now - start) / 1000;
ctx.clearRect(0, 0, width, height); ctx.clearRect(0, 0, width, height);
@@ -172,10 +272,12 @@
function drawLink(from, to, tick, index) { function drawLink(from, to, tick, index) {
if (!from || !to) return; if (!from || !to) return;
const ax = from[2] * width; const fromPosition = nodePosition(from);
const ay = from[3] * height; const toPosition = nodePosition(to);
const bx = to[2] * width; const ax = fromPosition.x;
const by = to[3] * height; const ay = fromPosition.y;
const bx = toPosition.x;
const by = toPosition.y;
ctx.save(); ctx.save();
ctx.strokeStyle = palette.line; ctx.strokeStyle = palette.line;
ctx.lineWidth = options.lineWidth || 1; ctx.lineWidth = options.lineWidth || 1;
@@ -196,10 +298,13 @@
function drawNode(node, tick, index) { function drawNode(node, tick, index) {
const [, label, nx, ny, colorKey] = node; const [, label, nx, ny, colorKey] = node;
const x = nx * width; const position = nodePosition(node);
const y = ny * height; const x = position.x;
const boxWidth = options.nodeWidth || 116; const y = position.y;
const boxHeight = options.nodeHeight || 46; const lines = String(label).split("\n");
const boxWidth = responsiveNodeWidth(options.nodeWidth || 116);
const lineHeight = options.lineHeight || 15;
const boxHeight = Math.max(options.nodeHeight || 46, lines.length * lineHeight + 22);
const pulse = Math.sin(tick * 2.2 + index) * 2; const pulse = Math.sin(tick * 2.2 + index) * 2;
const color = palette[colorKey] || palette.teal; const color = palette[colorKey] || palette.teal;
ctx.save(); ctx.save();
@@ -217,14 +322,40 @@
ctx.font = `${options.fontWeight || 700} ${options.fontSize || 13}px system-ui, sans-serif`; ctx.font = `${options.fontWeight || 700} ${options.fontSize || 13}px system-ui, sans-serif`;
ctx.textAlign = "center"; ctx.textAlign = "center";
ctx.textBaseline = "middle"; ctx.textBaseline = "middle";
ctx.fillText(label, x, y + pulse * 0.25); const firstLineY = y - ((lines.length - 1) * lineHeight) / 2;
lines.forEach((line, lineIndex) => {
ctx.fillText(line, x, firstLineY + lineIndex * lineHeight + pulse * 0.25);
});
ctx.restore(); ctx.restore();
} }
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));
return baseWidth;
}
function nodePosition(node) {
if (!options.wrapNodes || width >= 760) {
return { x: node[2] * width, y: node[3] * height };
}
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 };
}
function setTopology(name) { function setTopology(name) {
if (!topologies[name]) return; if (!graphSet[name]) return;
active = name; active = name;
caption.textContent = topologies[name].caption; caption.textContent = graphSet[name].caption;
buttons.forEach((button) => { buttons.forEach((button) => {
const selected = button.dataset[options.dataKey] === name; const selected = button.dataset[options.dataKey] === name;
button.classList.toggle("active", selected); button.classList.toggle("active", selected);
@@ -267,6 +398,26 @@
shadowBlur: 26 shadowBlur: 26
}); });
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
});
if (signalWord) { if (signalWord) {
const words = ["clearnet", "Tor", "I2P", "Gemini", "IPFS", "Reticulum"]; const words = ["clearnet", "Tor", "I2P", "Gemini", "IPFS", "Reticulum"];
let wordIndex = 0; let wordIndex = 0;
+57 -101
View File
@@ -21,8 +21,7 @@
<span>AMPGateway</span> <span>AMPGateway</span>
</a> </a>
<nav class="top-nav" aria-label="Primary"> <nav class="top-nav" aria-label="Primary">
<a href="#live-map">Live Map</a> <a href="#live-map">Maps</a>
<a href="#protocol-maps">Protocol Maps</a>
<a href="#topologies">Topologies</a> <a href="#topologies">Topologies</a>
<a href="#domains">Domains</a> <a href="#domains">Domains</a>
<a href="#flow">Flow</a> <a href="#flow">Flow</a>
@@ -66,105 +65,63 @@
<section id="live-map" class="section live-map-section" aria-labelledby="live-map-title"> <section id="live-map" class="section live-map-section" aria-labelledby="live-map-title">
<div class="section-heading"> <div class="section-heading">
<p class="eyebrow">Topology theatre</p> <p class="eyebrow">Topology Theater</p>
<h2 id="live-map-title">The animated maps, unobstructed.</h2> <h2 id="live-map-title">Animated maps.</h2>
<p> <p>
The same live topology views from the hero are given a full clean stage here, Deployment maps and protocol maps share one visual language: moving routes,
so the moving routes, nodes, and deployment shapes can be read directly. clear nodes, and transport-specific paths.
</p> </p>
</div> </div>
<div class="map-theatre" aria-label="Interactive topology map"> <div class="map-stack">
<canvas id="cleanTopologyCanvas" aria-hidden="true"></canvas> <article class="map-stage" aria-labelledby="deployment-map-title">
<div class="map-theatre-controls"> <div class="map-stage-heading">
<div class="mode-label">View</div> <div>
<div class="segmented" role="group" aria-label="Clean topology selection"> <p class="eyebrow">View</p>
<button class="mode-button active" type="button" data-clean-topology="vps">VPS</button> <h3 id="deployment-map-title">Deployment shape</h3>
<button class="mode-button" type="button" data-clean-topology="router">Router</button> </div>
<button class="mode-button" type="button" data-clean-topology="phone">Phone</button> <p>VPS, router, phone, and browser validation views show where AMPG sits in the stack.</p>
<button class="mode-button" type="button" data-clean-topology="browser">Browser</button>
</div> </div>
<p id="cleanTopologyCaption" class="topology-caption" aria-live="polite"> <div class="map-theater" aria-label="Interactive deployment topology map">
A VPS can publish clearnet, Tor, I2P, Gemini, and Reticulum outputs from one AMPG plan. <canvas id="cleanTopologyCanvas" aria-hidden="true"></canvas>
</p> <div class="map-theater-controls">
</div> <div class="mode-label">View</div>
</div> <div class="segmented" role="group" aria-label="Deployment topology selection">
</section> <button class="mode-button active" type="button" data-clean-topology="vps">VPS</button>
<button class="mode-button" type="button" data-clean-topology="router">Router</button>
<section id="protocol-maps" class="section protocol-map-section" aria-labelledby="protocol-maps-title"> <button class="mode-button" type="button" data-clean-topology="phone">Phone</button>
<div class="section-heading"> <button class="mode-button" type="button" data-clean-topology="browser">Browser</button>
<p class="eyebrow">Protocol maps</p> </div>
<h2 id="protocol-maps-title">How traffic moves on each transport.</h2> <p id="cleanTopologyCaption" class="topology-caption" aria-live="polite">
<p> A VPS can publish clearnet, Tor, I2P, Gemini, and Reticulum outputs from one AMPG plan.
These are the big maps: what the client talks to, where AMPG output sits, </p>
and which network pieces carry the request. </div>
</p>
</div>
<div class="protocol-map-stack">
<article class="protocol-map">
<h3>Clearnet</h3>
<div class="path-map" aria-label="Clearnet request path">
<span>Visitor browser</span>
<span>DNS</span>
<span>HTTPS ingress</span>
<span>Web server</span>
<span>AMPG clearnet output</span>
</div> </div>
<p>Best when a public domain and inbound web reachability are available.</p>
</article> </article>
<article class="protocol-map"> <article class="map-stage" aria-labelledby="protocol-map-title">
<h3>Tor onion</h3> <div class="map-stage-heading">
<div class="path-map" aria-label="Tor onion request path"> <div>
<span>Tor Browser</span> <p class="eyebrow">Transport</p>
<span>Tor circuits</span> <h3 id="protocol-map-title">Protocol flow</h3>
<span>Onion service</span> </div>
<span>Local HTTP</span> <p>Each transport gets its own animated path instead of a static comparison table.</p>
<span>AMPG privacy HTML</span>
</div> </div>
<p>The public address is the onion name; the origin can stay behind local loopback.</p> <div class="map-theater" aria-label="Interactive protocol flow map">
</article> <canvas id="protocolTopologyCanvas" aria-hidden="true"></canvas>
<article class="protocol-map"> <div class="map-theater-controls protocol-controls">
<h3>I2P tunnel</h3> <div class="mode-label">Transport</div>
<div class="path-map" aria-label="I2P tunnel request path"> <div class="segmented protocol-segmented" role="group" aria-label="Protocol flow selection">
<span>I2P browser</span> <button class="mode-button active" type="button" data-protocol-topology="clearnet">Clearnet</button>
<span>b32 destination</span> <button class="mode-button" type="button" data-protocol-topology="tor">Tor</button>
<span>I2P tunnels</span> <button class="mode-button" type="button" data-protocol-topology="i2p">I2P</button>
<span>i2pd server tunnel</span> <button class="mode-button" type="button" data-protocol-topology="gemini">Gemini</button>
<span>AMPG privacy HTML</span> <button class="mode-button" type="button" data-protocol-topology="reticulum">Reticulum</button>
<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.
</p>
</div>
</div> </div>
<p>The b32 address points at the I2P destination, not a normal DNS hostname.</p>
</article>
<article class="protocol-map">
<h3>Gemini</h3>
<div class="path-map" aria-label="Gemini request path">
<span>Gemini client</span>
<span>TLS capsule</span>
<span>Gemini daemon</span>
<span>Gemtext tree</span>
<span>AMPG text render</span>
</div>
<p>Small pages, simple links, and text-first structure make the capsule readable.</p>
</article>
<article class="protocol-map">
<h3>Reticulum</h3>
<div class="path-map" aria-label="Reticulum request path">
<span>RNS client</span>
<span>Reticulum mesh</span>
<span>Destination hash</span>
<span>NomadNet node</span>
<span>AMPG Micron output</span>
</div>
<p>Identity is destination-based, and content should stay compact by default.</p>
</article>
<article class="protocol-map">
<h3>Single transport</h3>
<div class="path-map" aria-label="Single transport deployment path">
<span>Source site</span>
<span>AMPG selection</span>
<span>One renderer</span>
<span>One daemon</span>
<span>One published route</span>
</div>
<p>Useful for Tor-only, I2P-only, Gemini-only, or Reticulum-focused sites.</p>
</article> </article>
</div> </div>
</section> </section>
@@ -262,18 +219,17 @@
<section id="docs" class="section docs-section" aria-labelledby="docs-title"> <section id="docs" class="section docs-section" aria-labelledby="docs-title">
<div> <div>
<p class="eyebrow">Public docs</p> <p class="eyebrow">Public docs</p>
<h2 id="docs-title">The visual site stays tied to the public spec.</h2> <h2 id="docs-title">Source, docs, and websites stay public and reproducible.</h2>
<p> <p>
The source for this page lives in the AMPG repository and can be rendered by AMPG publishes clean public docs, generated references, and website sources
AMPG like any other static site. The Markdown docs stay concise; this site in GitHub repositories. Private planning and deployment notes stay outside
turns the same concepts into topology-first views. the public docs tree.
</p> </p>
</div> </div>
<div class="doc-links"> <div class="doc-links">
<a href="#topologies">Topology guide</a> <a href="https://github.com/Such-Software/adaptive-multi-protocol-gateway">Gateway, docs, and sites</a>
<a href="#flow">Deploy stages</a> <a href="https://github.com/Such-Software/adaptive-multi-protocol-browser">Adaptive browser</a>
<a href="#domains">Domain examples</a> <a href="https://github.com/Such-Software">Such Software GitHub</a>
<a href="#top">Transport map</a>
</div> </div>
</section> </section>
</main> </main>
+50 -103
View File
@@ -113,7 +113,8 @@ a {
} }
#topologyCanvas, #topologyCanvas,
#cleanTopologyCanvas { #cleanTopologyCanvas,
#protocolTopologyCanvas {
position: absolute; position: absolute;
inset: 0; inset: 0;
width: 100%; width: 100%;
@@ -299,16 +300,14 @@ h3 {
} }
.topology-grid, .topology-grid,
.domain-grid, .domain-grid {
.protocol-map-stack {
display: grid; display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr)); grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 14px; gap: 14px;
} }
.topology-grid article, .topology-grid article,
.domain-card, .domain-card {
.protocol-map {
min-height: 190px; min-height: 190px;
padding: 20px; padding: 20px;
border: 1px solid var(--line); border: 1px solid var(--line);
@@ -317,8 +316,7 @@ h3 {
} }
.topology-grid article:nth-child(2), .topology-grid article:nth-child(2),
.domain-card.featured, .domain-card.featured {
.protocol-map:nth-child(2n) {
background: var(--panel-strong); background: var(--panel-strong);
} }
@@ -326,7 +324,6 @@ h3 {
.domain-card p, .domain-card p,
.docs-section p, .docs-section p,
.flow-list p, .flow-list p,
.protocol-map p,
.section-heading p { .section-heading p {
color: var(--muted); color: var(--muted);
} }
@@ -339,7 +336,35 @@ h3 {
width: min(1440px, calc(100% - 36px)); width: min(1440px, calc(100% - 36px));
} }
.map-theatre { .map-stack {
display: grid;
gap: clamp(28px, 5vw, 58px);
}
.map-stage {
display: grid;
gap: 18px;
}
.map-stage-heading {
display: grid;
grid-template-columns: minmax(0, 0.58fr) minmax(260px, 0.42fr);
gap: clamp(18px, 4vw, 46px);
align-items: end;
}
.map-stage-heading h3 {
margin-bottom: 0;
font-size: clamp(1.65rem, 3vw, 3.25rem);
line-height: 1.02;
}
.map-stage-heading p {
margin-bottom: 4px;
color: var(--muted);
}
.map-theater {
position: relative; position: relative;
min-height: clamp(540px, 72vw, 820px); min-height: clamp(540px, 72vw, 820px);
overflow: hidden; overflow: hidden;
@@ -348,7 +373,7 @@ h3 {
background: #10100f; background: #10100f;
} }
.map-theatre::after { .map-theater::after {
content: ""; content: "";
position: absolute; position: absolute;
inset: 0; inset: 0;
@@ -358,7 +383,7 @@ h3 {
radial-gradient(circle at 50% 50%, transparent 0 56%, rgba(16, 16, 15, 0.42) 100%); radial-gradient(circle at 50% 50%, transparent 0 56%, rgba(16, 16, 15, 0.42) 100%);
} }
.map-theatre-controls { .map-theater-controls {
position: absolute; position: absolute;
left: 18px; left: 18px;
right: 18px; right: 18px;
@@ -375,81 +400,13 @@ h3 {
backdrop-filter: blur(18px); backdrop-filter: blur(18px);
} }
.map-theatre-controls .mode-label, .map-theater-controls .mode-label,
.map-theatre-controls .topology-caption { .map-theater-controls .topology-caption {
margin: 0; margin: 0;
} }
.protocol-map-section { .protocol-segmented {
width: min(1360px, calc(100% - 36px)); grid-template-columns: repeat(6, minmax(0, 1fr));
}
.protocol-map-stack {
grid-template-columns: 1fr;
gap: 18px;
}
.protocol-map {
display: grid;
grid-template-columns: 180px minmax(0, 1fr) 280px;
gap: clamp(18px, 3vw, 34px);
align-items: center;
min-height: 210px;
}
.protocol-map h3 {
margin-bottom: 0;
font-size: clamp(1.45rem, 2.4vw, 2.4rem);
}
.path-map {
display: grid;
grid-template-columns: repeat(5, minmax(120px, 1fr));
gap: 12px;
align-items: center;
}
.path-map span {
position: relative;
display: grid;
min-height: 78px;
place-items: center;
padding: 12px;
border: 1px solid rgba(246, 241, 228, 0.26);
border-radius: 8px;
background: rgba(246, 241, 228, 0.055);
color: var(--text);
font-size: 0.92rem;
font-weight: 900;
text-align: center;
}
.path-map span:not(:last-child)::after {
content: "";
position: absolute;
top: 50%;
right: -13px;
width: 13px;
height: 2px;
background: var(--amber);
transform: translateY(-50%);
}
.path-map span:nth-child(1) {
border-color: var(--green);
}
.path-map span:nth-child(2),
.path-map span:nth-child(3) {
border-color: var(--amber);
}
.path-map span:nth-child(4) {
border-color: var(--coral);
}
.path-map span:nth-child(5) {
border-color: var(--teal);
} }
.domain-grid { .domain-grid {
@@ -572,7 +529,8 @@ h3 {
} }
.split, .split,
.docs-section { .docs-section,
.map-stage-heading {
grid-template-columns: 1fr; grid-template-columns: 1fr;
} }
@@ -581,29 +539,14 @@ h3 {
grid-template-columns: 1fr; grid-template-columns: 1fr;
} }
.map-theatre-controls, .map-theater-controls {
.protocol-map {
grid-template-columns: 1fr; grid-template-columns: 1fr;
} }
.map-theatre { .map-theater {
min-height: 680px; min-height: 680px;
} }
.path-map {
grid-template-columns: 1fr;
}
.path-map span:not(:last-child)::after {
top: auto;
right: auto;
bottom: -13px;
left: 50%;
width: 2px;
height: 13px;
transform: translateX(-50%);
}
.flow-list { .flow-list {
grid-template-columns: repeat(2, minmax(0, 1fr)); grid-template-columns: repeat(2, minmax(0, 1fr));
} }
@@ -638,16 +581,20 @@ h3 {
grid-template-columns: 1fr; grid-template-columns: 1fr;
} }
.map-theatre { .map-theater {
min-height: 760px; min-height: 760px;
} }
.map-theatre-controls { .map-theater-controls {
left: 12px; left: 12px;
right: 12px; right: 12px;
bottom: 12px; bottom: 12px;
} }
.protocol-segmented {
grid-template-columns: 1fr;
}
.flow-list li { .flow-list li {
min-height: 0; min-height: 0;
} }