1
0

Save point: Interactive mesh network visualization for Scene 9 — canvas-based P2P topology with clickable nodes, live packet animation, and self-healing rerouting

This commit is contained in:
avi
2026-05-14 11:09:33 -05:00
parent 3a72df8d25
commit 2bff2c0514

View File

@@ -577,6 +577,41 @@
} }
.s8visual.visible { opacity: 1; } .s8visual.visible { opacity: 1; }
#s9Text { text-align: center; } #s9Text { text-align: center; }
.s9canvas {
width: 100%;
max-width: 55rem;
margin-top: 1.5rem;
opacity: 0;
transition: opacity 4s ease;
}
.s9canvas.visible { opacity: 1; }
#meshCanvas {
width: 100%;
height: 320px;
border: 1px solid #003300;
border-radius: 4px;
cursor: pointer;
display: block;
}
.mesh-legend {
display: flex;
gap: 2rem;
justify-content: center;
margin-top: 0.8rem;
font-size: 0.8rem;
}
.legend-item { display: flex; align-items: center; gap: 0.5rem; }
.legend-dot { width: 10px; height: 10px; border-radius: 50%; display: inline-block; }
.legend-dot.online { background: #00ff00; }
.legend-dot.offline { background: #ff4444; }
.legend-dot.packet { background: #ffff00; }
.mesh-status {
text-align: center;
color: #007700;
font-size: 0.85rem;
margin-top: 0.5rem;
min-height: 1.5rem;
}
.s6section { .s6section {
margin-top: 1.5rem; margin-top: 1.5rem;
@@ -723,7 +758,15 @@
<div id="scene9" class="scene" style="flex-direction:column;"> <div id="scene9" class="scene" style="flex-direction:column;">
<div class="scene8text" id="s9Text"></div> <div class="scene8text" id="s9Text"></div>
<div class="s8visual" id="s9Visual"></div> <div class="s9canvas" id="s9Canvas">
<canvas id="meshCanvas" width="800" height="320"></canvas>
<div class="mesh-legend">
<span class="legend-item"><span class="legend-dot online"></span> ONLINE</span>
<span class="legend-item"><span class="legend-dot offline"></span> OFFLINE</span>
<span class="legend-item"><span class="legend-dot packet"></span> PACKET</span>
</div>
<div class="mesh-status" id="meshStatus">CLICK A NODE TO SIMULATE FAILURE</div>
</div>
<button class="btnNext" id="returnFromScene9">RETURN</button> <button class="btnNext" id="returnFromScene9">RETURN</button>
</div> </div>
@@ -1589,14 +1632,179 @@
},30); },30);
} }
// Mesh network interactive visualization state
let meshAnimId = null;
let meshClicked = false;
let meshState = null;
function initMesh() {
meshClicked = false;
meshState = {
nodes: [
{ id: 0, x: 120, y: 50, label: 'A', online: true },
{ id: 1, x: 400, y: 40, label: 'B', online: true },
{ id: 2, x: 680, y: 50, label: 'C', online: true },
{ id: 3, x: 100, y: 150, label: 'D', online: true },
{ id: 4, x: 400, y: 160, label: 'E', online: true },
{ id: 5, x: 700, y: 150, label: 'F', online: true },
{ id: 6, x: 200, y: 260, label: 'G', online: true },
{ id: 7, x: 600, y: 260, label: 'H', online: true },
],
packets: [],
};
computeConnections();
for (let i = 0; i < 4; i++) spawnPacket();
}
function computeConnections() {
const online = meshState.nodes.filter(n => n.online);
for (const node of meshState.nodes) {
if (!node.online) { node.connections = []; continue; }
const distances = online
.filter(n => n.id !== node.id && n.online)
.map(n => ({ node: n, dist: Math.hypot(n.x - node.x, n.y - node.y) }))
.sort((a, b) => a.dist - b.dist);
node.connections = distances.slice(0, 3).map(d => d.node.id);
}
}
function spawnPacket() {
const online = meshState.nodes.filter(n => n.online);
if (online.length < 2) return;
const from = online[Math.floor(Math.random() * online.length)];
if (!from.connections || from.connections.length === 0) return;
const toId = from.connections[Math.floor(Math.random() * from.connections.length)];
const to = meshState.nodes.find(n => n.id === toId);
if (!to || !to.online) return;
meshState.packets.push({ fromId: from.id, toId: to.id, progress: Math.random() });
}
function drawMesh() {
const canvas = document.getElementById('meshCanvas');
if (!canvas || canvas.offsetParent === null) { meshAnimId = requestAnimationFrame(drawMesh); return; }
const ctx = canvas.getContext('2d');
ctx.clearRect(0, 0, canvas.width, canvas.height);
for (const node of meshState.nodes) {
if (!node.online) continue;
for (const connId of node.connections) {
const conn = meshState.nodes.find(n => n.id === connId);
if (!conn || !conn.online) continue;
ctx.strokeStyle = '#003300';
ctx.lineWidth = 1;
ctx.beginPath();
ctx.moveTo(node.x, node.y);
ctx.lineTo(conn.x, conn.y);
ctx.stroke();
}
}
for (const pkt of meshState.packets) {
const from = meshState.nodes.find(n => n.id === pkt.fromId);
const to = meshState.nodes.find(n => n.id === pkt.toId);
if (!from || !to || !from.online || !to.online) continue;
const x = from.x + (to.x - from.x) * pkt.progress;
const y = from.y + (to.y - from.y) * pkt.progress;
ctx.shadowColor = '#ffff00';
ctx.shadowBlur = 10;
ctx.fillStyle = '#ffff00';
ctx.beginPath();
ctx.arc(x, y, 5, 0, Math.PI * 2);
ctx.fill();
ctx.shadowBlur = 0;
}
for (const node of meshState.nodes) {
const color = node.online ? '#00ff00' : '#ff4444';
if (node.online) {
ctx.shadowColor = '#00ff00';
ctx.shadowBlur = 12;
}
ctx.fillStyle = color;
ctx.beginPath();
ctx.arc(node.x, node.y, 12, 0, Math.PI * 2);
ctx.fill();
ctx.shadowBlur = 0;
ctx.strokeStyle = color;
ctx.lineWidth = 2;
if (!node.online) ctx.setLineDash([3, 3]);
ctx.beginPath();
ctx.arc(node.x, node.y, 12, 0, Math.PI * 2);
ctx.stroke();
ctx.setLineDash([]);
ctx.fillStyle = color;
ctx.font = 'bold 11px Courier New, monospace';
ctx.textAlign = 'center';
ctx.textBaseline = 'middle';
ctx.fillText(node.label, node.x, node.y + 24);
}
for (const pkt of meshState.packets) {
pkt.progress += 0.01;
if (pkt.progress >= 1) {
pkt.progress = 0;
pkt.fromId = pkt.toId;
const from = meshState.nodes.find(n => n.id === pkt.fromId);
if (!from || !from.online) {
const online = meshState.nodes.filter(n => n.online);
if (online.length > 0) pkt.fromId = online[Math.floor(Math.random() * online.length)].id;
}
const from2 = meshState.nodes.find(n => n.id === pkt.fromId);
if (from2 && from2.online && from2.connections) {
const valid = from2.connections.filter(cid => {
const cn = meshState.nodes.find(n => n.id === cid);
return cn && cn.online;
});
pkt.toId = valid.length > 0 ? valid[Math.floor(Math.random() * valid.length)] : pkt.fromId;
}
}
}
meshState.packets = meshState.packets.filter(p => {
const from = meshState.nodes.find(n => n.id === p.fromId);
const to = meshState.nodes.find(n => n.id === p.toId);
return from && to && from.online && to.online && p.fromId !== p.toId;
});
while (meshState.packets.length < 4) spawnPacket();
meshAnimId = requestAnimationFrame(drawMesh);
}
function toggleMeshNode(mx, my) {
const canvas = document.getElementById('meshCanvas');
if (!canvas) return;
const rect = canvas.getBoundingClientRect();
const cx = (mx - rect.left) * (canvas.width / rect.width);
const cy = (my - rect.top) * (canvas.height / rect.height);
for (const node of meshState.nodes) {
if (Math.hypot(cx - node.x, cy - node.y) < 20) {
node.online = !node.online;
meshState.packets = meshState.packets.filter(p => p.fromId !== node.id && p.toId !== node.id);
computeConnections();
const status = document.getElementById('meshStatus');
if (status) {
status.textContent = node.online
? 'NODE ' + node.label + ' RESTORED — MESH PATH FOUND'
: 'NODE ' + node.label + ' OFFLINE — MESH REROUTED';
}
meshClicked = true;
if (document.getElementById('returnFromScene9').style.visibility !== 'visible') {
document.getElementById('returnFromScene9').style.cssText = '';
showNextBtn('returnFromScene9');
}
break;
}
}
}
function loadScene9(sceneElem) { function loadScene9(sceneElem) {
s9c=[]; s9c=[];
sceneElem.style.display='flex'; sceneElem.style.display='flex';
const txt=document.getElementById('s9Text'); const txt=document.getElementById('s9Text');
const vis=document.getElementById('s9Visual'); const canvasDiv=document.getElementById('s9Canvas');
txt.innerHTML=''; txt.innerHTML='';
vis.innerHTML=''; canvasDiv.classList.remove('visible');
vis.className='s8visual'; if (meshAnimId) { cancelAnimationFrame(meshAnimId); meshAnimId = null; }
let o=0; let o=0;
const fi=setInterval(()=>{ const fi=setInterval(()=>{
if (sceneElem.style.display !== 'flex' || document.getElementById('returnFromScene9').style.visibility === 'visible') { clearInterval(fi); return; } if (sceneElem.style.display !== 'flex' || document.getElementById('returnFromScene9').style.visibility === 'visible') { clearInterval(fi); return; }
@@ -1605,17 +1813,11 @@
const t1=setTimeout(()=>{ const t1=setTimeout(()=>{
typeCalmly(txt,"\n\nMESH NETWORKS AND P2P PROTOCOLS LET COMMUNITIES BUILD THEIR OWN INTERNET — NO ISP REQUIRED.",()=>{ typeCalmly(txt,"\n\nMESH NETWORKS AND P2P PROTOCOLS LET COMMUNITIES BUILD THEIR OWN INTERNET — NO ISP REQUIRED.",()=>{
const t2=setTimeout(()=>{ const t2=setTimeout(()=>{
vis.classList.add('visible'); canvasDiv.classList.add('visible');
vis.innerHTML='<div class="s6section"><div class="s6section-title">INTERNET WITHOUT AN ISP</div><div class="punch-row">' initMesh();
+'<div class="punch-card"><span class="punch-icon">🌐</span><span class="punch-text">MESH NETWORKS —<br>EVERY DEVICE IS A NODE</span></div>' drawMesh();
+'<div class="punch-card"><span class="punch-icon">🔗</span><span class="punch-text">PEER TO PEER —<br>DIRECT CONNECTION, NO MIDDLEMAN</span></div>' const status=document.getElementById('meshStatus');
+'<div class="punch-card"><span class="punch-icon">📡</span><span class="punch-text">NO ISP —<br>COMMUNITY-OWNED INFRASTRUCTURE</span></div>' if(status) status.textContent='CLICK A NODE TO SIMULATE FAILURE';
+'</div></div>';
const t3=setTimeout(()=>{
document.getElementById('returnFromScene9').style.cssText='';
showNextBtn('returnFromScene9');
},400);
s9c.push(t3);
},300); },300);
s9c.push(t2); s9c.push(t2);
},8,20,s9c); },8,20,s9c);
@@ -1731,6 +1933,7 @@
}); });
function showTechHub() { function showTechHub() {
if (meshAnimId) { cancelAnimationFrame(meshAnimId); meshAnimId = null; }
const s6 = document.getElementById('scene6'); const s6 = document.getElementById('scene6');
s6.style.display = 'flex'; s6.style.display = 'flex';
s6.style.opacity = '1'; s6.style.opacity = '1';
@@ -1757,6 +1960,10 @@
showTechHub(); showTechHub();
}); });
document.getElementById('meshCanvas').addEventListener('click', (e) => {
if (meshState) toggleMeshNode(e.clientX, e.clientY);
});
// Keyboard shortcuts for testing // Keyboard shortcuts for testing
document.addEventListener('keydown', (e) => { document.addEventListener('keydown', (e) => {
if (e.key === 'Escape') { if (e.key === 'Escape') {
@@ -1922,14 +2129,12 @@
if (s9.style.display === 'flex') { if (s9.style.display === 'flex') {
s9c.forEach(t => clearTimeout(t)); s9c = []; s9c.forEach(t => clearTimeout(t)); s9c = [];
const txt = document.getElementById('s9Text'); const txt = document.getElementById('s9Text');
const vis = document.getElementById('s9Visual');
txt.innerHTML = "THE INTERNET WAS DESIGNED TO BE DECENTRALIZED — BUT ISPS HAVE TURNED IT INTO A UTILITY CONTROLLED BY GATEKEEPERS.\n\nMESH NETWORKS AND P2P PROTOCOLS LET COMMUNITIES BUILD THEIR OWN INTERNET — NO ISP REQUIRED."; txt.innerHTML = "THE INTERNET WAS DESIGNED TO BE DECENTRALIZED — BUT ISPS HAVE TURNED IT INTO A UTILITY CONTROLLED BY GATEKEEPERS.\n\nMESH NETWORKS AND P2P PROTOCOLS LET COMMUNITIES BUILD THEIR OWN INTERNET — NO ISP REQUIRED.";
vis.innerHTML='<div class="s6section"><div class="s6section-title">INTERNET WITHOUT AN ISP</div><div class="punch-row">' const canvasDiv = document.getElementById('s9Canvas');
+'<div class="punch-card"><span class="punch-icon">🌐</span><span class="punch-text">MESH NETWORKS —<br>EVERY DEVICE IS A NODE</span></div>' canvasDiv.classList.add('visible');
+'<div class="punch-card"><span class="punch-icon">🔗</span><span class="punch-text">PEER TO PEER —<br>DIRECT CONNECTION, NO MIDDLEMAN</span></div>' if (meshAnimId) { cancelAnimationFrame(meshAnimId); meshAnimId = null; }
+'<div class="punch-card"><span class="punch-icon">📡</span><span class="punch-text">NO ISP —<br>COMMUNITY-OWNED INFRASTRUCTURE</span></div>' initMesh();
+'</div></div>'; drawMesh();
vis.classList.add('visible');
document.getElementById('returnFromScene9').style.cssText=''; document.getElementById('returnFromScene9').style.cssText='';
showNextBtn('returnFromScene9'); showNextBtn('returnFromScene9');
} }