diff --git a/sites/ampgateway-online/app.js b/sites/ampgateway-online/app.js
index c4e469a..420c92a 100644
--- a/sites/ampgateway-online/app.js
+++ b/sites/ampgateway-online/app.js
@@ -1,18 +1,6 @@
(function () {
- const canvas = document.getElementById("topologyCanvas");
- const caption = document.getElementById("topologyCaption");
- const buttons = Array.from(document.querySelectorAll("[data-topology]"));
const signalWord = document.getElementById("signalWord");
- if (!canvas || !caption || buttons.length === 0) {
- return;
- }
-
- const ctx = canvas.getContext("2d");
- if (!ctx) {
- return;
- }
-
const palette = {
text: "#f6f1e4",
muted: "rgba(246, 241, 228, 0.52)",
@@ -110,127 +98,7 @@
}
};
- const words = ["clearnet", "Tor", "I2P", "Gemini", "IPFS", "Reticulum"];
- let wordIndex = 0;
- let active = "vps";
- let width = 0;
- let height = 0;
- let 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 = topologies[active];
- const nodes = nodeMap(topology);
- const tick = (now - start) / 1000;
- ctx.clearRect(0, 0, width, height);
- drawField(tick);
- topology.links.forEach((link, index) => drawLink(nodes.get(link[0]), nodes.get(link[1]), tick, index));
- topology.nodes.forEach((node, index) => drawNode(node, tick, index));
- requestAnimationFrame(draw);
- }
-
- function drawField(tick) {
- ctx.save();
- ctx.globalAlpha = 0.38;
- for (let i = 0; i < 36; 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 drawLink(from, to, tick, index) {
- if (!from || !to) return;
- const ax = from[2] * width;
- const ay = from[3] * height;
- const bx = to[2] * width;
- const by = to[3] * height;
- ctx.save();
- ctx.strokeStyle = palette.line;
- ctx.lineWidth = 1;
- ctx.beginPath();
- ctx.moveTo(ax, ay);
- 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;
- ctx.fillStyle = index % 2 === 0 ? palette.teal : palette.amber;
- ctx.beginPath();
- ctx.arc(px, py, 3.5, 0, Math.PI * 2);
- ctx.fill();
- ctx.restore();
- }
-
- function drawNode(node, tick, index) {
- const [, label, nx, ny, colorKey] = node;
- const x = nx * width;
- const y = ny * height;
- const pulse = Math.sin(tick * 2.2 + index) * 2;
- const color = palette[colorKey] || palette.teal;
- ctx.save();
- ctx.shadowColor = color;
- ctx.shadowBlur = 20;
- ctx.fillStyle = "rgba(16, 16, 15, 0.86)";
- ctx.strokeStyle = color;
- ctx.lineWidth = 2;
- ctx.beginPath();
- ctx.roundRect(x - 58, y - 23, 116, 46, 8);
- ctx.fill();
- ctx.stroke();
- ctx.shadowBlur = 0;
- ctx.fillStyle = palette.text;
- ctx.font = "700 13px system-ui, sans-serif";
- ctx.textAlign = "center";
- ctx.textBaseline = "middle";
- ctx.fillText(label, x, y + pulse * 0.25);
- ctx.restore();
- }
-
- function setTopology(name) {
- if (!topologies[name]) return;
- active = name;
- caption.textContent = topologies[name].caption;
- buttons.forEach((button) => {
- const selected = button.dataset.topology === name;
- button.classList.toggle("active", selected);
- button.setAttribute("aria-pressed", selected ? "true" : "false");
- });
- }
-
- buttons.forEach((button) => {
- button.addEventListener("click", () => setTopology(button.dataset.topology));
- });
-
- if (signalWord) {
- window.setInterval(() => {
- wordIndex = (wordIndex + 1) % words.length;
- signalWord.textContent = words[wordIndex];
- signalWord.classList.remove("swap");
- void signalWord.offsetWidth;
- signalWord.classList.add("swap");
- }, 1800);
- }
-
- if (!CanvasRenderingContext2D.prototype.roundRect) {
+ if (window.CanvasRenderingContext2D && !CanvasRenderingContext2D.prototype.roundRect) {
CanvasRenderingContext2D.prototype.roundRect = function (x, y, w, h, r) {
this.beginPath();
this.moveTo(x + r, y);
@@ -246,8 +114,168 @@
};
}
- window.addEventListener("resize", resize);
- resize();
- setTopology(active);
- requestAnimationFrame(draw);
+ function createTopologyMap(options) {
+ 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 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 = topologies[active];
+ const nodes = nodeMap(topology);
+ const tick = (now - start) / 1000;
+ ctx.clearRect(0, 0, width, height);
+ drawField(tick);
+ topology.links.forEach((link, index) => {
+ drawLink(nodes.get(link[0]), nodes.get(link[1]), tick, index);
+ });
+ 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 drawLink(from, to, tick, index) {
+ if (!from || !to) return;
+ const ax = from[2] * width;
+ const ay = from[3] * height;
+ const bx = to[2] * width;
+ const by = to[3] * height;
+ ctx.save();
+ ctx.strokeStyle = palette.line;
+ ctx.lineWidth = options.lineWidth || 1;
+ ctx.beginPath();
+ ctx.moveTo(ax, ay);
+ 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;
+ ctx.fillStyle = index % 2 === 0 ? palette.teal : palette.amber;
+ ctx.beginPath();
+ ctx.arc(px, py, options.packetRadius || 3.5, 0, Math.PI * 2);
+ ctx.fill();
+ ctx.restore();
+ }
+
+ function drawNode(node, tick, index) {
+ const [, label, nx, ny, colorKey] = node;
+ const x = nx * width;
+ const y = ny * height;
+ const boxWidth = options.nodeWidth || 116;
+ const boxHeight = options.nodeHeight || 46;
+ const pulse = Math.sin(tick * 2.2 + index) * 2;
+ const color = palette[colorKey] || palette.teal;
+ ctx.save();
+ ctx.shadowColor = color;
+ ctx.shadowBlur = 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} ${options.fontSize || 13}px system-ui, sans-serif`;
+ ctx.textAlign = "center";
+ ctx.textBaseline = "middle";
+ ctx.fillText(label, x, y + pulse * 0.25);
+ ctx.restore();
+ }
+
+ function setTopology(name) {
+ if (!topologies[name]) return;
+ active = name;
+ caption.textContent = topologies[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]));
+ });
+
+ window.addEventListener("resize", resize);
+ resize();
+ setTopology(active);
+ requestAnimationFrame(draw);
+ }
+
+ createTopologyMap({
+ canvasId: "topologyCanvas",
+ captionId: "topologyCaption",
+ buttonSelector: "[data-topology]",
+ dataKey: "topology",
+ 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
+ });
+
+ 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);
+ }
})();
diff --git a/sites/ampgateway-online/index.html b/sites/ampgateway-online/index.html
index 9fd5fb1..95f7b3f 100644
--- a/sites/ampgateway-online/index.html
+++ b/sites/ampgateway-online/index.html
@@ -21,7 +21,8 @@
AMPGateway
- Transports
+ Live Map
+ Protocol Maps
Topologies
Domains
Flow
@@ -45,7 +46,7 @@
without losing the map.
@@ -63,63 +64,107 @@
-
+
-
Traffic flows
-
Same source, different paths through the network.
+
Topology theatre
+
The animated maps, unobstructed.
- AMPG keeps the authoring path consistent while each transport uses the shape
- that fits its network, daemon, and client expectations.
+ The same live topology views from the hero are given a full clean stage here,
+ so the moving routes, nodes, and deployment shapes can be read directly.
-
-
+
+
+
+
View
+
+ VPS
+ Router
+ Phone
+ Browser
+
+
+ A VPS can publish clearnet, Tor, I2P, Gemini, and Reticulum outputs from one AMPG plan.
+
+
+
+
+
+
+
+
Protocol maps
+
How traffic moves on each transport.
+
+ These are the big maps: what the client talks to, where AMPG output sits,
+ and which network pieces carry the request.
+
+
+
+
Clearnet
-
- Visitor resolves DNS for the site domain.
- HTTP or HTTPS reaches the public web server.
- The server reads AMPG clearnet output.
-
+
+ Visitor browser
+ DNS
+ HTTPS ingress
+ Web server
+ AMPG clearnet output
+
+ Best when a public domain and inbound web reachability are available.
-
+
Tor onion
-
- Tor client resolves the onion service descriptor.
- Tor circuits connect to the hidden service.
- The local service serves AMPG privacy HTML.
-
+
+ Tor Browser
+ Tor circuits
+ Onion service
+ Local HTTP
+ AMPG privacy HTML
+
+ The public address is the onion name; the origin can stay behind local loopback.
-
+
I2P tunnel
-
- I2P client resolves the b32 destination.
- Inbound and outbound tunnels carry HTTP.
- i2pd forwards to the local AMPG output.
-
+
+ I2P browser
+ b32 destination
+ I2P tunnels
+ i2pd server tunnel
+ AMPG privacy HTML
+
+ The b32 address points at the I2P destination, not a normal DNS hostname.
-
+
Gemini
-
- Client opens a TLS connection to the capsule.
- Gemini server maps the URL to Gemtext.
- The rendered page keeps links and headings simple.
-
+
+ Gemini client
+ TLS capsule
+ Gemini daemon
+ Gemtext tree
+ AMPG text render
+
+ Small pages, simple links, and text-first structure make the capsule readable.
-
+
Reticulum
-
- Client reaches an RNS destination.
- NomadNet-style pages request compact Micron content.
- AMPG output keeps media and layout intentionally small.
-
+
+ RNS client
+ Reticulum mesh
+ Destination hash
+ NomadNet node
+ AMPG Micron output
+
+ Identity is destination-based, and content should stay compact by default.
-
+
Single transport
-
- Operator enables only the target protocol.
- AMPG builds only the selected output.
- Deploy stages skip unrelated daemons and DNS.
-
+
+ Source site
+ AMPG selection
+ One renderer
+ One daemon
+ One published route
+
+ Useful for Tor-only, I2P-only, Gemini-only, or Reticulum-focused sites.
diff --git a/sites/ampgateway-online/styles.css b/sites/ampgateway-online/styles.css
index 42ee615..b8e77f4 100644
--- a/sites/ampgateway-online/styles.css
+++ b/sites/ampgateway-online/styles.css
@@ -112,7 +112,8 @@ a {
border-bottom: 1px solid var(--line);
}
-#topologyCanvas {
+#topologyCanvas,
+#cleanTopologyCanvas {
position: absolute;
inset: 0;
width: 100%;
@@ -299,7 +300,7 @@ h3 {
.topology-grid,
.domain-grid,
-.transport-flow-grid {
+.protocol-map-stack {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 14px;
@@ -307,7 +308,7 @@ h3 {
.topology-grid article,
.domain-card,
-.transport-flow {
+.protocol-map {
min-height: 190px;
padding: 20px;
border: 1px solid var(--line);
@@ -317,7 +318,7 @@ h3 {
.topology-grid article:nth-child(2),
.domain-card.featured,
-.transport-flow:nth-child(2n) {
+.protocol-map:nth-child(2n) {
background: var(--panel-strong);
}
@@ -325,7 +326,7 @@ h3 {
.domain-card p,
.docs-section p,
.flow-list p,
-.transport-flow li,
+.protocol-map p,
.section-heading p {
color: var(--muted);
}
@@ -334,23 +335,121 @@ h3 {
margin-bottom: 28px;
}
-.transport-flow-grid {
- grid-template-columns: repeat(3, minmax(0, 1fr));
+.live-map-section {
+ width: min(1440px, calc(100% - 36px));
}
-.transport-flow {
- min-height: 250px;
+.map-theatre {
+ position: relative;
+ min-height: clamp(540px, 72vw, 820px);
+ overflow: hidden;
+ border: 1px solid var(--line);
+ border-radius: 8px;
+ background: #10100f;
}
-.transport-flow h3 {
- font-size: 1.35rem;
+.map-theatre::after {
+ content: "";
+ position: absolute;
+ inset: 0;
+ pointer-events: none;
+ background:
+ linear-gradient(rgba(16, 16, 15, 0.08), rgba(16, 16, 15, 0.28)),
+ radial-gradient(circle at 50% 50%, transparent 0 56%, rgba(16, 16, 15, 0.42) 100%);
}
-.transport-flow ol {
+.map-theatre-controls {
+ position: absolute;
+ left: 18px;
+ right: 18px;
+ bottom: 18px;
+ z-index: 2;
display: grid;
- gap: 10px;
- margin: 16px 0 0;
- padding-left: 20px;
+ 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;
+ background: rgba(16, 16, 15, 0.78);
+ backdrop-filter: blur(18px);
+}
+
+.map-theatre-controls .mode-label,
+.map-theatre-controls .topology-caption {
+ margin: 0;
+}
+
+.protocol-map-section {
+ width: min(1360px, calc(100% - 36px));
+}
+
+.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 {
@@ -478,11 +577,33 @@ h3 {
}
.domain-grid,
- .topology-grid,
- .transport-flow-grid {
+ .topology-grid {
grid-template-columns: 1fr;
}
+ .map-theatre-controls,
+ .protocol-map {
+ grid-template-columns: 1fr;
+ }
+
+ .map-theatre {
+ 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 {
grid-template-columns: repeat(2, minmax(0, 1fr));
}
@@ -517,6 +638,16 @@ h3 {
grid-template-columns: 1fr;
}
+ .map-theatre {
+ min-height: 760px;
+ }
+
+ .map-theatre-controls {
+ left: 12px;
+ right: 12px;
+ bottom: 12px;
+ }
+
.flow-list li {
min-height: 0;
}