Improve mobile topology maps

This commit is contained in:
jwinterm
2026-07-07 15:17:42 -04:00
parent 53f5bb4236
commit cb6739c98b
2 changed files with 203 additions and 20 deletions
+163 -14
View File
@@ -14,6 +14,19 @@
const topologies = { const topologies = {
vps: { vps: {
caption: "A VPS shape starts with one source tree, renders transport outputs, then hands them to service-specific daemons on the host.", 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: [ nodes: [
["source", "source\nsite", 0.11, 0.48, "amber"], ["source", "source\nsite", 0.11, 0.48, "amber"],
["build", "AMPG\nbuild", 0.27, 0.48, "teal"], ["build", "AMPG\nbuild", 0.27, 0.48, "teal"],
@@ -47,6 +60,17 @@
}, },
router: { 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.", 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: [ nodes: [
["visitor", "visitor", 0.1, 0.42, "green"], ["visitor", "visitor", 0.1, 0.42, "green"],
["dns", "DNS /\nDDNS", 0.25, 0.24, "amber"], ["dns", "DNS /\nDDNS", 0.25, 0.24, "amber"],
@@ -75,6 +99,17 @@
}, },
phone: { phone: {
caption: "A spare Android phone can run Termux packages, local AMPG state, and user-space daemons for small always-on sites.", 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: [ nodes: [
["phone", "Android\nphone", 0.14, 0.46, "green"], ["phone", "Android\nphone", 0.14, 0.46, "green"],
["termux", "Termux\npackages", 0.31, 0.28, "amber"], ["termux", "Termux\npackages", 0.31, 0.28, "amber"],
@@ -102,6 +137,17 @@
}, },
browser: { browser: {
caption: "Browser validation consumes AMPG fixture manifests, chooses an adapter, then verifies each route through the intended transport profile.", 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: [ nodes: [
["manifest", "fixture\nmanifest", 0.12, 0.43, "amber"], ["manifest", "fixture\nmanifest", 0.12, 0.43, "amber"],
["ampb", "AMPB", 0.29, 0.43, "teal"], ["ampb", "AMPB", 0.29, 0.43, "teal"],
@@ -134,6 +180,17 @@
clearnet: { clearnet: {
motif: "clearnet", motif: "clearnet",
caption: "Clearnet splits into a DNS discovery path and an HTTPS request path before the web server reads AMPG output.", 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: [ nodes: [
["client", "visitor\nbrowser", 0.11, 0.48, "green"], ["client", "visitor\nbrowser", 0.11, 0.48, "green"],
["resolver", "recursive\nDNS", 0.29, 0.25, "amber"], ["resolver", "recursive\nDNS", 0.29, 0.25, "amber"],
@@ -161,6 +218,19 @@
tor: { tor: {
motif: "onion", motif: "onion",
caption: "Tor onion traffic builds client and service circuits through relays; the rendezvous point joins them without revealing the origin.", 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: [ nodes: [
["browser", "Tor\nBrowser", 0.09, 0.5, "green"], ["browser", "Tor\nBrowser", 0.09, 0.5, "green"],
["hsdir", "HSDir\nrelays", 0.28, 0.2, "amber"], ["hsdir", "HSDir\nrelays", 0.28, 0.2, "amber"],
@@ -193,6 +263,19 @@
i2p: { i2p: {
motif: "garlic", motif: "garlic",
caption: "I2P uses netDb LeaseSets and one-way tunnels; client outbound tunnels meet server inbound tunnels inside the I2P network.", 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: [ nodes: [
["browser", "I2P\nbrowser", 0.09, 0.52, "green"], ["browser", "I2P\nbrowser", 0.09, 0.52, "green"],
["address", "b32 /\naddressbook", 0.25, 0.26, "amber"], ["address", "b32 /\naddressbook", 0.25, 0.26, "amber"],
@@ -225,6 +308,17 @@
gemini: { gemini: {
motif: "gemini", motif: "gemini",
caption: "Gemini is intentionally simple: resolve the capsule, open TLS on 1965, and serve text-first AMPG Gemtext.", 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: [ nodes: [
["client", "Gemini\nclient", 0.12, 0.5, "green"], ["client", "Gemini\nclient", 0.12, 0.5, "green"],
["name", "capsule\nname", 0.28, 0.24, "amber"], ["name", "capsule\nname", 0.28, 0.24, "amber"],
@@ -253,6 +347,17 @@
reticulum: { reticulum: {
motif: "reticulum", motif: "reticulum",
caption: "Reticulum routes to destination identities across known paths and peers; it is transport-flexible, not automatically anonymous.", 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: [ nodes: [
["client", "RNS\nclient", 0.1, 0.5, "green"], ["client", "RNS\nclient", 0.1, 0.5, "green"],
["cache", "path\ncache", 0.28, 0.24, "amber"], ["cache", "path\ncache", 0.28, 0.24, "amber"],
@@ -282,6 +387,18 @@
ipfs: { ipfs: {
motif: "ipfs", motif: "ipfs",
caption: "IPFS-style publishing addresses AMPG output by content hash, then discovers providers and fetches blocks from peers or gateways.", 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: [ nodes: [
["browser", "browser\nor gateway", 0.1, 0.5, "green"], ["browser", "browser\nor gateway", 0.1, 0.5, "green"],
["name", "CID /\nIPNS", 0.28, 0.24, "amber"], ["name", "CID /\nIPNS", 0.28, 0.24, "amber"],
@@ -495,7 +612,7 @@
: ay + (by - ay) * progress; : ay + (by - ay) * progress;
ctx.fillStyle = index % 2 === 0 ? palette.teal : palette.amber; ctx.fillStyle = index % 2 === 0 ? palette.teal : palette.amber;
ctx.beginPath(); ctx.beginPath();
ctx.arc(px, py, options.packetRadius || 3.5, 0, Math.PI * 2); ctx.arc(px, py, responsivePacketRadius(options.packetRadius || 3.5), 0, Math.PI * 2);
ctx.fill(); ctx.fill();
ctx.restore(); ctx.restore();
} }
@@ -507,13 +624,14 @@
const y = position.y; const y = position.y;
const lines = String(label).split("\n"); const lines = String(label).split("\n");
const boxWidth = responsiveNodeWidth(options.nodeWidth || 116); const boxWidth = responsiveNodeWidth(options.nodeWidth || 116);
const lineHeight = options.lineHeight || 15; const fontSize = responsiveFontSize(options.fontSize || 13);
const boxHeight = Math.max(options.nodeHeight || 46, lines.length * lineHeight + 22); 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 pulse = Math.sin(tick * 2.2 + index) * 2;
const color = palette[colorKey] || palette.teal; const color = palette[colorKey] || palette.teal;
ctx.save(); ctx.save();
ctx.shadowColor = color; ctx.shadowColor = color;
ctx.shadowBlur = options.shadowBlur || 20; ctx.shadowBlur = responsiveShadowBlur(options.shadowBlur || 20);
ctx.fillStyle = "rgba(16, 16, 15, 0.86)"; ctx.fillStyle = "rgba(16, 16, 15, 0.86)";
ctx.strokeStyle = color; ctx.strokeStyle = color;
ctx.lineWidth = 2; ctx.lineWidth = 2;
@@ -523,7 +641,7 @@
ctx.stroke(); ctx.stroke();
ctx.shadowBlur = 0; ctx.shadowBlur = 0;
ctx.fillStyle = palette.text; ctx.fillStyle = palette.text;
ctx.font = `${options.fontWeight || 700} ${options.fontSize || 13}px system-ui, sans-serif`; ctx.font = `${options.fontWeight || 700} ${fontSize}px system-ui, sans-serif`;
ctx.textAlign = "center"; ctx.textAlign = "center";
ctx.textBaseline = "middle"; ctx.textBaseline = "middle";
const firstLineY = y - ((lines.length - 1) * lineHeight) / 2; const firstLineY = y - ((lines.length - 1) * lineHeight) / 2;
@@ -534,24 +652,55 @@
} }
function responsiveNodeWidth(baseWidth) { function responsiveNodeWidth(baseWidth) {
if (width < 520) return Math.max(104, Math.min(baseWidth, width * 0.38)); if (width < 520) return Math.max(70, Math.min(baseWidth, width * 0.215));
if (width < 720) return Math.max(104, Math.min(baseWidth, width * 0.28)); 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)); if (width < 980) return Math.max(112, Math.min(baseWidth, width * 0.18));
return baseWidth; 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) { function nodePosition(node) {
if (!options.wrapNodes || width >= 760) { if (!options.wrapNodes || width >= 760) {
return { x: node[2] * width, y: node[3] * height }; return { x: node[2] * width, y: node[3] * height };
} }
const topology = graphSet[active]; const topology = graphSet[active];
const index = topology.nodes.findIndex((candidate) => candidate[0] === node[0]); const mobilePosition = topology.mobile && topology.mobile[node[0]];
const columns = width < 520 ? 2 : 3; if (mobilePosition) {
const rows = Math.max(1, Math.ceil(topology.nodes.length / columns)); const x = Math.min(0.88, Math.max(0.12, mobilePosition[0]));
const col = index % columns; const y = Math.min(0.84, Math.max(0.26, mobilePosition[1]));
const row = Math.floor(index / columns); return { x: x * width, y: y * height };
const x = (col + 0.5) / columns; }
const y = rows === 1 ? 0.34 : 0.15 + (row / (rows - 1)) * 0.48; const x = 0.12 + node[2] * 0.76;
const y = 0.1 + node[3] * 0.68;
return { x: x * width, y: y * height }; return { x: x * width, y: y * height };
} }
+40 -6
View File
@@ -596,6 +596,18 @@ h3 {
padding: 14px 18px; padding: 14px 18px;
} }
.section {
padding: 48px 0;
}
.section-heading {
margin-bottom: 20px;
}
.map-stage {
gap: 14px;
}
h1 { h1 {
font-size: clamp(2.75rem, 14vw, 3.45rem); font-size: clamp(2.75rem, 14vw, 3.45rem);
} }
@@ -616,23 +628,45 @@ h3 {
} }
.map-theater { .map-theater {
min-height: 820px; min-height: 0;
padding-top: 420px;
overflow: visible;
}
#cleanTopologyCanvas,
#protocolTopologyCanvas,
.map-canvas {
bottom: auto;
height: 420px;
}
.map-theater::after {
bottom: auto;
height: 420px;
} }
.map-tabs { .map-tabs {
left: 12px; left: 12px;
right: 12px; right: 12px;
grid-template-columns: 1fr; grid-template-columns: repeat(2, minmax(0, 1fr));
} }
.map-theater-controls { .map-theater-controls {
left: 12px; position: relative;
right: 12px; left: auto;
bottom: 12px; right: auto;
bottom: auto;
margin: 12px;
padding: 12px;
} }
.map-control-panel .segmented,
.protocol-segmented { .protocol-segmented {
grid-template-columns: 1fr; grid-template-columns: repeat(2, minmax(0, 1fr));
}
.map-control-panel .topology-caption {
min-height: 0;
} }
.flow-list li { .flow-list li {