From 34434d082a9d4fe8479bbbfcbde297bdc63b4d92 Mon Sep 17 00:00:00 2001 From: jwinterm Date: Tue, 7 Jul 2026 13:00:33 -0400 Subject: [PATCH] Show AMPGateway maps as primary visuals --- sites/ampgateway-online/app.js | 302 ++++++++++++++++------------- sites/ampgateway-online/index.html | 133 ++++++++----- sites/ampgateway-online/styles.css | 165 ++++++++++++++-- 3 files changed, 402 insertions(+), 198 deletions(-) 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