diff --git a/index.html b/index.html index af2fd15..a17c631 100644 --- a/index.html +++ b/index.html @@ -577,6 +577,41 @@ } .s8visual.visible { opacity: 1; } #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 { margin-top: 1.5rem; @@ -723,7 +758,15 @@
-
+
+ +
+ ONLINE + OFFLINE + PACKET +
+
CLICK A NODE TO SIMULATE FAILURE
+
@@ -1589,14 +1632,179 @@ },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) { s9c=[]; sceneElem.style.display='flex'; const txt=document.getElementById('s9Text'); - const vis=document.getElementById('s9Visual'); + const canvasDiv=document.getElementById('s9Canvas'); txt.innerHTML=''; - vis.innerHTML=''; - vis.className='s8visual'; + canvasDiv.classList.remove('visible'); + if (meshAnimId) { cancelAnimationFrame(meshAnimId); meshAnimId = null; } let o=0; const fi=setInterval(()=>{ if (sceneElem.style.display !== 'flex' || document.getElementById('returnFromScene9').style.visibility === 'visible') { clearInterval(fi); return; } @@ -1605,17 +1813,11 @@ const t1=setTimeout(()=>{ typeCalmly(txt,"\n\nMESH NETWORKS AND P2P PROTOCOLS LET COMMUNITIES BUILD THEIR OWN INTERNET — NO ISP REQUIRED.",()=>{ const t2=setTimeout(()=>{ - vis.classList.add('visible'); - vis.innerHTML='
INTERNET WITHOUT AN ISP
' - +'
🌐MESH NETWORKS —
EVERY DEVICE IS A NODE
' - +'
🔗PEER TO PEER —
DIRECT CONNECTION, NO MIDDLEMAN
' - +'
📡NO ISP —
COMMUNITY-OWNED INFRASTRUCTURE
' - +'
'; - const t3=setTimeout(()=>{ - document.getElementById('returnFromScene9').style.cssText=''; - showNextBtn('returnFromScene9'); - },400); - s9c.push(t3); + canvasDiv.classList.add('visible'); + initMesh(); + drawMesh(); + const status=document.getElementById('meshStatus'); + if(status) status.textContent='CLICK A NODE TO SIMULATE FAILURE'; },300); s9c.push(t2); },8,20,s9c); @@ -1731,6 +1933,7 @@ }); function showTechHub() { + if (meshAnimId) { cancelAnimationFrame(meshAnimId); meshAnimId = null; } const s6 = document.getElementById('scene6'); s6.style.display = 'flex'; s6.style.opacity = '1'; @@ -1757,6 +1960,10 @@ showTechHub(); }); + document.getElementById('meshCanvas').addEventListener('click', (e) => { + if (meshState) toggleMeshNode(e.clientX, e.clientY); + }); + // Keyboard shortcuts for testing document.addEventListener('keydown', (e) => { if (e.key === 'Escape') { @@ -1922,14 +2129,12 @@ if (s9.style.display === 'flex') { s9c.forEach(t => clearTimeout(t)); s9c = []; 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."; - vis.innerHTML='
INTERNET WITHOUT AN ISP
' - +'
🌐MESH NETWORKS —
EVERY DEVICE IS A NODE
' - +'
🔗PEER TO PEER —
DIRECT CONNECTION, NO MIDDLEMAN
' - +'
📡NO ISP —
COMMUNITY-OWNED INFRASTRUCTURE
' - +'
'; - vis.classList.add('visible'); + const canvasDiv = document.getElementById('s9Canvas'); + canvasDiv.classList.add('visible'); + if (meshAnimId) { cancelAnimationFrame(meshAnimId); meshAnimId = null; } + initMesh(); + drawMesh(); document.getElementById('returnFromScene9').style.cssText=''; showNextBtn('returnFromScene9'); }