Save point: Data Exfiltration Simulator (scene8b) — interactive canvas showing where phone data goes on GrapheneOS/Android/iOS, accessible via SEE THE RISK button from scene8

This commit is contained in:
avi
2026-05-14 11:42:20 -05:00
parent 63587952ee
commit 439415e748

View File

@@ -612,6 +612,22 @@
margin-top: 0.5rem;
min-height: 1.5rem;
}
.s8canvas {
width: 100%;
max-width: 55rem;
margin-top: 0.8rem;
opacity: 0;
transition: opacity 4s ease;
}
.s8canvas.visible { opacity: 1; }
#phoneCanvas {
width: 100%;
height: 320px;
border: 1px solid #003300;
border-radius: 4px;
cursor: pointer;
display: block;
}
.s6section {
margin-top: 1.5rem;
@@ -651,6 +667,63 @@
}
.comp-table td.graphene-col .no { color: #44ff44; }
.comp-table td.graphene-col .yes { color: #ff4444; }
/* Scene 8b — Exfiltration Simulator */
#s8BtnRow {
position: fixed;
bottom: 5%;
left: 50%;
transform: translateX(-50%);
z-index: 5;
display: flex;
gap: 1.5rem;
justify-content: center;
visibility: hidden;
opacity: 0;
transition: opacity 0.5s ease;
}
#s8BtnRow .btnNext {
position: static;
transform: none;
margin: 0;
visibility: visible;
opacity: 1;
pointer-events: auto;
}
.s8bcanvas {
width: 100%;
max-width: 55rem;
margin-top: 0.8rem;
opacity: 0;
transition: opacity 4s ease;
}
.s8bcanvas.visible { opacity: 1; }
#exfilCanvas {
width: 100%;
height: 360px;
border: 1px solid #003300;
border-radius: 4px;
cursor: pointer;
display: block;
}
.exfil-counter {
text-align: center;
color: #00ff00;
font-size: 1.1rem;
padding: 0.5rem;
min-height: 2rem;
font-weight: bold;
}
.exfil-legend {
display: flex;
gap: 1rem;
justify-content: center;
flex-wrap: wrap;
margin-top: 0.3rem;
font-size: 0.75rem;
}
.exfil-legend-item { display: flex; align-items: center; gap: 0.3rem; }
.exfil-legend-dot { width: 8px; height: 8px; border-radius: 50%; display: inline-block; }
</style>
</head>
<body>
@@ -750,11 +823,28 @@
<button class="btnNext" id="returnFromScene7">RETURN</button>
</div>
<div id="scene8" class="scene">
<div id="scene8" class="scene" style="flex-direction:column;">
<div class="scene8text" id="s8Text"></div>
<div class="s8visual" id="s8Visual"></div>
<div class="s8canvas" id="s8Canvas">
<canvas id="phoneCanvas" width="800" height="320"></canvas>
<div class="mesh-status" id="phoneStatus">CLICK A TAB TO COMPARE OS SECURITY</div>
</div>
<div id="s8BtnRow">
<button class="btnNext" id="nextFromScene8">SEE THE RISK</button>
<button class="btnNext" id="returnFromScene8">RETURN</button>
</div>
</div>
<div id="scene8b" class="scene" style="flex-direction:column;">
<div class="scene8text" id="s8bText"></div>
<div class="s8bcanvas" id="s8bCanvas">
<canvas id="exfilCanvas" width="800" height="360"></canvas>
<div class="exfil-counter" id="exfilCounter">DATA VALUE EXPOSED: $0/yr</div>
<div class="mesh-status" id="exfilStatus">CLICK A TAB TO SEE WHERE YOUR DATA GOES</div>
<div class="exfil-legend" id="exfilLegend"></div>
</div>
<button class="btnNext" id="returnFromScene8b">RETURN</button>
</div>
<div id="scene9" class="scene" style="flex-direction:column;">
<div class="scene8text" id="s9Text"></div>
@@ -1601,30 +1691,231 @@
},30);
}
// Phone security interactive visualization state
let phoneAnimId = null;
let phoneMode = 'grapheneos';
let phonePackets = [];
let phoneClicked = false;
const phoneModeData = {
grapheneos: {
label: 'GRAPHENEOS', color: '#00ff00',
locks: { camera: true, mic: true, location: true, contacts: true, clipboard: true, network: true, storage: true, sensors: true },
packetType: 'stay',
status: 'EVERY PERMISSION REQUIRES EXPLICIT CONSENT — APPS CANNOT ACCESS SENSORS WITHOUT YOUR APPROVAL'
},
android: {
label: 'ANDROID', color: '#ff4444',
locks: { camera: false, mic: false, location: false, contacts: false, clipboard: false, network: false, storage: false, sensors: false },
packetType: 'leak',
status: 'GOOGLE PLAY SERVICES HAS SYSTEM-LEVEL ACCESS TO NEARLY EVERY SENSOR — DATA COLLECTED BY DEFAULT'
},
ios: {
label: 'iOS', color: '#ffff00',
locks: { camera: false, mic: false, location: true, contacts: false, clipboard: false, network: true, storage: true, sensors: false },
packetType: 'mixed',
status: 'APPLE BLOCKS SOME TRACKING BUT STILL COLLECTS DATA THROUGH ITS OWN SERVICES'
}
};
const phoneTabs = [
{ id: 'grapheneos', x: 170, w: 130, label: 'GRAPHENEOS' },
{ id: 'android', x: 335, w: 130, label: 'ANDROID' },
{ id: 'ios', x: 500, w: 130, label: 'iOS' },
];
const phoneFeatList = [
{ id: 'camera', label: 'CAMERA', x: 215, y: 100, lx: 365 },
{ id: 'mic', label: 'MICROPHONE', x: 215, y: 135, lx: 365 },
{ id: 'clipboard', label: 'CLIPBOARD', x: 215, y: 170, lx: 365 },
{ id: 'storage', label: 'STORAGE', x: 215, y: 205, lx: 365 },
{ id: 'location', label: 'LOCATION', x: 440, y: 100, lx: 570 },
{ id: 'contacts', label: 'CONTACTS', x: 440, y: 135, lx: 570 },
{ id: 'network', label: 'NETWORK', x: 440, y: 170, lx: 570 },
{ id: 'sensors', label: 'SENSORS', x: 440, y: 205, lx: 570 },
];
function initPhoneScene() {
phoneClicked = false;
phoneMode = 'grapheneos';
initPhonePackets();
}
function initPhonePackets() {
const mode = phoneModeData[phoneMode];
phonePackets = [];
for (let i = 0; i < 4; i++) {
const orbit = mode.packetType === 'stay' || (mode.packetType === 'mixed' && i < 2);
phonePackets.push({
angle: (i / 4) * Math.PI * 2,
speed: 0.012 + Math.random() * 0.008,
radius: 30 + i * 15,
orbit: orbit,
leakX: 400 + (Math.random() - 0.5) * 200,
leakY: 30 + Math.random() * 40,
progress: Math.random(),
});
}
}
function drawPhoneScene() {
const canvas = document.getElementById('phoneCanvas');
if (!canvas || canvas.offsetParent === null) { phoneAnimId = requestAnimationFrame(drawPhoneScene); return; }
const ctx = canvas.getContext('2d');
ctx.clearRect(0, 0, canvas.width, canvas.height);
const mode = phoneModeData[phoneMode];
for (const tab of phoneTabs) {
const active = tab.id === phoneMode;
ctx.fillStyle = active ? mode.color : '#001100';
ctx.strokeStyle = active ? mode.color : '#003300';
ctx.lineWidth = active ? 2 : 1;
const r = 4;
ctx.beginPath();
ctx.moveTo(tab.x + r, 8);
ctx.lineTo(tab.x + tab.w - r, 8);
ctx.quadraticCurveTo(tab.x + tab.w, 8, tab.x + tab.w, 8 + r);
ctx.lineTo(tab.x + tab.w, 30 - r);
ctx.quadraticCurveTo(tab.x + tab.w, 30, tab.x + tab.w - r, 30);
ctx.lineTo(tab.x + r, 30);
ctx.quadraticCurveTo(tab.x, 30, tab.x, 30 - r);
ctx.lineTo(tab.x, 8 + r);
ctx.quadraticCurveTo(tab.x, 8, tab.x + r, 8);
ctx.closePath();
ctx.fill();
ctx.stroke();
ctx.fillStyle = active ? '#000' : (active ? '#000' : mode.color);
ctx.font = 'bold 11px Courier New, monospace';
ctx.textAlign = 'center';
ctx.textBaseline = 'middle';
ctx.fillText(tab.label, tab.x + tab.w / 2, 19);
}
ctx.strokeStyle = '#003300';
ctx.lineWidth = 2;
ctx.fillStyle = '#000400';
ctx.beginPath();
ctx.roundRect(170, 65, 460, 205, 16);
ctx.fill();
ctx.stroke();
ctx.fillStyle = '#003300';
ctx.fillRect(370, 65, 60, 4);
ctx.fillRect(370, 252, 60, 2);
for (const feat of phoneFeatList) {
ctx.fillStyle = '#00ff00';
ctx.font = '11px Courier New, monospace';
ctx.textAlign = 'left';
ctx.textBaseline = 'middle';
ctx.fillText(feat.label, feat.x, feat.y);
const locked = mode.locks[feat.id];
ctx.fillStyle = locked ? '#00ff00' : '#ff4444';
ctx.font = '14px sans-serif';
ctx.textAlign = 'center';
ctx.fillText(locked ? '\u{1F512}' : '\u{1F513}', feat.lx, feat.y);
}
for (const pkt of phonePackets) {
let x, y;
if (pkt.orbit) {
const cx = 400, cy = 165;
x = cx + Math.cos(pkt.angle + pkt.progress * Math.PI * 2) * pkt.radius;
y = cy + Math.sin(pkt.angle + pkt.progress * Math.PI * 2) * pkt.radius * 0.6;
x = Math.max(185, Math.min(615, x));
y = Math.max(80, Math.min(255, y));
} else {
const t = pkt.progress;
if (t < 0.5) {
const p = t * 2;
x = 400 + (pkt.leakX - 400) * p;
y = 170 + (pkt.leakY - 170) * p;
} else {
const p = (t - 0.5) * 2;
x = pkt.leakX + (pkt.leakX - 400) * p;
y = pkt.leakY - 80 * p;
}
}
ctx.shadowColor = mode.color;
ctx.shadowBlur = 8;
ctx.fillStyle = mode.color;
ctx.beginPath();
ctx.arc(x, y, 4, 0, Math.PI * 2);
ctx.fill();
ctx.shadowBlur = 0;
pkt.progress += pkt.speed;
if (pkt.progress >= 1) {
pkt.progress = 0;
if (!pkt.orbit) {
pkt.leakX = 400 + (Math.random() - 0.5) * 200;
pkt.leakY = 30 + Math.random() * 40;
}
}
}
phoneAnimId = requestAnimationFrame(drawPhoneScene);
}
function handlePhoneCanvasClick(mx, my) {
const canvas = document.getElementById('phoneCanvas');
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 tab of phoneTabs) {
if (cx >= tab.x && cx <= tab.x + tab.w && cy >= 8 && cy <= 30) {
if (tab.id !== phoneMode) {
phoneMode = tab.id;
initPhonePackets();
const status = document.getElementById('phoneStatus');
if (status) status.textContent = phoneModeData[phoneMode].status;
phoneClicked = true;
const s8Row = document.getElementById('s8BtnRow');
if (s8Row.style.visibility !== 'visible') {
s8Row.style.cssText = '';
s8Row.style.visibility = 'visible';
let ro = 0;
const rfi = setInterval(() => {
ro += 0.05; if (ro >= 1) { ro = 1; clearInterval(rfi); s8Row.style.pointerEvents = 'auto'; }
s8Row.style.opacity = ro;
}, 30);
}
}
break;
}
}
}
function loadScene8(sceneElem) {
s8c=[];
sceneElem.style.display='flex';
const txt=document.getElementById('s8Text');
const vis=document.getElementById('s8Visual');
const canvasDiv=document.getElementById('s8Canvas');
txt.innerHTML='';
vis.className='s8visual';
canvasDiv.classList.remove('visible');
if (phoneAnimId) { cancelAnimationFrame(phoneAnimId); phoneAnimId = null; }
if (exfilAnimId) { cancelAnimationFrame(exfilAnimId); exfilAnimId = null; }
const s8Row = document.getElementById('s8BtnRow');
s8Row.style.visibility = 'hidden';
s8Row.style.opacity = '0';
s8Row.style.pointerEvents = 'none';
let o=0;
const fi=setInterval(()=>{
if (sceneElem.style.display !== 'flex' || document.getElementById('returnFromScene8').style.visibility === 'visible') { clearInterval(fi); return; }
if (sceneElem.style.display !== 'flex' || document.getElementById('s8BtnRow').style.visibility === 'visible') { clearInterval(fi); return; }
o+=0.05;if(o>=1){o=1;clearInterval(fi);
typeCalmly(txt,"YOUR PHONE IS LITERALLY A TRACKING DEVICE IN YOUR POCKET — UNLESS YOU CHOOSE OTHERWISE.",()=>{
const t1=setTimeout(()=>{
txt.innerHTML+="\n\n";
vis.classList.add('visible');
vis.innerHTML=buildS6MobileTable();
typeCalmly(txt,"GRAPHENEOS PROVES THAT PRIVACY AND USABILITY CAN COEXIST — BUT ONLY WHEN YOU TAKE CONTROL.",()=>{
const t3=setTimeout(()=>{
document.getElementById('returnFromScene8').style.cssText='';
showNextBtn('returnFromScene8');
},400);
s8c.push(t3);
},8,20,s8c);
typeCalmly(txt,"\n\nGRAPHENEOS LOCKS DOWN EVERY SENSOR AND PERMISSION — CLICK A TAB TO SEE THE DIFFERENCE.",()=>{
const t2=setTimeout(()=>{
canvasDiv.classList.add('visible');
initPhoneScene();
drawPhoneScene();
const status=document.getElementById('phoneStatus');
if(status) status.textContent=phoneModeData[phoneMode].status;
},300);
s8c.push(t2);
},8,20,s8c);
},600);
s8c.push(t1);
},8,20,s8c);
}
@@ -1839,6 +2130,283 @@
let s7c = [];
let s8c = [];
let s9c = [];
let s8bc = [];
// Scene 8b — Data Exfiltration Simulator
let exfilAnimId = null;
let exfilMode = 'grapheneos';
let exfilPackets = [];
let exfilCounter = 0;
let exfilCounterTarget = 0;
const exfilModeData = {
grapheneos: { label: 'GRAPHENEOS', counterMax: 0, color: '#00ff00', status: 'YOUR DATA STAYS ON YOUR DEVICE. NO TRACKING. NO PROFILING.' },
android: { label: 'ANDROID', counterMax: 1200, color: '#ff4444', status: 'GOOGLE AND THIRD-PARTY APPS COLLECT YOUR DATA BY DEFAULT. YOU ARE THE PRODUCT.' },
ios: { label: 'iOS', counterMax: 600, color: '#ffff00', status: 'APPLE COLLECTS DATA THROUGH ITS SERVICES. SOME THIRD-PARTY TRACKING IS BLOCKED.' },
};
const exfilTabs = [
{ id: 'grapheneos', x: 170, w: 130, label: 'GRAPHENEOS' },
{ id: 'android', x: 335, w: 130, label: 'ANDROID' },
{ id: 'ios', x: 500, w: 130, label: 'iOS' },
];
const exfilServers = {
grapheneos: [],
android: [
{ id: 'google', label: 'GOOGLE', x: 540, y: 55, w: 100, h: 34, color: '#4285f4' },
{ id: 'broker', label: 'DATA BROKER', x: 645, y: 115, w: 115, h: 34, color: '#ff6644' },
{ id: 'ads', label: 'AD NETWORKS', x: 555, y: 195, w: 115, h: 34, color: '#ff44ff' },
{ id: 'hacker', label: 'HACKER', x: 635, y: 285, w: 100, h: 34, color: '#ff0000' },
],
ios: [
{ id: 'apple', label: 'APPLE', x: 575, y: 65, w: 100, h: 34, color: '#aaaaaa' },
{ id: 'ads', label: 'AD NETWORKS', x: 555, y: 185, w: 115, h: 34, color: '#ff44ff' },
],
};
const exfilDataTypes = [
{ id: 'location', label: 'LOC', color: '#ff6644' },
{ id: 'contacts', label: 'CONTACTS', color: '#44aaff' },
{ id: 'camera', label: 'CAMERA', color: '#ff44ff' },
{ id: 'mic', label: 'MIC', color: '#ffff44' },
{ id: 'browsing', label: 'BROWSING', color: '#44ffaa' },
{ id: 'sms', label: 'SMS', color: '#ff8844' },
];
function initExfilScene() {
exfilMode = 'grapheneos';
exfilPackets = [];
exfilCounter = 0;
exfilCounterTarget = 0;
const legend = document.getElementById('exfilLegend');
if (legend) {
legend.innerHTML = '';
for (const dt of exfilDataTypes) {
const item = document.createElement('span');
item.className = 'exfil-legend-item';
item.innerHTML = '<span class="exfil-legend-dot" style="background:' + dt.color + '"></span>' + dt.label;
legend.appendChild(item);
}
}
}
function spawnExfilPacket() {
const servers = exfilServers[exfilMode];
if (servers.length === 0) return;
const server = servers[Math.floor(Math.random() * servers.length)];
const dataType = exfilDataTypes[Math.floor(Math.random() * exfilDataTypes.length)];
const px = 80, py = 65, pw = 140, ph = 245;
const fromX = px + 10 + Math.random() * (pw - 20);
const fromY = py + 40 + Math.random() * (ph - 60);
const toX = server.x + Math.random() * server.w;
const toY = server.y + Math.random() * server.h;
exfilPackets.push({
fromX, fromY, toX, toY,
dataType,
progress: 0,
speed: 0.008 + Math.random() * 0.006,
});
}
function drawExfilScene() {
const canvas = document.getElementById('exfilCanvas');
if (!canvas || canvas.offsetParent === null) { exfilAnimId = requestAnimationFrame(drawExfilScene); return; }
const ctx = canvas.getContext('2d');
ctx.clearRect(0, 0, canvas.width, canvas.height);
const mode = exfilModeData[exfilMode];
const servers = exfilServers[exfilMode];
// Tabs
for (const tab of exfilTabs) {
const active = tab.id === exfilMode;
ctx.fillStyle = active ? mode.color : '#001100';
ctx.strokeStyle = active ? mode.color : '#003300';
ctx.lineWidth = active ? 2 : 1;
const r = 4;
ctx.beginPath();
ctx.moveTo(tab.x + r, 8);
ctx.lineTo(tab.x + tab.w - r, 8);
ctx.quadraticCurveTo(tab.x + tab.w, 8, tab.x + tab.w, 8 + r);
ctx.lineTo(tab.x + tab.w, 30 - r);
ctx.quadraticCurveTo(tab.x + tab.w, 30, tab.x + tab.w - r, 30);
ctx.lineTo(tab.x + r, 30);
ctx.quadraticCurveTo(tab.x, 30, tab.x, 30 - r);
ctx.lineTo(tab.x, 8 + r);
ctx.quadraticCurveTo(tab.x, 8, tab.x + r, 8);
ctx.closePath();
ctx.fill();
ctx.stroke();
ctx.fillStyle = active ? '#000' : mode.color;
ctx.font = 'bold 11px Courier New, monospace';
ctx.textAlign = 'center';
ctx.textBaseline = 'middle';
ctx.fillText(tab.label, tab.x + tab.w / 2, 19);
}
// Phone silhouette
const px = 70, py = 60, pw = 140, ph = 250;
ctx.shadowColor = mode.color;
ctx.shadowBlur = 15;
ctx.strokeStyle = mode.color;
ctx.lineWidth = 2;
ctx.fillStyle = '#000800';
ctx.beginPath();
ctx.roundRect(px, py, pw, ph, 18);
ctx.fill();
ctx.stroke();
ctx.shadowBlur = 0;
// Phone screen
ctx.fillStyle = '#001100';
ctx.beginPath();
ctx.roundRect(px + 8, py + 35, pw - 16, ph - 55, 6);
ctx.fill();
// Camera dot
ctx.fillStyle = '#003300';
ctx.beginPath();
ctx.arc(px + pw / 2, py + 16, 4, 0, Math.PI * 2);
ctx.fill();
// Phone label
ctx.fillStyle = mode.color;
ctx.font = 'bold 12px Courier New, monospace';
ctx.textAlign = 'center';
ctx.textBaseline = 'middle';
ctx.fillText(exfilMode === 'grapheneos' ? 'SECURE' : 'LEAKING', px + pw / 2, py + ph / 2);
// Servers + connection lines
for (const srv of servers) {
ctx.strokeStyle = srv.color + '22';
ctx.lineWidth = 1;
ctx.setLineDash([4, 6]);
ctx.beginPath();
ctx.moveTo(px + pw / 2, py + ph / 2);
ctx.lineTo(srv.x + srv.w / 2, srv.y + srv.h / 2);
ctx.stroke();
ctx.setLineDash([]);
ctx.fillStyle = srv.color + '33';
ctx.strokeStyle = srv.color;
ctx.lineWidth = 1.5;
ctx.beginPath();
ctx.roundRect(srv.x, srv.y, srv.w, srv.h, 4);
ctx.fill();
ctx.stroke();
ctx.fillStyle = srv.color;
ctx.font = 'bold 10px Courier New, monospace';
ctx.textAlign = 'center';
ctx.textBaseline = 'middle';
ctx.fillText(srv.label, srv.x + srv.w / 2, srv.y + srv.h / 2);
}
// Spawn packets
if (servers.length > 0 && Math.random() < 0.06) spawnExfilPacket();
// Draw and update packets
for (let i = exfilPackets.length - 1; i >= 0; i--) {
const pkt = exfilPackets[i];
const x = pkt.fromX + (pkt.toX - pkt.fromX) * pkt.progress;
const y = pkt.fromY + (pkt.toY - pkt.fromY) * pkt.progress;
ctx.strokeStyle = pkt.dataType.color + '44';
ctx.lineWidth = 2;
ctx.beginPath();
ctx.moveTo(pkt.fromX, pkt.fromY);
ctx.lineTo(x, y);
ctx.stroke();
ctx.shadowColor = pkt.dataType.color;
ctx.shadowBlur = 10;
ctx.fillStyle = pkt.dataType.color;
ctx.beginPath();
ctx.arc(x, y, 5, 0, Math.PI * 2);
ctx.fill();
ctx.shadowBlur = 0;
ctx.fillStyle = pkt.dataType.color;
ctx.font = '8px Courier New, monospace';
ctx.textAlign = 'center';
ctx.textBaseline = 'bottom';
ctx.fillText(pkt.dataType.label, x, y - 6);
pkt.progress += pkt.speed;
if (pkt.progress >= 1) {
exfilPackets.splice(i, 1);
if (exfilCounterTarget < mode.counterMax) {
exfilCounterTarget = Math.min(mode.counterMax, exfilCounterTarget + 18);
}
}
}
while (exfilPackets.length > 30) exfilPackets.shift();
// Smooth counter
if (exfilCounter < exfilCounterTarget) {
exfilCounter = Math.min(exfilCounterTarget, exfilCounter + 5);
} else if (exfilCounter > exfilCounterTarget) {
exfilCounter = Math.max(exfilCounterTarget, exfilCounter - 5);
}
const ce = document.getElementById('exfilCounter');
if (ce) {
ce.textContent = 'DATA VALUE EXPOSED: $' + exfilCounter.toFixed(0) + '/yr';
ce.style.color = exfilCounter > 0 ? '#ff4444' : '#00ff00';
}
const st = document.getElementById('exfilStatus');
if (st) st.textContent = mode.status;
exfilAnimId = requestAnimationFrame(drawExfilScene);
}
function handleExfilCanvasClick(mx, my) {
const canvas = document.getElementById('exfilCanvas');
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 tab of exfilTabs) {
if (cx >= tab.x && cx <= tab.x + tab.w && cy >= 8 && cy <= 30) {
if (tab.id !== exfilMode) {
exfilMode = tab.id;
exfilPackets = [];
exfilCounterTarget = 0;
}
break;
}
}
}
function loadScene8b(sceneElem) {
s8bc = [];
sceneElem.style.display = 'flex';
const txt = document.getElementById('s8bText');
const canvasDiv = document.getElementById('s8bCanvas');
txt.innerHTML = '';
canvasDiv.classList.remove('visible');
if (exfilAnimId) { cancelAnimationFrame(exfilAnimId); exfilAnimId = null; }
let o = 0;
const fi = setInterval(() => {
if (sceneElem.style.display !== 'flex' || document.getElementById('returnFromScene8b').style.visibility === 'visible') { clearInterval(fi); return; }
o += 0.05; if (o >= 1) { o = 1; clearInterval(fi);
typeCalmly(txt, "EACH TIME YOUR PHONE SENDS DATA TO THIRD-PARTY SERVERS, IT CREATES A DIGITAL PROFILE OF YOU — SOLD TO THE HIGHEST BIDDER.", () => {
const t1 = setTimeout(() => {
typeCalmly(txt, "\n\nCLICK THE TABS TO SEE WHERE YOUR DATA ACTUALLY GOES.", () => {
const t2 = setTimeout(() => {
canvasDiv.classList.add('visible');
initExfilScene();
drawExfilScene();
}, 300);
s8bc.push(t2);
}, 8, 20, s8bc);
}, 600);
s8bc.push(t1);
}, 8, 20, s8bc);
}
sceneElem.style.opacity = o;
}, 30);
}
// Initialize on load
window.addEventListener('load', () => {
@@ -1938,6 +2506,8 @@
function showTechHub() {
if (meshAnimId) { cancelAnimationFrame(meshAnimId); meshAnimId = null; }
if (phoneAnimId) { cancelAnimationFrame(phoneAnimId); phoneAnimId = null; }
if (exfilAnimId) { cancelAnimationFrame(exfilAnimId); exfilAnimId = null; }
const s6 = document.getElementById('scene6');
s6.style.display = 'flex';
s6.style.opacity = '1';
@@ -1968,6 +2538,27 @@
if (meshState) toggleMeshNode(e.clientX, e.clientY);
});
document.getElementById('phoneCanvas').addEventListener('click', (e) => {
handlePhoneCanvasClick(e.clientX, e.clientY);
});
document.getElementById('exfilCanvas').addEventListener('click', (e) => {
handleExfilCanvasClick(e.clientX, e.clientY);
});
document.getElementById('nextFromScene8').addEventListener('click', () => {
if (exfilAnimId) { cancelAnimationFrame(exfilAnimId); exfilAnimId = null; }
if (phoneAnimId) { cancelAnimationFrame(phoneAnimId); phoneAnimId = null; }
document.getElementById('scene8').style.display = 'none';
loadScene8b(document.getElementById('scene8b'));
});
document.getElementById('returnFromScene8b').addEventListener('click', () => {
if (exfilAnimId) { cancelAnimationFrame(exfilAnimId); exfilAnimId = null; }
document.getElementById('scene8b').style.display = 'none';
showTechHub();
});
// Keyboard shortcuts for testing
document.addEventListener('keydown', (e) => {
if (e.key === 'Escape') {
@@ -2120,12 +2711,18 @@
if (s8.style.display === 'flex') {
s8c.forEach(t => clearTimeout(t)); s8c = [];
const txt = document.getElementById('s8Text');
const vis = document.getElementById('s8Visual');
txt.innerHTML = "YOUR PHONE IS LITERALLY A TRACKING DEVICE IN YOUR POCKET — UNLESS YOU CHOOSE OTHERWISE.\n\nGRAPHENEOS PROVES THAT PRIVACY AND USABILITY CAN COEXIST — BUT ONLY WHEN YOU TAKE CONTROL.";
vis.innerHTML=buildS6MobileTable();
vis.classList.add('visible');
document.getElementById('returnFromScene8').style.cssText='';
showNextBtn('returnFromScene8');
txt.innerHTML = "YOUR PHONE IS LITERALLY A TRACKING DEVICE IN YOUR POCKET — UNLESS YOU CHOOSE OTHERWISE.\n\nGRAPHENEOS LOCKS DOWN EVERY SENSOR AND PERMISSION — CLICK A TAB TO SEE THE DIFFERENCE.";
const canvasDiv = document.getElementById('s8Canvas');
canvasDiv.classList.add('visible');
if (phoneAnimId) { cancelAnimationFrame(phoneAnimId); phoneAnimId = null; }
if (exfilAnimId) { cancelAnimationFrame(exfilAnimId); exfilAnimId = null; }
initPhoneScene();
drawPhoneScene();
const s8Row = document.getElementById('s8BtnRow');
s8Row.style.cssText = '';
s8Row.style.visibility = 'visible';
s8Row.style.opacity = '1';
s8Row.style.pointerEvents = 'auto';
}
// Scene 9 skip
@@ -2143,6 +2740,21 @@
showNextBtn('returnFromScene9');
}
// Scene 8b skip
const s8b = document.getElementById('scene8b');
if (s8b.style.display === 'flex') {
s8bc.forEach(t => clearTimeout(t)); s8bc = [];
const txt = document.getElementById('s8bText');
txt.innerHTML = "EACH TIME YOUR PHONE SENDS DATA TO THIRD-PARTY SERVERS, IT CREATES A DIGITAL PROFILE OF YOU — SOLD TO THE HIGHEST BIDDER.\n\nCLICK THE TABS TO SEE WHERE YOUR DATA ACTUALLY GOES.";
const canvasDiv = document.getElementById('s8bCanvas');
canvasDiv.classList.add('visible');
if (exfilAnimId) { cancelAnimationFrame(exfilAnimId); exfilAnimId = null; }
initExfilScene();
drawExfilScene();
document.getElementById('returnFromScene8b').style.cssText='';
showNextBtn('returnFromScene8b');
}
}
if (e.key === 'Delete') {