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:
249
index.html
249
index.html
@@ -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');
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user