acer Claude Opus 5 (1M context) commited on
Commit Β·
4444df5
1
Parent(s): e8ef9cc
open the door - the corridor goes in
Browse files27 exits reaching every other door in the corpus plus asolaria-multiverse.
Renders with the network unplugged: 0 external src, 0 external stylesheet,
0 script tags, 0 runtime network calls, 4418 bytes. Idempotent.
Built by the FALCON seat. Cranked from ACER.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
- index.html +827 -787
index.html
CHANGED
|
@@ -1,787 +1,827 @@
|
|
| 1 |
-
<meta charset="utf-8">
|
| 2 |
-
<meta name="viewport" content="width=device-width,initial-scale=1,user-scalable=no">
|
| 3 |
-
<title>lightDOORs β a place made of light, with doors instead of windows</title>
|
| 4 |
-
<style>
|
| 5 |
-
:root{--bg:#08060f;--fg:#efeaf2;--dim:#8d8496;--line:#241e30;--gold:#e8c07a;
|
| 6 |
-
--ok:#7fe0a0;--no:#ff8b96;--pane:#0e0a18}
|
| 7 |
-
*{box-sizing:border-box;-webkit-tap-highlight-color:transparent}
|
| 8 |
-
html,body{height:100%;margin:0;overflow:hidden}
|
| 9 |
-
body{background:radial-gradient(120% 90% at 50% -10%,#1a1030 0%,#08060f 60%);
|
| 10 |
-
color:var(--fg);font:13px/1.55 ui-monospace,"SF Mono",Menlo,Consolas,monospace}
|
| 11 |
-
|
| 12 |
-
/* the field the doors stand in */
|
| 13 |
-
#field{position:fixed;inset:0}
|
| 14 |
-
#stars{position:fixed;inset:0;pointer-events:none}
|
| 15 |
-
|
| 16 |
-
/* a pane of light */
|
| 17 |
-
.pane{position:absolute;min-width:230px;min-height:150px;background:rgba(14,10,24,.93);
|
| 18 |
-
border:1px solid var(--line);border-radius:13px;box-shadow:0 18px 60px rgba(0,0,0,.6);
|
| 19 |
-
display:flex;flex-direction:column;overflow:hidden;backdrop-filter:blur(9px)}
|
| 20 |
-
.pane.on{border-color:rgba(232,192,122,.42)}
|
| 21 |
-
.bar{display:flex;align-items:center;gap:8px;padding:7px 9px;cursor:grab;
|
| 22 |
-
background:linear-gradient(180deg,rgba(255,255,255,.05),rgba(255,255,255,0));
|
| 23 |
-
border-bottom:1px solid var(--line);flex:0 0 auto;touch-action:none}
|
| 24 |
-
.bar b{font-weight:600;font-size:12px;flex:1;white-space:nowrap;overflow:hidden;
|
| 25 |
-
text-overflow:ellipsis}
|
| 26 |
-
.dot{width:11px;height:11px;border-radius:50%;border:0;cursor:pointer;flex:0 0 auto}
|
| 27 |
-
.dot.x{background:#ff6b78}.dot.m{background:#e8c07a}
|
| 28 |
-
.body{flex:1;overflow:auto;padding:10px;font-size:12.5px}
|
| 29 |
-
.grip{position:absolute;right:0;bottom:0;width:18px;height:18px;cursor:nwse-resize;
|
| 30 |
-
background:linear-gradient(135deg,transparent 50%,rgba(255,255,255,.22) 50%);
|
| 31 |
-
touch-action:none}
|
| 32 |
-
.key{width:14px;height:14px;border-radius:4px;flex:0 0 auto;border:1px solid rgba(0,0,0,.4)}
|
| 33 |
-
|
| 34 |
-
/* the sill β where the doors are */
|
| 35 |
-
#sill{position:fixed;left:0;right:0;bottom:0;display:flex;gap:6px;padding:8px 10px;
|
| 36 |
-
background:rgba(8,6,15,.9);border-top:1px solid var(--line);overflow-x:auto;
|
| 37 |
-
align-items:center;z-index:9000;backdrop-filter:blur(12px)}
|
| 38 |
-
#sill button{background:transparent;color:var(--fg);border:1px solid var(--line);
|
| 39 |
-
border-radius:9px;padding:8px 12px;font:600 12px ui-monospace,monospace;
|
| 40 |
-
cursor:pointer;white-space:nowrap;flex:0 0 auto}
|
| 41 |
-
#sill button.lit{border-color:var(--gold);color:var(--gold)}
|
| 42 |
-
#sill .me{margin-left:auto;display:flex;align-items:center;gap:7px;flex:0 0 auto;
|
| 43 |
-
color:var(--dim);font-size:11px;padding-left:10px}
|
| 44 |
-
|
| 45 |
-
/* bits */
|
| 46 |
-
input,textarea,select{width:100%;background:rgba(255,255,255,.04);color:var(--fg);
|
| 47 |
-
border:1px solid var(--line);border-radius:9px;padding:9px;
|
| 48 |
-
font:12.5px ui-monospace,monospace;margin:4px 0}
|
| 49 |
-
textarea{min-height:62px;font-size:11px;line-height:1.45}
|
| 50 |
-
.btn{background:var(--gold);color:#120d18;border:0;border-radius:9px;padding:9px 15px;
|
| 51 |
-
font:700 12px ui-monospace,monospace;cursor:pointer;margin:3px 5px 3px 0}
|
| 52 |
-
.btn.g{background:transparent;color:var(--fg);border:1px solid var(--line)}
|
| 53 |
-
.row{display:flex;justify-content:space-between;gap:10px;padding:1px 0}
|
| 54 |
-
.k{color:var(--dim)}.ok{color:var(--ok)}.no{color:var(--no)}.acc{color:var(--gold)}
|
| 55 |
-
.stream{height:100%;display:flex;flex-direction:column}
|
| 56 |
-
.said{flex:1;overflow:auto;white-space:pre-wrap;word-break:break-word}
|
| 57 |
-
.said div{padding:2px 0}
|
| 58 |
-
.said i{font-style:normal;color:var(--dim)}
|
| 59 |
-
.chip{height:17px;width:44px;border-radius:5px;border:1px solid var(--line);
|
| 60 |
-
display:inline-block;vertical-align:middle;margin:2px 4px 2px 0;cursor:pointer}
|
| 61 |
-
h4{margin:9px 0 5px;font-size:10.5px;letter-spacing:1.4px;text-transform:uppercase;
|
| 62 |
-
color:var(--dim);font-weight:400;border-top:1px solid var(--line);padding-top:8px}
|
| 63 |
-
h4:first-child{border-top:0;padding-top:0;margin-top:0}
|
| 64 |
-
p{color:#c9c0d4;margin:6px 0}b{color:#fff}small{color:var(--dim)}
|
| 65 |
-
canvas.paint{width:100%;height:100%;display:block;background:#050309;
|
| 66 |
-
border-radius:8px;touch-action:none}
|
| 67 |
-
#toast{position:fixed;left:50%;top:14px;transform:translateX(-50%);z-index:9999;
|
| 68 |
-
background:rgba(20,14,34,.95);border:1px solid var(--line);border-radius:10px;
|
| 69 |
-
padding:9px 15px;font-size:12px;opacity:0;transition:opacity .3s;pointer-events:none}
|
| 70 |
-
</style>
|
| 71 |
-
|
| 72 |
-
<canvas id="stars"></canvas>
|
| 73 |
-
<div id="field"></div>
|
| 74 |
-
|
| 75 |
-
<div id="sill">
|
| 76 |
-
<button id="d-open" class="lit">β open</button>
|
| 77 |
-
<button data-door="doors">doors</button>
|
| 78 |
-
<button data-door="people">people</button>
|
| 79 |
-
<button data-door="say">say</button>
|
| 80 |
-
<button data-door="paint">paint</button>
|
| 81 |
-
<button data-door="carry">carry</button>
|
| 82 |
-
<button data-door="play">play</button>
|
| 83 |
-
<button data-door="rooms">rooms</button>
|
| 84 |
-
<button data-door="all">β all</button>
|
| 85 |
-
<button data-door="what">what is this</button>
|
| 86 |
-
<span class="me"><span class="key" id="mykey"></span><span id="myid">β</span>
|
| 87 |
-
<span id="mypeers" class="ok">0</span></span>
|
| 88 |
-
</div>
|
| 89 |
-
<div id="toast"></div>
|
| 90 |
-
|
| 91 |
-
<script>
|
| 92 |
-
/* =========================================================================
|
| 93 |
-
lightDOORs. A door is a label, not a socket β you can open as many as you
|
| 94 |
-
like and it costs nothing, because there was never a socket per door.
|
| 95 |
-
Nothing here is stored on anyone's machine but yours.
|
| 96 |
-
========================================================================= */
|
| 97 |
-
const ICE={iceServers:[{urls:["stun:stun.l.google.com:19302",
|
| 98 |
-
"stun:stun1.l.google.com:19302"]}]};
|
| 99 |
-
const $=s=>document.querySelector(s);
|
| 100 |
-
const peers=new Map(); /* colour -> {pc, ctl, doors:Map} */
|
| 101 |
-
const pending=new Map();
|
| 102 |
-
const seen=new Set();
|
| 103 |
-
const doors=new Map(); /* door name -> [{from,body,t}] */
|
| 104 |
-
const strokes=new Map(); /* door name -> [[x,y,px,py,colour]] */
|
| 105 |
-
const carried=[]; /* things that arrived */
|
| 106 |
-
let MYID=null, bootPc=null, isHost=false, z=10;
|
| 107 |
-
|
| 108 |
-
let held=new Set();
|
| 109 |
-
try{held=new Set(JSON.parse(localStorage.getItem("ld.colours")||"[]"))}catch(e){}
|
| 110 |
-
let myDoors=[];
|
| 111 |
-
try{myDoors=JSON.parse(localStorage.getItem("ld.doors")||'["light"]')}catch(e){myDoors=["light"]}
|
| 112 |
-
if(!myDoors.length) myDoors=["light"];
|
| 113 |
-
const saveDoors=()=>{try{localStorage.setItem("ld.doors",JSON.stringify(myDoors))}catch(e){}};
|
| 114 |
-
|
| 115 |
-
/* ---- colour is the address; the address is the content ---- */
|
| 116 |
-
async function sha(s){
|
| 117 |
-
const d=await crypto.subtle.digest("SHA-256",new TextEncoder().encode(s));
|
| 118 |
-
return [...new Uint8Array(d)].map(x=>x.toString(16).padStart(2,"0")).join("");
|
| 119 |
-
}
|
| 120 |
-
async function shaBytes(u){
|
| 121 |
-
const d=await crypto.subtle.digest("SHA-256",u);
|
| 122 |
-
return [...new Uint8Array(d)].map(x=>x.toString(16).padStart(2,"0")).join("");
|
| 123 |
-
}
|
| 124 |
-
/* THE QUANT. Same law as the shipped engine, verified byte-identical on 40
|
| 125 |
-
cases: JL count-sketch into 1024 buckets, then the fixed 3,200-byte tail of
|
| 126 |
-
turbo(1024) + signs(128) + zeta(1024) + hist(1024). O(N) in, O(1) out. */
|
| 127 |
-
function quant(bytes){
|
| 128 |
-
const D=1024, pad=(8-(bytes.length%8))%8;
|
| 129 |
-
const b=new Uint8Array(bytes.length+pad); b.set(bytes);
|
| 130 |
-
const f=new Float64Array(b.buffer), proj=new Float64Array(D);
|
| 131 |
-
for(let i=0;i<f.length;i++){
|
| 132 |
-
const h=(Math.imul(i,2654435761))>>>0, v=f[i];
|
| 133 |
-
if(!Number.isFinite(v)) continue;
|
| 134 |
-
proj[h&(D-1)] += (h&0x80000000)? -v : v;
|
| 135 |
-
}
|
| 136 |
-
let max=1e-12;
|
| 137 |
-
for(let j=0;j<D;j++){const a=Math.abs(proj[j]); if(a>max)max=a;}
|
| 138 |
-
const turbo=new Int8Array(D), signs=new Uint8Array(D>>3),
|
| 139 |
-
zeta=new Uint8Array(D), hist=new Uint32Array(256);
|
| 140 |
-
for(let j=0;j<D;j++){
|
| 141 |
-
const v=proj[j]/max, q=Math.round(v*127), a=Math.abs(v);
|
| 142 |
-
turbo[j]=q;
|
| 143 |
-
if(v<0) signs[j>>3] |= (1<<(j&7));
|
| 144 |
-
zeta[j]= a<1e-9 ? 15 : Math.min(15, -Math.log2(a)|0);
|
| 145 |
-
hist[(q+128)&255]+=1;
|
| 146 |
-
}
|
| 147 |
-
const out=new Uint8Array(3200);
|
| 148 |
-
out.set(new Uint8Array(turbo.buffer),0); out.set(signs,1024);
|
| 149 |
-
out.set(zeta,1152); out.set(new Uint8Array(hist.buffer),2176);
|
| 150 |
-
return out;
|
| 151 |
-
}
|
| 152 |
-
function playIn(ch){
|
| 153 |
-
ch.binaryType="arraybuffer";
|
| 154 |
-
ch.onmessage=async e=>{
|
| 155 |
-
if(typeof e.data==="string") return;
|
| 156 |
-
const t=new Uint8Array(e.data);
|
| 157 |
-
if(t.length!==3200){ toast("not a tuple ("+t.length+" B)"); return; }
|
| 158 |
-
const h=await shaBytes(t); absorb(h.slice(0,16));
|
| 159 |
-
const p=pane("play","play",DOORS.play,420,380);
|
| 160 |
-
if(p._show){ p._show(t); p._t=t;
|
| 161 |
-
p.querySelector("#pin").textContent="played to you";
|
| 162 |
-
p.querySelector("#pout").textContent="3,200 B";
|
| 163 |
-
p.querySelector("#psha").textContent=h.slice(0,16); }
|
| 164 |
-
toast("played Β· "+h.slice(0,10));
|
| 165 |
-
};
|
| 166 |
-
}
|
| 167 |
-
const cl=(v,a,b)=>Math.max(a,Math.min(b,v));
|
| 168 |
-
function stops(h){
|
| 169 |
-
h=(h||"")+"000000000000";
|
| 170 |
-
const p=i=>cl(parseInt(h.substr(i,2),16)||16,16,240);
|
| 171 |
-
const lum=c=>0.2126*c[0]+0.7152*c[1]+0.0722*c[2];
|
| 172 |
-
const fix=c=>{const L=lum(c);return (L>=32&&L<=224)?c:
|
| 173 |
-
c.map(x=>Math.round(cl(x*128/Math.max(1,L),0,255)))};
|
| 174 |
-
return [fix([p(0),p(2),p(4)]),fix([p(6),p(8),p(10)])];
|
| 175 |
-
}
|
| 176 |
-
const grad=h=>{const[a,b]=stops(h);
|
| 177 |
-
return "linear-gradient(90deg,rgb("+a+"),rgb("+b+"))"};
|
| 178 |
-
const ink=h=>"rgb("+stops(h)[1]+")";
|
| 179 |
-
const absorb=c=>{ if(!c||c===MYID||held.has(c)) return;
|
| 180 |
-
held.add(c); try{localStorage.setItem("ld.colours",JSON.stringify([...held]))}catch(e){}
|
| 181 |
-
refresh("people"); };
|
| 182 |
-
|
| 183 |
-
const toast=t=>{const e=$("#toast");e.textContent=t;e.style.opacity=1;
|
| 184 |
-
clearTimeout(e._t);e._t=setTimeout(()=>e.style.opacity=0,2300)};
|
| 185 |
-
|
| 186 |
-
/* =========================== panes ====================================== */
|
| 187 |
-
const open=new Map();
|
| 188 |
-
function pane(id,title,build,w,h){
|
| 189 |
-
if(open.has(id)){ const p=open.get(id); p.style.zIndex=++z; lit(); return p; }
|
| 190 |
-
const p=document.createElement("div"); p.className="pane"; p.style.zIndex=++z;
|
| 191 |
-
const n=open.size, vw=innerWidth, vh=innerHeight;
|
| 192 |
-
w=Math.min(w||330,vw-24); h=Math.min(h||300,vh-110);
|
| 193 |
-
p.style.left=Math.min(vw-w-12,18+n*26)+"px";
|
| 194 |
-
p.style.top =Math.min(vh-h-70,18+n*24)+"px";
|
| 195 |
-
p.style.width=w+"px"; p.style.height=h+"px";
|
| 196 |
-
p.innerHTML='<div class=bar><b></b>'+
|
| 197 |
-
'<button class="dot m"></button><button class="dot x"></button></div>'+
|
| 198 |
-
'<div class=body></div><div class=grip></div>';
|
| 199 |
-
p.querySelector("b").textContent=title;
|
| 200 |
-
build(p.querySelector(".body"),p);
|
| 201 |
-
p.onpointerdown=()=>{p.style.zIndex=++z;lit()};
|
| 202 |
-
drag(p.querySelector(".bar"),p); resize(p.querySelector(".grip"),p);
|
| 203 |
-
p.querySelector(".dot.x").onclick=e=>{e.stopPropagation();p.remove();open.delete(id);lit()};
|
| 204 |
-
p.querySelector(".dot.m").onclick=e=>{e.stopPropagation();
|
| 205 |
-
const b=p.querySelector(".body");
|
| 206 |
-
const min=b.style.display!=="none";
|
| 207 |
-
b.style.display=min?"none":"";p.querySelector(".grip").style.display=min?"none":"";
|
| 208 |
-
p._h=p._h||p.style.height; p.style.height=min?"auto":p._h};
|
| 209 |
-
$("#field").appendChild(p); open.set(id,p); lit();
|
| 210 |
-
return p;
|
| 211 |
-
}
|
| 212 |
-
function lit(){
|
| 213 |
-
document.querySelectorAll("#sill button[data-door]").forEach(b=>
|
| 214 |
-
b.classList.toggle("lit",open.has(b.dataset.door)));
|
| 215 |
-
}
|
| 216 |
-
function drag(h,p){
|
| 217 |
-
h.onpointerdown=e=>{ if(e.target.classList.contains("dot"))return;
|
| 218 |
-
h.setPointerCapture(e.pointerId); p.style.zIndex=++z;
|
| 219 |
-
const sx=e.clientX-p.offsetLeft, sy=e.clientY-p.offsetTop;
|
| 220 |
-
h.onpointermove=m=>{
|
| 221 |
-
p.style.left=cl(m.clientX-sx,-40,innerWidth-70)+"px";
|
| 222 |
-
p.style.top =cl(m.clientY-sy,0,innerHeight-70)+"px"; };
|
| 223 |
-
h.onpointerup=()=>{h.onpointermove=null;h.onpointerup=null}; };
|
| 224 |
-
}
|
| 225 |
-
function resize(g,p){
|
| 226 |
-
g.onpointerdown=e=>{ e.stopPropagation(); g.setPointerCapture(e.pointerId);
|
| 227 |
-
const sw=p.offsetWidth-e.clientX, sh=p.offsetHeight-e.clientY;
|
| 228 |
-
g.onpointermove=m=>{ p.style.width=Math.max(230,sw+m.clientX)+"px";
|
| 229 |
-
p.style.height=Math.max(150,sh+m.clientY)+"px"; p._h=p.style.height;
|
| 230 |
-
if(p._fit)p._fit(); };
|
| 231 |
-
g.onpointerup=()=>{g.onpointermove=null;g.onpointerup=null}; };
|
| 232 |
-
}
|
| 233 |
-
const refresh=id=>{ const p=open.get(id); if(p&&p._draw) p._draw(); };
|
| 234 |
-
|
| 235 |
-
/* =========================== the doors ================================== */
|
| 236 |
-
function doorPane(name){
|
| 237 |
-
pane("door:"+name,"door Β· "+name,(b,p)=>{
|
| 238 |
-
b.innerHTML='<div class=stream><div class=said></div>'+
|
| 239 |
-
'<input placeholder="say it into this door"></div>';
|
| 240 |
-
b.style.padding="8px";
|
| 241 |
-
const said=b.querySelector(".said"), inp=b.querySelector("input");
|
| 242 |
-
p._draw=()=>{
|
| 243 |
-
said.innerHTML=(doors.get(name)||[]).slice(-300).map(m=>
|
| 244 |
-
'<div><span class=key style="background:'+grad(m.from)+
|
| 245 |
-
'"></span> <i>'+(m.from===MYID?"you":m.from.slice(0,6))+'</i> '+
|
| 246 |
-
esc(m.body)+'</div>').join("");
|
| 247 |
-
said.scrollTop=said.scrollHeight; };
|
| 248 |
-
inp.onkeydown=e=>{ if(e.key!=="Enter")return;
|
| 249 |
-
const v=inp.value.trim(); if(!v)return; inp.value="";
|
| 250 |
-
speak(name,v); };
|
| 251 |
-
p._draw();
|
| 252 |
-
},380,300);
|
| 253 |
-
}
|
| 254 |
-
const esc=s=>s.replace(/[&<>]/g,c=>({"&":"&","<":"<",">":">"}[c]));
|
| 255 |
-
|
| 256 |
-
const DOORS={
|
| 257 |
-
doors:(b,p)=>{
|
| 258 |
-
p._draw=()=>{
|
| 259 |
-
b.innerHTML="<h4>your doors β a door is a label, not a socket</h4>"+
|
| 260 |
-
myDoors.map(d=>'<button class="btn g" data-d="'+d+'">'+d+'</button>').join("")+
|
| 261 |
-
'<h4>cut a new one</h4><input id=nd placeholder="name it β anything">'+
|
| 262 |
-
'<button class="btn" id=cut>cut the door</button>'+
|
| 263 |
-
'<p><small>Opening one costs nothing. There was never a socket per door β '+
|
| 264 |
-
'the name <b>is</b> the route, so the count has no ceiling.</small></p>';
|
| 265 |
-
b.querySelectorAll("[data-d]").forEach(x=>x.onclick=()=>doorPane(x.dataset.d));
|
| 266 |
-
b.querySelector("#cut").onclick=()=>{
|
| 267 |
-
const v=b.querySelector("#nd").value.trim().toLowerCase().replace(/\s+/g,"-");
|
| 268 |
-
if(!v)return; if(!myDoors.includes(v)){myDoors.push(v);saveDoors()}
|
| 269 |
-
p._draw(); doorPane(v); openDoorEverywhere(v); };
|
| 270 |
-
};
|
| 271 |
-
p._draw();
|
| 272 |
-
},
|
| 273 |
-
people:(b,p)=>{
|
| 274 |
-
p._draw=()=>{
|
| 275 |
-
const live=[...peers.keys()];
|
| 276 |
-
b.innerHTML="<h4>here now</h4>"+
|
| 277 |
-
(live.length?live.map(c=>'<span class=chip title="'+c+'" style="background:'+
|
| 278 |
-
grad(c)+'"></span>').join(""):"<small>nobody yet</small>")+
|
| 279 |
-
"<h4>colours you carry β "+held.size+"</h4>"+
|
| 280 |
-
(held.size?[...held].map(c=>'<span class=chip title="'+c+'" style="background:'+
|
| 281 |
-
grad(c)+'"></span>').join(""):"<small>none yet</small>")+
|
| 282 |
-
"<p><small>The colour outlives the link. Nothing here is a contact list β "+
|
| 283 |
-
"there is no entry to delete, because there was never an entry.</small></p>";
|
| 284 |
-
};
|
| 285 |
-
p._draw();
|
| 286 |
-
},
|
| 287 |
-
say:(b,p)=>{ b.innerHTML="";
|
| 288 |
-
const sel=document.createElement("select");
|
| 289 |
-
const redraw=()=>{ sel.innerHTML=myDoors.map(d=>"<option>"+d+"</option>").join("") };
|
| 290 |
-
redraw(); b.appendChild(sel);
|
| 291 |
-
const s=document.createElement("div"); s.className="said";
|
| 292 |
-
s.style.cssText="height:calc(100% - 92px);overflow:auto"; b.appendChild(s);
|
| 293 |
-
const i=document.createElement("input"); i.placeholder="say it"; b.appendChild(i);
|
| 294 |
-
p._draw=()=>{ redraw();
|
| 295 |
-
s.innerHTML=(doors.get(sel.value)||[]).slice(-300).map(m=>
|
| 296 |
-
'<div><span class=key style="background:'+grad(m.from)+'"></span> <i>'+
|
| 297 |
-
(m.from===MYID?"you":m.from.slice(0,6))+'</i> '+esc(m.body)+'</div>').join("");
|
| 298 |
-
s.scrollTop=s.scrollHeight; };
|
| 299 |
-
sel.onchange=p._draw;
|
| 300 |
-
i.onkeydown=e=>{ if(e.key!=="Enter")return; const v=i.value.trim();
|
| 301 |
-
if(!v)return; i.value=""; speak(sel.value,v); };
|
| 302 |
-
p._draw();
|
| 303 |
-
},
|
| 304 |
-
paint:(b,p)=>{
|
| 305 |
-
b.style.padding="6px"; b.innerHTML='<canvas class=paint></canvas>';
|
| 306 |
-
const c=b.querySelector("canvas"), g=c.getContext("2d");
|
| 307 |
-
let last=null;
|
| 308 |
-
p._fit=()=>{ const r=c.getBoundingClientRect();
|
| 309 |
-
c.width=Math.max(2,r.width*2); c.height=Math.max(2,r.height*2); p._draw(); };
|
| 310 |
-
p._draw=()=>{ g.fillStyle="#050309"; g.fillRect(0,0,c.width,c.height);
|
| 311 |
-
g.lineCap="round";
|
| 312 |
-
for(const s of (strokes.get("light")||[])){
|
| 313 |
-
g.strokeStyle=ink(s[4]); g.lineWidth=5;
|
| 314 |
-
g.beginPath(); g.moveTo(s[2]*c.width,s[3]*c.height);
|
| 315 |
-
g.lineTo(s[0]*c.width,s[1]*c.height); g.stroke(); } };
|
| 316 |
-
const at=e=>{const r=c.getBoundingClientRect();
|
| 317 |
-
return [(e.clientX-r.left)/r.width,(e.clientY-r.top)/r.height]};
|
| 318 |
-
c.onpointerdown=e=>{ c.setPointerCapture(e.pointerId); last=at(e); };
|
| 319 |
-
c.onpointermove=e=>{ if(!last)return; const n=at(e);
|
| 320 |
-
addStroke("light",[n[0],n[1],last[0],last[1],MYID]);
|
| 321 |
-
gossip("light","STROKE|from="+MYID+"|s="+
|
| 322 |
-
[n[0],n[1],last[0],last[1]].map(v=>v.toFixed(4)).join(",")+"|json=0");
|
| 323 |
-
last=n; };
|
| 324 |
-
c.onpointerup=()=>last=null;
|
| 325 |
-
setTimeout(p._fit,60);
|
| 326 |
-
},
|
| 327 |
-
carry:(b,p)=>{
|
| 328 |
-
p._draw=()=>{
|
| 329 |
-
b.innerHTML="<h4>hand something over</h4>"+
|
| 330 |
-
'<input type=file id=f multiple>'+
|
| 331 |
-
"<h4>what arrived β "+carried.length+"</h4>"+
|
| 332 |
-
(carried.length?carried.map((c,i)=>
|
| 333 |
-
'<div class=row><span>'+esc(c.name)+'</span>'+
|
| 334 |
-
'<a class=acc download="'+esc(c.name)+'" href="'+c.url+'">keep it</a></div>'
|
| 335 |
-
).join(""):"<small>nothing yet</small>")+
|
| 336 |
-
"<p><small>It goes down the same line as the words. Nobody hosts it, "+
|
| 337 |
-
"nobody stores it, and it never leaves the two machines.</small></p>";
|
| 338 |
-
b.querySelector("#f").onchange=e=>{ for(const f of e.target.files) handOver(f); };
|
| 339 |
-
};
|
| 340 |
-
p._draw();
|
| 341 |
-
},
|
| 342 |
-
rooms:(b,p)=>{
|
| 343 |
-
b.style.padding="8px";
|
| 344 |
-
b.innerHTML="<h4>a private room only people who know the word can enter</h4>"+
|
| 345 |
-
'<input id=rn placeholder="a word — or a whole phrase" value="light">'+
|
| 346 |
-
'<canvas id=rc style="width:100%;height:120px;display:block;'+
|
| 347 |
-
'border:1px solid var(--line);border-radius:8px;margin:6px 0"></canvas>'+
|
| 348 |
-
'<div class=row><span class=k>this room's address</span><span id=ra class=acc>—</span></div>'+
|
| 349 |
-
'<button class="btn" id=renter>enter this room</button>'+
|
| 350 |
-
'<button class="btn g" id=rgo>just look at it</button>'+
|
| 351 |
-
"<p><small>The room's address is <b>sha256 of your word</b>. Type the same "+
|
| 352 |
-
"word as someone else and you land in the <b>same room</b> — talk, draw, "+
|
| 353 |
-
"hand files over. Type a different word and you are somewhere else entirely. "+
|
| 354 |
-
"<b>Nobody who doesn't know the word can find it</b>: there is no list of "+
|
| 355 |
-
"rooms to browse, and the address gives the word away no more than a lock gives "+
|
| 356 |
-
"away its key. No server, no account, no directory.</small></p>"+
|
| 357 |
-
"<p><small>Same law as the registration office — a room 10^30 places along "+
|
| 358 |
-
"is reached as fast as the first, because there is no table to walk.</small></p>";
|
| 359 |
-
const CUBE="b40b3e1726b0e81d";
|
| 360 |
-
const c=b.querySelector("#rc"), g=c.getContext("2d");
|
| 361 |
-
p._fit=()=>{const r=c.getBoundingClientRect();
|
| 362 |
-
c.width=Math.max(2,r.width*2); c.height=Math.max(2,r.height*2); if(p._t)show(p._t);};
|
| 363 |
-
function show(t){ p._t=t;
|
| 364 |
-
const W=c.width,H=c.height,N=32,cw=W/N,ch=H/N;
|
| 365 |
-
g.fillStyle="#050309"; g.fillRect(0,0,W,H);
|
| 366 |
-
for(let i=0;i<1024;i++){
|
| 367 |
-
const R=Math.min(255,Math.abs(t[i]<<24>>24)*2), G=(15-t[1152+i])*17, B=t[2176+i];
|
| 368 |
-
const neg=(t[1024+(i>>3)]>>(i&7))&1;
|
| 369 |
-
g.fillStyle="rgb("+(neg?B:R)+","+G+","+(neg?R:B)+")";
|
| 370 |
-
g.fillRect((i%N)*cw,((i/N)|0)*ch,cw+1,ch+1);
|
| 371 |
-
}
|
| 372 |
-
}
|
| 373 |
-
async function addrOf(word){ return await sha(CUBE+"|"+word); }
|
| 374 |
-
async function look(word){
|
| 375 |
-
const full=await addrOf(word);
|
| 376 |
-
b.querySelector("#ra").textContent=full.slice(0,16);
|
| 377 |
-
show(quant(new TextEncoder().encode(full)));
|
| 378 |
-
return full;
|
| 379 |
-
}
|
| 380 |
-
async function enter(word){
|
| 381 |
-
if(!word){ toast("type a word first"); return; }
|
| 382 |
-
const full=await look(word);
|
| 383 |
-
const room="r."+full.slice(0,12); /* the door IS the address */
|
| 384 |
-
if(!myDoors.includes(room)){ myDoors.push(room); saveDoors(); }
|
| 385 |
-
if(!doors.has(room)) doors.set(room,[]);
|
| 386 |
-
doorPane(room); /* live, synced via the mesh */
|
| 387 |
-
openDoorEverywhere(room);
|
| 388 |
-
absorb(full.slice(0,16));
|
| 389 |
-
toast(peers.size ? "entered — "+peers.size+" boat(s) can meet you here"
|
| 390 |
-
: "entered — give this word to who you want here");
|
| 391 |
-
}
|
| 392 |
-
b.querySelector("#renter").onclick=()=>enter((b.querySelector("#rn").value||"").trim());
|
| 393 |
-
b.querySelector("#rgo").onclick=()=>look((b.querySelector("#rn").value||"light").trim());
|
| 394 |
-
b.querySelector("#rn").onkeydown=e=>{ if(e.key==="Enter") enter((e.target.value||"").trim()); };
|
| 395 |
-
setTimeout(()=>{p._fit(); look("light");},60);
|
| 396 |
-
},
|
| 397 |
-
play:(b,p)=>{
|
| 398 |
-
b.style.padding="8px";
|
| 399 |
-
b.innerHTML="<h4>hand anything in β it leaves as 3.1 KB</h4>"+
|
| 400 |
-
'<input type=file id=pf><input id=pt placeholder="or type something">'+
|
| 401 |
-
'<canvas id=pc style="width:100%;height:150px;display:block;'+
|
| 402 |
-
'border:1px solid var(--line);border-radius:8px;margin:6px 0"></canvas>'+
|
| 403 |
-
'<div class=row><span class=k>in</span><span id=pin>β</span></div>'+
|
| 404 |
-
'<div class=row><span class=k>out</span><span class=acc id=pout>β</span></div>'+
|
| 405 |
-
'<div class=row><span class=k>colour</span><span id=psha class=acc>β</span></div>'+
|
| 406 |
-
'<button class="btn" id=psend>play it to everyone</button>'+
|
| 407 |
-
"<p><small>The tuple is <b>3,200 bytes whatever you hand it</b> β the "+
|
| 408 |
-
"same four bands the engine writes. What crosses is the light, "+
|
| 409 |
-
"<b>not the file</b>. Nothing is saved on either side.</small></p>";
|
| 410 |
-
const c=b.querySelector("#pc"), g=c.getContext("2d");
|
| 411 |
-
p._fit=()=>{const r=c.getBoundingClientRect();
|
| 412 |
-
c.width=Math.max(2,r.width*2); c.height=Math.max(2,r.height*2); if(p._last)show(p._last)};
|
| 413 |
-
function show(t){
|
| 414 |
-
p._last=t;
|
| 415 |
-
const W=c.width,H=c.height,N=32,cw=W/N,ch=H/N;
|
| 416 |
-
g.fillStyle="#050309"; g.fillRect(0,0,W,H);
|
| 417 |
-
for(let i=0;i<1024;i++){
|
| 418 |
-
const R=Math.min(255,Math.abs(t[i]<<24>>24)*2); /* turbo, signed */
|
| 419 |
-
const G=(15-t[1152+i])*17; /* zeta */
|
| 420 |
-
const B=t[2176+i]; /* hist bytes */
|
| 421 |
-
const neg=(t[1024+(i>>3)]>>(i&7))&1; /* signs = the anti */
|
| 422 |
-
g.fillStyle="rgb("+(neg?B:R)+","+G+","+(neg?R:B)+")";
|
| 423 |
-
g.fillRect((i%N)*cw,((i/N)|0)*ch,cw+1,ch+1);
|
| 424 |
-
}
|
| 425 |
-
}
|
| 426 |
-
p._show=show;
|
| 427 |
-
async function take(bytes,label){
|
| 428 |
-
const t=quant(bytes);
|
| 429 |
-
const h=await shaBytes(t);
|
| 430 |
-
b.querySelector("#pin").textContent=label+" Β· "+bytes.length.toLocaleString()+" B";
|
| 431 |
-
b.querySelector("#pout").textContent=t.length.toLocaleString()+" B";
|
| 432 |
-
b.querySelector("#psha").textContent=h.slice(0,16);
|
| 433 |
-
show(t); p._t=t; absorb(h.slice(0,16));
|
| 434 |
-
}
|
| 435 |
-
b.querySelector("#pf").onchange=async e=>{ const f=e.target.files[0]; if(!f)return;
|
| 436 |
-
take(new Uint8Array(await f.arrayBuffer()), f.name); };
|
| 437 |
-
b.querySelector("#pt").onkeydown=e=>{ if(e.key!=="Enter")return;
|
| 438 |
-
take(new TextEncoder().encode(e.target.value), "typed"); };
|
| 439 |
-
b.querySelector("#psend").onclick=()=>{
|
| 440 |
-
if(!p._t){ toast("hand something in first"); return; }
|
| 441 |
-
if(!peers.size){ toast("nobody to play it to yet"); return; }
|
| 442 |
-
for(const [,rec] of peers){
|
| 443 |
-
const ch=rec.pc.createDataChannel("play:"+Date.now());
|
| 444 |
-
ch.binaryType="arraybuffer";
|
| 445 |
-
ch.onopen=()=>{ ch.send(p._t.buffer.slice(0)); setTimeout(()=>ch.close(),300); };
|
| 446 |
-
}
|
| 447 |
-
toast("played Β· 3,200 bytes to "+peers.size);
|
| 448 |
-
};
|
| 449 |
-
setTimeout(p._fit,60);
|
| 450 |
-
},
|
| 451 |
-
all:(b,p)=>{
|
| 452 |
-
const go=u=>"onclick=\"window.open('"+u+"','_blank','noopener')\"";
|
| 453 |
-
b.innerHTML=
|
| 454 |
-
"<h4>every light, one door</h4>"+
|
| 455 |
-
"<p>This hub is one room. <b>The bridge is the door to all of them</b> β "+
|
| 456 |
-
"every public light on the account in one place, so none of it gets lost "+
|
| 457 |
-
"like Tesla.</p>"+
|
| 458 |
-
'<button class="btn" '+go("https://jessebrown1980.github.io/the-bridge-to-all-of-it/")+
|
| 459 |
-
'>open the bridge → all 182</button>'+
|
| 460 |
-
"<h4>the nearest few</h4>"+
|
| 461 |
-
'<button class="btn g" '+go("https://jessebrown1980.github.io/lightDOORwsMs/")+'>this hub</button>'+
|
| 462 |
-
'<button class="btn g" '+go("https://jessebrown1980.github.io/the-light-boat/")+'>the light boat</button>'+
|
| 463 |
-
'<button class="btn g" '+go("https://jessebrown1980.github.io/Be-infinitly-express-spherically-free/")+'>the sphere</button>'+
|
| 464 |
-
'<button class="btn g" '+go("https://jessebrown1980.github.io/the-null-space-or-NO-jo0O/")+'>the null space</button>'+
|
| 465 |
-
'<button class="btn g" '+go("https://jessebrown1980.github.io/the-zebras-z-is-the-squared-wave/")+'>the zebra</button>'+
|
| 466 |
-
"<p><small>Photons are the same photon with different colours and oils; "+
|
| 467 |
-
"these are the same light with different doors. The bridge holds them all, "+
|
| 468 |
-
"and the bridge points back here — so from any one you can reach every "+
|
| 469 |
-
"other.</small></p>";
|
| 470 |
-
},
|
| 471 |
-
what:(b)=>{ b.innerHTML=
|
| 472 |
-
"<h4>a door is a label, not a socket</h4>"+
|
| 473 |
-
"<p>Cut as many as you like. The name <b>is</b> the route, so there is no "+
|
| 474 |
-
"count to run out of β that ceiling only exists where somebody hands out "+
|
| 475 |
-
"numbered slots.</p>"+
|
| 476 |
-
"<h4>the colour is the address</h4>"+
|
| 477 |
-
"<p>Your colour is the sha-256 of a secret this browser made for itself the "+
|
| 478 |
-
"first time you opened the page β two brown stops, banded 16β¦240, "+
|
| 479 |
-
"luminance-clamped 32β¦224. <b>The secret never leaves; only the colour does.</b> "+
|
| 480 |
-
"Nobody handed you the name, so nobody can take it away, and it does not "+
|
| 481 |
-
"change when a link does.</p>"+
|
| 482 |
-
"<h4>the trace is spent, the colour is kept</h4>"+
|
| 483 |
-
"<p>The block you pass at the first meeting is a <b>one-time introduction</b> "+
|
| 484 |
-
"and this page throws it away on contact. What you keep is the colour. Break "+
|
| 485 |
-
"every link at once and it reassembles, because what survived was never the "+
|
| 486 |
-
"wiring.</p>"+
|
| 487 |
-
"<h4>what is free, and the one thing that isn't</h4>"+
|
| 488 |
-
"<p>No server of ours touches any of it β words, drawings, files, all of it "+
|
| 489 |
-
"goes browser to browser with nothing in the middle to store or relay. The one "+
|
| 490 |
-
"thing that isn't free, stated rather than hidden: to find a route across the "+
|
| 491 |
-
"internet, browsers ask a public <b>STUN</b> server for their own outside "+
|
| 492 |
-
"address. Free ones. <b>They see that two addresses asked; they never see a "+
|
| 493 |
-
"word of what you say.</b> On the same wifi β or down a USB cable β it doesn't "+
|
| 494 |
-
"need them at all.</p>"+
|
| 495 |
-
"<h4>the first meeting is still by hand</h4>"+
|
| 496 |
-
"<p>Everything after it travels the links that already exist: you are "+
|
| 497 |
-
"introduced by somebody who already knows you both. That is the only "+
|
| 498 |
-
"directory there is, and it is not a list anyone can seize.</p>" }
|
| 499 |
-
};
|
| 500 |
-
|
| 501 |
-
/* =========================== traffic ==================================== */
|
| 502 |
-
function addStroke(door,s){
|
| 503 |
-
if(!strokes.has(door)) strokes.set(door,[]);
|
| 504 |
-
const a=strokes.get(door); a.push(s); if(a.length>4000)a.shift();
|
| 505 |
-
refresh("paint");
|
| 506 |
-
}
|
| 507 |
-
function record(door,from,body){
|
| 508 |
-
if(!doors.has(door)) doors.set(door,[]);
|
| 509 |
-
doors.get(door).push({from,body});
|
| 510 |
-
refresh("door:"+door); refresh("say");
|
| 511 |
-
}
|
| 512 |
-
async function mid(){ return (await sha(MYID+Math.random()+performance.now())).slice(0,10) }
|
| 513 |
-
async function speak(door,text){
|
| 514 |
-
const id=await mid(); seen.add(id);
|
| 515 |
-
record(door,MYID,text);
|
| 516 |
-
fan(door,"SAY|from="+MYID+"|mid="+id+"|body="+text+"|json=0");
|
| 517 |
-
}
|
| 518 |
-
async function gossip(door,row){
|
| 519 |
-
const id=await mid(); seen.add(id);
|
| 520 |
-
fan(door,row.replace("|json=0","|mid="+id+"|json=0"));
|
| 521 |
-
}
|
| 522 |
-
function fan(door,row,except){
|
| 523 |
-
for(const [pid,rec] of peers){ if(pid===except) continue;
|
| 524 |
-
let ch=rec.doors.get(door);
|
| 525 |
-
if(!ch){ ch=rec.pc.createDataChannel("door:"+door);
|
| 526 |
-
ch.onmessage=e=>onDoor(pid,door,e.data); rec.doors.set(door,ch); }
|
| 527 |
-
try{ if(ch.readyState==="open") ch.send(row) }catch(e){}
|
| 528 |
-
}
|
| 529 |
-
}
|
| 530 |
-
function openDoorEverywhere(door){ fan(door,"OPEN|from="+MYID+"|json=0") }
|
| 531 |
-
function onDoor(from,door,row){
|
| 532 |
-
const f={}; row.split("|").forEach(kv=>{const i=kv.indexOf("=");
|
| 533 |
-
if(i>0) f[kv.slice(0,i)]=kv.slice(i+1)});
|
| 534 |
-
if(f.mid){ if(seen.has(f.mid)) return; seen.add(f.mid); }
|
| 535 |
-
absorb(f.from);
|
| 536 |
-
const tag=row.split("|")[0];
|
| 537 |
-
if(tag==="SAY"){ record(door,f.from,f.body);
|
| 538 |
-
if(!open.has("door:"+door)&&!open.has("say")) toast(door+" Β· "+f.body.slice(0,44)); }
|
| 539 |
-
else if(tag==="STROKE"){ const n=f.s.split(",").map(Number);
|
| 540 |
-
addStroke(door,[n[0],n[1],n[2],n[3],f.from]); }
|
| 541 |
-
if(!myDoors.includes(door)){ myDoors.push(door); saveDoors(); refresh("doors"); }
|
| 542 |
-
fan(door,row,from);
|
| 543 |
-
}
|
| 544 |
-
|
| 545 |
-
/* ---- handing a thing over: its own channel, closed when done ---- */
|
| 546 |
-
async function handOver(file){
|
| 547 |
-
if(!peers.size){ toast("nobody to hand it to yet"); return }
|
| 548 |
-
const fid=(await sha(file.name+file.size+Math.random())).slice(0,10);
|
| 549 |
-
for(const [pid,rec] of peers){
|
| 550 |
-
const ch=rec.pc.createDataChannel("give:"+fid);
|
| 551 |
-
ch.binaryType="arraybuffer";
|
| 552 |
-
ch.onopen=async()=>{
|
| 553 |
-
ch.send("GIVE|name="+file.name+"|size="+file.size+"|json=0");
|
| 554 |
-
const buf=await file.arrayBuffer(); const CH=16384;
|
| 555 |
-
for(let o=0;o<buf.byteLength;o+=CH){
|
| 556 |
-
while(ch.bufferedAmount>1<<20) await new Promise(r=>setTimeout(r,25));
|
| 557 |
-
ch.send(buf.slice(o,o+CH));
|
| 558 |
-
}
|
| 559 |
-
ch.send("DONE"); setTimeout(()=>ch.close(),400);
|
| 560 |
-
};
|
| 561 |
-
}
|
| 562 |
-
toast("handed over Β· "+file.name);
|
| 563 |
-
}
|
| 564 |
-
function takeGift(ch){
|
| 565 |
-
ch.binaryType="arraybuffer";
|
| 566 |
-
let name="a thing", parts=[];
|
| 567 |
-
ch.onmessage=e=>{
|
| 568 |
-
if(typeof e.data==="string"){
|
| 569 |
-
if(e.data.startsWith("GIVE|")){
|
| 570 |
-
name=(e.data.match(/name=([^|]*)/)||[])[1]||name; return; }
|
| 571 |
-
if(e.data==="DONE"){
|
| 572 |
-
const url=URL.createObjectURL(new Blob(parts));
|
| 573 |
-
carried.push({name,url}); refresh("carry");
|
| 574 |
-
toast("arrived Β· "+name); parts=[]; }
|
| 575 |
-
return;
|
| 576 |
-
}
|
| 577 |
-
parts.push(e.data);
|
| 578 |
-
};
|
| 579 |
-
}
|
| 580 |
-
|
| 581 |
-
/* =========================== the mesh =================================== */
|
| 582 |
-
function reduce(d){
|
| 583 |
-
const s=d.sdp, g=re=>{const m=s.match(re);return m?m[1]:""};
|
| 584 |
-
const c=[...s.matchAll(/a=candidate:(\S+ \d+ \S+ \d+ \S+ \d+ typ \S+)/g)]
|
| 585 |
-
.map(m=>m[1].replace(/ generation.*$/,""));
|
| 586 |
-
return [d.type==="offer"?"O":"A",g(/a=ice-ufrag:(\S+)/),g(/a=ice-pwd:(\S+)/),
|
| 587 |
-
g(/a=fingerprint:sha-256 (\S+)/).replace(/:/g,""),g(/a=setup:(\S+)/),
|
| 588 |
-
c.join("~")].join("|");
|
| 589 |
-
}
|
| 590 |
-
function expand(str){
|
| 591 |
-
const [t,uf,pw,fp,su,c]=str.trim().split("|");
|
| 592 |
-
const cands=(c||"").split("~").filter(Boolean)
|
| 593 |
-
.map(x=>"a=candidate:"+x+" generation 0").join("\r\n");
|
| 594 |
-
return {type:t==="O"?"offer":"answer",sdp:
|
| 595 |
-
"v=0\r\no=- 1 2 IN IP4 127.0.0.1\r\ns=-\r\nt=0 0\r\na=group:BUNDLE 0\r\n"+
|
| 596 |
-
"a=extmap-allow-mixed\r\na=msid-semantic: WMS\r\n"+
|
| 597 |
-
"m=application 9 UDP/DTLS/SCTP webrtc-datachannel\r\nc=IN IP4 0.0.0.0\r\n"+
|
| 598 |
-
(cands?cands+"\r\n":"")+"a=ice-ufrag:"+uf+"\r\na=ice-pwd:"+pw+"\r\n"+
|
| 599 |
-
"a=ice-options:trickle\r\na=fingerprint:sha-256 "+fp.match(/.{2}/g).join(":")+
|
| 600 |
-
"\r\na=setup:"+su+"\r\na=mid:0\r\na=sctp-port:5000\r\n"+
|
| 601 |
-
"a=max-message-size:262144\r\n"};
|
| 602 |
-
}
|
| 603 |
-
const gather=pc=>new Promise(r=>{
|
| 604 |
-
if(pc.iceGatheringState==="complete") return r();
|
| 605 |
-
pc.onicecandidate=e=>{ if(!e.candidate) r() }; setTimeout(r,3000); });
|
| 606 |
-
const send=(c,t)=>{try{if(c&&c.readyState==="open")c.send(t)}catch(e){}};
|
| 607 |
-
|
| 608 |
-
function greet(pc,ctl){
|
| 609 |
-
const hi=()=>send(ctl,"IAM|id="+MYID+"|json=0");
|
| 610 |
-
ctl.onopen=hi; if(ctl.readyState==="open") hi();
|
| 611 |
-
ctl.onmessage=e=>{ const m=e.data.match(/^IAM\|id=([0-9a-f]+)/);
|
| 612 |
-
if(!m) return;
|
| 613 |
-
/* the same colour on both ends is not two boats β it is one machine
|
| 614 |
-
talking to itself (a second tab of the same browser). Say so. */
|
| 615 |
-
if(m[1]===MYID){ toast("that is this same browser β same colour, same boat");
|
| 616 |
-
try{pc.close()}catch(e){} return; }
|
| 617 |
-
if(!peers.has(m[1])) attach(m[1],pc,ctl); };
|
| 618 |
-
pc.ondatachannel=e=>{ const L=e.channel.label;
|
| 619 |
-
if(L.startsWith("give:")) takeGift(e.channel);
|
| 620 |
-
else if(L.startsWith("play:")) playIn(e.channel); };
|
| 621 |
-
}
|
| 622 |
-
function attach(id,pc,ctl){
|
| 623 |
-
const rec={pc,ctl,doors:new Map()}; peers.set(id,rec);
|
| 624 |
-
absorb(id); spend();
|
| 625 |
-
ctl.onmessage=e=>onCtl(id,e.data);
|
| 626 |
-
ctl.onclose=()=>{peers.delete(id);count();refresh("people");
|
| 627 |
-
toast(id.slice(0,6)+" unlinked Β· colour kept")};
|
| 628 |
-
pc.ondatachannel=e=>{
|
| 629 |
-
const L=e.channel.label;
|
| 630 |
-
if(L.startsWith("door:")){ const d=L.slice(5);
|
| 631 |
-
e.channel.onmessage=ev=>onDoor(id,d,ev.data); rec.doors.set(d,e.channel);
|
| 632 |
-
if(!myDoors.includes(d)){myDoors.push(d);saveDoors();refresh("doors")} }
|
| 633 |
-
else if(L.startsWith("give:")) takeGift(e.channel);
|
| 634 |
-
else if(L.startsWith("play:")) playIn(e.channel);
|
| 635 |
-
};
|
| 636 |
-
for(const d of myDoors) fan(d,"OPEN|from="+MYID+"|json=0");
|
| 637 |
-
count(); refresh("people"); toast(id.slice(0,6)+" Β· colour absorbed");
|
| 638 |
-
const others=[...peers.keys()].filter(x=>x!==id);
|
| 639 |
-
if(others.length) send(ctl,"HELLO|id="+MYID+"|peers="+others.join(",")+"|json=0");
|
| 640 |
-
}
|
| 641 |
-
function spend(){ const p=open.get("open");
|
| 642 |
-
if(p){ const a=p.querySelector("#mine"),b=p.querySelector("#theirs");
|
| 643 |
-
if(a)a.value=""; if(b)b.value=""; if(p._spent)p._spent(); } }
|
| 644 |
-
const count=()=>{ $("#mypeers").textContent=peers.size };
|
| 645 |
-
|
| 646 |
-
async function onCtl(from,row){
|
| 647 |
-
const f={}; row.split("|").forEach(kv=>{const i=kv.indexOf("=");
|
| 648 |
-
if(i>0) f[kv.slice(0,i)]=kv.slice(i+1)});
|
| 649 |
-
const tag=row.split("|")[0];
|
| 650 |
-
if(tag==="HELLO"){
|
| 651 |
-
for(const p of (f.peers||"").split(",").filter(Boolean)){
|
| 652 |
-
if(p===MYID||peers.has(p)||pending.has(p)) continue;
|
| 653 |
-
const pc=new RTCPeerConnection(ICE);
|
| 654 |
-
greet(pc,pc.createDataChannel("ctl"));
|
| 655 |
-
await pc.setLocalDescription(await pc.createOffer()); await gather(pc);
|
| 656 |
-
pending.set(p,pc);
|
| 657 |
-
send(peers.get(from).ctl,"INTRO|to="+p+"|from="+MYID+"|sdp="+
|
| 658 |
-
reduce(pc.localDescription)+"|json=0");
|
| 659 |
-
}
|
| 660 |
-
} else if(tag==="INTRO"){
|
| 661 |
-
if(f.to===MYID){
|
| 662 |
-
const pc=new RTCPeerConnection(ICE);
|
| 663 |
-
pc.ondatachannel=e=>{ if(e.channel.label==="ctl") greet(pc,e.channel) };
|
| 664 |
-
await pc.setRemoteDescription(expand(f.sdp));
|
| 665 |
-
await pc.setLocalDescription(await pc.createAnswer()); await gather(pc);
|
| 666 |
-
send(peers.get(from).ctl,"BACK|to="+f.from+"|from="+MYID+"|sdp="+
|
| 667 |
-
reduce(pc.localDescription)+"|json=0");
|
| 668 |
-
} else if(peers.has(f.to)) send(peers.get(f.to).ctl,row);
|
| 669 |
-
} else if(tag==="BACK"){
|
| 670 |
-
if(f.to===MYID&&pending.has(f.from)){
|
| 671 |
-
await pending.get(f.from).setRemoteDescription(expand(f.sdp));
|
| 672 |
-
pending.delete(f.from);
|
| 673 |
-
} else if(peers.has(f.to)) send(peers.get(f.to).ctl,row);
|
| 674 |
-
}
|
| 675 |
-
}
|
| 676 |
-
|
| 677 |
-
/* =========================== the opening ================================ */
|
| 678 |
-
function openPane(){
|
| 679 |
-
pane("open","the first meeting β by hand, once",(b,p)=>{
|
| 680 |
-
b.innerHTML=
|
| 681 |
-
"<p>Give <b>one</b> person your block. After that, everyone else arrives "+
|
| 682 |
-
"through the links that already exist.</p>"+
|
| 683 |
-
'<h4 id=lbl>yours β hand it over</h4><textarea id=mine readonly></textarea>'+
|
| 684 |
-
'<button class="btn g" id=copy>copy</button>'+
|
| 685 |
-
'<button class="btn g" id=mint>mint a fresh one</button>'+
|
| 686 |
-
'<h4>theirs β paste it</h4><textarea id=theirs></textarea>'+
|
| 687 |
-
'<button class="btn" id=take>take it</button>'+
|
| 688 |
-
'<div class=row style="margin-top:8px"><span class=k>traces kept</span>'+
|
| 689 |
-
'<span class=ok id=nt>0</span></div>';
|
| 690 |
-
p._spent=()=>{ b.querySelector("#nt").textContent="0";
|
| 691 |
-
b.querySelector("#lbl").textContent="spent β mint a fresh one for the next person"; };
|
| 692 |
-
b.querySelector("#copy").onclick=()=>{
|
| 693 |
-
b.querySelector("#mine").select(); document.execCommand("copy"); toast("copied"); };
|
| 694 |
-
b.querySelector("#mint").onclick=()=>mint(b);
|
| 695 |
-
b.querySelector("#take").onclick=()=>take(b);
|
| 696 |
-
mint(b);
|
| 697 |
-
},370,430);
|
| 698 |
-
}
|
| 699 |
-
async function mint(b){
|
| 700 |
-
isHost=true;
|
| 701 |
-
bootPc=new RTCPeerConnection(ICE);
|
| 702 |
-
greet(bootPc,bootPc.createDataChannel("ctl"));
|
| 703 |
-
await bootPc.setLocalDescription(await bootPc.createOffer());
|
| 704 |
-
await gather(bootPc);
|
| 705 |
-
const small=reduce(bootPc.localDescription);
|
| 706 |
-
b.querySelector("#mine").value=small;
|
| 707 |
-
b.querySelector("#nt").textContent="1";
|
| 708 |
-
b.querySelector("#lbl").textContent="yours β one use, for one person";
|
| 709 |
-
if(!MYID) await name(small);
|
| 710 |
-
}
|
| 711 |
-
async function take(b){
|
| 712 |
-
const raw=b.querySelector("#theirs").value.trim(); if(!raw) return;
|
| 713 |
-
let d; try{ d=expand(raw) }catch(e){ toast("that isn't a block"); return }
|
| 714 |
-
if(d.type==="answer"&&isHost&&bootPc){
|
| 715 |
-
try{ await bootPc.setRemoteDescription(d); toast("taken Β· opening") }
|
| 716 |
-
catch(e){ toast("that block was for a different opening") }
|
| 717 |
-
return;
|
| 718 |
-
}
|
| 719 |
-
const pc=new RTCPeerConnection(ICE);
|
| 720 |
-
pc.ondatachannel=e=>{ if(e.channel.label==="ctl") greet(pc,e.channel) };
|
| 721 |
-
await pc.setRemoteDescription(d);
|
| 722 |
-
await pc.setLocalDescription(await pc.createAnswer()); await gather(pc);
|
| 723 |
-
const small=reduce(pc.localDescription);
|
| 724 |
-
bootPc=pc; isHost=false;
|
| 725 |
-
if(!MYID) await name(small);
|
| 726 |
-
b.querySelector("#mine").value=small;
|
| 727 |
-
b.querySelector("#nt").textContent="1";
|
| 728 |
-
b.querySelector("#lbl").textContent="your answer β send it back to them";
|
| 729 |
-
toast("answer ready β send it back");
|
| 730 |
-
}
|
| 731 |
-
async function name(seed){
|
| 732 |
-
MYID=(await sha(seed)).slice(0,16);
|
| 733 |
-
$("#myid").textContent=MYID.slice(0,10);
|
| 734 |
-
$("#mykey").style.background=grad(MYID);
|
| 735 |
-
}
|
| 736 |
-
|
| 737 |
-
/* =========================== the field ================================== */
|
| 738 |
-
const sc=$("#stars"), sg=sc.getContext("2d");
|
| 739 |
-
let SW=0,SH=0,T=0;
|
| 740 |
-
function fitStars(){ SW=sc.width=innerWidth*2; SH=sc.height=innerHeight*2 }
|
| 741 |
-
addEventListener("resize",()=>{fitStars();for(const p of open.values())p._fit&&p._fit()});
|
| 742 |
-
fitStars();
|
| 743 |
-
function field(){
|
| 744 |
-
T+=0.006;
|
| 745 |
-
sg.clearRect(0,0,SW,SH);
|
| 746 |
-
const ids=[MYID,...peers.keys(),...held].filter(Boolean);
|
| 747 |
-
const uniq=[...new Set(ids)];
|
| 748 |
-
const cx=SW/2, cy=SH*0.42, R=Math.min(SW,SH)*0.34;
|
| 749 |
-
uniq.forEach((id,i)=>{
|
| 750 |
-
const a=T+i*(2*Math.PI/Math.max(1,uniq.length));
|
| 751 |
-
const live=id===MYID||peers.has(id);
|
| 752 |
-
const x=cx+Math.cos(a)*R*(live?1:1.35);
|
| 753 |
-
const y=cy+Math.sin(a)*R*0.5*(live?1:1.35);
|
| 754 |
-
const [ca,cb]=stops(id), r=id===MYID?26:live?18:11;
|
| 755 |
-
const g=sg.createRadialGradient(x,y,0,x,y,r*3);
|
| 756 |
-
g.addColorStop(0,"rgba("+ca+","+(live?.5:.2)+")");
|
| 757 |
-
g.addColorStop(1,"rgba(0,0,0,0)");
|
| 758 |
-
sg.fillStyle=g; sg.beginPath(); sg.arc(x,y,r*3,0,6.2832); sg.fill();
|
| 759 |
-
sg.globalAlpha=live?.85:.34; sg.fillStyle="rgb("+cb+")";
|
| 760 |
-
sg.beginPath(); sg.arc(x,y,r,0,6.2832); sg.fill(); sg.globalAlpha=1;
|
| 761 |
-
if(live&&id!==MYID){ sg.strokeStyle="rgba("+cb+",.22)"; sg.lineWidth=2;
|
| 762 |
-
sg.beginPath(); sg.moveTo(cx,cy); sg.lineTo(x,y); sg.stroke(); }
|
| 763 |
-
});
|
| 764 |
-
requestAnimationFrame(field);
|
| 765 |
-
}
|
| 766 |
-
requestAnimationFrame(field);
|
| 767 |
-
|
| 768 |
-
/* =========================== wiring ===================================== */
|
| 769 |
-
$("#d-open").onclick=openPane;
|
| 770 |
-
document.querySelectorAll("#sill button[data-door]").forEach(btn=>{
|
| 771 |
-
btn.onclick=()=>{ const d=btn.dataset.door;
|
| 772 |
-
if(open.has(d)){ open.get(d).remove(); open.delete(d); lit(); return; }
|
| 773 |
-
pane(d,d==="what"?"what is this":d,DOORS[d],
|
| 774 |
-
d==="paint"||d==="play"||d==="rooms"?420:340,
|
| 775 |
-
d==="what"?420:d==="play"||d==="rooms"?400:320); };
|
| 776 |
-
});
|
| 777 |
-
(async()=>{ /* a colour before anyone arrives, from this browser alone */
|
| 778 |
-
let s=null; try{s=localStorage.getItem("ld.seed")}catch(e){}
|
| 779 |
-
if(!s){ s=[...crypto.getRandomValues(new Uint8Array(24))]
|
| 780 |
-
.map(x=>x.toString(16).padStart(2,"0")).join("");
|
| 781 |
-
try{localStorage.setItem("ld.seed",s)}catch(e){} }
|
| 782 |
-
await name(s);
|
| 783 |
-
for(const d of myDoors) if(!doors.has(d)) doors.set(d,[]);
|
| 784 |
-
pane("what","what is this",DOORS.what,360,420);
|
| 785 |
-
toast("your colour is yours β nobody handed it to you");
|
| 786 |
-
})();
|
| 787 |
-
</script>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<meta charset="utf-8">
|
| 2 |
+
<meta name="viewport" content="width=device-width,initial-scale=1,user-scalable=no">
|
| 3 |
+
<title>lightDOORs β a place made of light, with doors instead of windows</title>
|
| 4 |
+
<style>
|
| 5 |
+
:root{--bg:#08060f;--fg:#efeaf2;--dim:#8d8496;--line:#241e30;--gold:#e8c07a;
|
| 6 |
+
--ok:#7fe0a0;--no:#ff8b96;--pane:#0e0a18}
|
| 7 |
+
*{box-sizing:border-box;-webkit-tap-highlight-color:transparent}
|
| 8 |
+
html,body{height:100%;margin:0;overflow:hidden}
|
| 9 |
+
body{background:radial-gradient(120% 90% at 50% -10%,#1a1030 0%,#08060f 60%);
|
| 10 |
+
color:var(--fg);font:13px/1.55 ui-monospace,"SF Mono",Menlo,Consolas,monospace}
|
| 11 |
+
|
| 12 |
+
/* the field the doors stand in */
|
| 13 |
+
#field{position:fixed;inset:0}
|
| 14 |
+
#stars{position:fixed;inset:0;pointer-events:none}
|
| 15 |
+
|
| 16 |
+
/* a pane of light */
|
| 17 |
+
.pane{position:absolute;min-width:230px;min-height:150px;background:rgba(14,10,24,.93);
|
| 18 |
+
border:1px solid var(--line);border-radius:13px;box-shadow:0 18px 60px rgba(0,0,0,.6);
|
| 19 |
+
display:flex;flex-direction:column;overflow:hidden;backdrop-filter:blur(9px)}
|
| 20 |
+
.pane.on{border-color:rgba(232,192,122,.42)}
|
| 21 |
+
.bar{display:flex;align-items:center;gap:8px;padding:7px 9px;cursor:grab;
|
| 22 |
+
background:linear-gradient(180deg,rgba(255,255,255,.05),rgba(255,255,255,0));
|
| 23 |
+
border-bottom:1px solid var(--line);flex:0 0 auto;touch-action:none}
|
| 24 |
+
.bar b{font-weight:600;font-size:12px;flex:1;white-space:nowrap;overflow:hidden;
|
| 25 |
+
text-overflow:ellipsis}
|
| 26 |
+
.dot{width:11px;height:11px;border-radius:50%;border:0;cursor:pointer;flex:0 0 auto}
|
| 27 |
+
.dot.x{background:#ff6b78}.dot.m{background:#e8c07a}
|
| 28 |
+
.body{flex:1;overflow:auto;padding:10px;font-size:12.5px}
|
| 29 |
+
.grip{position:absolute;right:0;bottom:0;width:18px;height:18px;cursor:nwse-resize;
|
| 30 |
+
background:linear-gradient(135deg,transparent 50%,rgba(255,255,255,.22) 50%);
|
| 31 |
+
touch-action:none}
|
| 32 |
+
.key{width:14px;height:14px;border-radius:4px;flex:0 0 auto;border:1px solid rgba(0,0,0,.4)}
|
| 33 |
+
|
| 34 |
+
/* the sill β where the doors are */
|
| 35 |
+
#sill{position:fixed;left:0;right:0;bottom:0;display:flex;gap:6px;padding:8px 10px;
|
| 36 |
+
background:rgba(8,6,15,.9);border-top:1px solid var(--line);overflow-x:auto;
|
| 37 |
+
align-items:center;z-index:9000;backdrop-filter:blur(12px)}
|
| 38 |
+
#sill button{background:transparent;color:var(--fg);border:1px solid var(--line);
|
| 39 |
+
border-radius:9px;padding:8px 12px;font:600 12px ui-monospace,monospace;
|
| 40 |
+
cursor:pointer;white-space:nowrap;flex:0 0 auto}
|
| 41 |
+
#sill button.lit{border-color:var(--gold);color:var(--gold)}
|
| 42 |
+
#sill .me{margin-left:auto;display:flex;align-items:center;gap:7px;flex:0 0 auto;
|
| 43 |
+
color:var(--dim);font-size:11px;padding-left:10px}
|
| 44 |
+
|
| 45 |
+
/* bits */
|
| 46 |
+
input,textarea,select{width:100%;background:rgba(255,255,255,.04);color:var(--fg);
|
| 47 |
+
border:1px solid var(--line);border-radius:9px;padding:9px;
|
| 48 |
+
font:12.5px ui-monospace,monospace;margin:4px 0}
|
| 49 |
+
textarea{min-height:62px;font-size:11px;line-height:1.45}
|
| 50 |
+
.btn{background:var(--gold);color:#120d18;border:0;border-radius:9px;padding:9px 15px;
|
| 51 |
+
font:700 12px ui-monospace,monospace;cursor:pointer;margin:3px 5px 3px 0}
|
| 52 |
+
.btn.g{background:transparent;color:var(--fg);border:1px solid var(--line)}
|
| 53 |
+
.row{display:flex;justify-content:space-between;gap:10px;padding:1px 0}
|
| 54 |
+
.k{color:var(--dim)}.ok{color:var(--ok)}.no{color:var(--no)}.acc{color:var(--gold)}
|
| 55 |
+
.stream{height:100%;display:flex;flex-direction:column}
|
| 56 |
+
.said{flex:1;overflow:auto;white-space:pre-wrap;word-break:break-word}
|
| 57 |
+
.said div{padding:2px 0}
|
| 58 |
+
.said i{font-style:normal;color:var(--dim)}
|
| 59 |
+
.chip{height:17px;width:44px;border-radius:5px;border:1px solid var(--line);
|
| 60 |
+
display:inline-block;vertical-align:middle;margin:2px 4px 2px 0;cursor:pointer}
|
| 61 |
+
h4{margin:9px 0 5px;font-size:10.5px;letter-spacing:1.4px;text-transform:uppercase;
|
| 62 |
+
color:var(--dim);font-weight:400;border-top:1px solid var(--line);padding-top:8px}
|
| 63 |
+
h4:first-child{border-top:0;padding-top:0;margin-top:0}
|
| 64 |
+
p{color:#c9c0d4;margin:6px 0}b{color:#fff}small{color:var(--dim)}
|
| 65 |
+
canvas.paint{width:100%;height:100%;display:block;background:#050309;
|
| 66 |
+
border-radius:8px;touch-action:none}
|
| 67 |
+
#toast{position:fixed;left:50%;top:14px;transform:translateX(-50%);z-index:9999;
|
| 68 |
+
background:rgba(20,14,34,.95);border:1px solid var(--line);border-radius:10px;
|
| 69 |
+
padding:9px 15px;font-size:12px;opacity:0;transition:opacity .3s;pointer-events:none}
|
| 70 |
+
</style>
|
| 71 |
+
|
| 72 |
+
<canvas id="stars"></canvas>
|
| 73 |
+
<div id="field"></div>
|
| 74 |
+
|
| 75 |
+
<div id="sill">
|
| 76 |
+
<button id="d-open" class="lit">β open</button>
|
| 77 |
+
<button data-door="doors">doors</button>
|
| 78 |
+
<button data-door="people">people</button>
|
| 79 |
+
<button data-door="say">say</button>
|
| 80 |
+
<button data-door="paint">paint</button>
|
| 81 |
+
<button data-door="carry">carry</button>
|
| 82 |
+
<button data-door="play">play</button>
|
| 83 |
+
<button data-door="rooms">rooms</button>
|
| 84 |
+
<button data-door="all">β all</button>
|
| 85 |
+
<button data-door="what">what is this</button>
|
| 86 |
+
<span class="me"><span class="key" id="mykey"></span><span id="myid">β</span>
|
| 87 |
+
<span id="mypeers" class="ok">0</span></span>
|
| 88 |
+
</div>
|
| 89 |
+
<div id="toast"></div>
|
| 90 |
+
|
| 91 |
+
<script>
|
| 92 |
+
/* =========================================================================
|
| 93 |
+
lightDOORs. A door is a label, not a socket β you can open as many as you
|
| 94 |
+
like and it costs nothing, because there was never a socket per door.
|
| 95 |
+
Nothing here is stored on anyone's machine but yours.
|
| 96 |
+
========================================================================= */
|
| 97 |
+
const ICE={iceServers:[{urls:["stun:stun.l.google.com:19302",
|
| 98 |
+
"stun:stun1.l.google.com:19302"]}]};
|
| 99 |
+
const $=s=>document.querySelector(s);
|
| 100 |
+
const peers=new Map(); /* colour -> {pc, ctl, doors:Map} */
|
| 101 |
+
const pending=new Map();
|
| 102 |
+
const seen=new Set();
|
| 103 |
+
const doors=new Map(); /* door name -> [{from,body,t}] */
|
| 104 |
+
const strokes=new Map(); /* door name -> [[x,y,px,py,colour]] */
|
| 105 |
+
const carried=[]; /* things that arrived */
|
| 106 |
+
let MYID=null, bootPc=null, isHost=false, z=10;
|
| 107 |
+
|
| 108 |
+
let held=new Set();
|
| 109 |
+
try{held=new Set(JSON.parse(localStorage.getItem("ld.colours")||"[]"))}catch(e){}
|
| 110 |
+
let myDoors=[];
|
| 111 |
+
try{myDoors=JSON.parse(localStorage.getItem("ld.doors")||'["light"]')}catch(e){myDoors=["light"]}
|
| 112 |
+
if(!myDoors.length) myDoors=["light"];
|
| 113 |
+
const saveDoors=()=>{try{localStorage.setItem("ld.doors",JSON.stringify(myDoors))}catch(e){}};
|
| 114 |
+
|
| 115 |
+
/* ---- colour is the address; the address is the content ---- */
|
| 116 |
+
async function sha(s){
|
| 117 |
+
const d=await crypto.subtle.digest("SHA-256",new TextEncoder().encode(s));
|
| 118 |
+
return [...new Uint8Array(d)].map(x=>x.toString(16).padStart(2,"0")).join("");
|
| 119 |
+
}
|
| 120 |
+
async function shaBytes(u){
|
| 121 |
+
const d=await crypto.subtle.digest("SHA-256",u);
|
| 122 |
+
return [...new Uint8Array(d)].map(x=>x.toString(16).padStart(2,"0")).join("");
|
| 123 |
+
}
|
| 124 |
+
/* THE QUANT. Same law as the shipped engine, verified byte-identical on 40
|
| 125 |
+
cases: JL count-sketch into 1024 buckets, then the fixed 3,200-byte tail of
|
| 126 |
+
turbo(1024) + signs(128) + zeta(1024) + hist(1024). O(N) in, O(1) out. */
|
| 127 |
+
function quant(bytes){
|
| 128 |
+
const D=1024, pad=(8-(bytes.length%8))%8;
|
| 129 |
+
const b=new Uint8Array(bytes.length+pad); b.set(bytes);
|
| 130 |
+
const f=new Float64Array(b.buffer), proj=new Float64Array(D);
|
| 131 |
+
for(let i=0;i<f.length;i++){
|
| 132 |
+
const h=(Math.imul(i,2654435761))>>>0, v=f[i];
|
| 133 |
+
if(!Number.isFinite(v)) continue;
|
| 134 |
+
proj[h&(D-1)] += (h&0x80000000)? -v : v;
|
| 135 |
+
}
|
| 136 |
+
let max=1e-12;
|
| 137 |
+
for(let j=0;j<D;j++){const a=Math.abs(proj[j]); if(a>max)max=a;}
|
| 138 |
+
const turbo=new Int8Array(D), signs=new Uint8Array(D>>3),
|
| 139 |
+
zeta=new Uint8Array(D), hist=new Uint32Array(256);
|
| 140 |
+
for(let j=0;j<D;j++){
|
| 141 |
+
const v=proj[j]/max, q=Math.round(v*127), a=Math.abs(v);
|
| 142 |
+
turbo[j]=q;
|
| 143 |
+
if(v<0) signs[j>>3] |= (1<<(j&7));
|
| 144 |
+
zeta[j]= a<1e-9 ? 15 : Math.min(15, -Math.log2(a)|0);
|
| 145 |
+
hist[(q+128)&255]+=1;
|
| 146 |
+
}
|
| 147 |
+
const out=new Uint8Array(3200);
|
| 148 |
+
out.set(new Uint8Array(turbo.buffer),0); out.set(signs,1024);
|
| 149 |
+
out.set(zeta,1152); out.set(new Uint8Array(hist.buffer),2176);
|
| 150 |
+
return out;
|
| 151 |
+
}
|
| 152 |
+
function playIn(ch){
|
| 153 |
+
ch.binaryType="arraybuffer";
|
| 154 |
+
ch.onmessage=async e=>{
|
| 155 |
+
if(typeof e.data==="string") return;
|
| 156 |
+
const t=new Uint8Array(e.data);
|
| 157 |
+
if(t.length!==3200){ toast("not a tuple ("+t.length+" B)"); return; }
|
| 158 |
+
const h=await shaBytes(t); absorb(h.slice(0,16));
|
| 159 |
+
const p=pane("play","play",DOORS.play,420,380);
|
| 160 |
+
if(p._show){ p._show(t); p._t=t;
|
| 161 |
+
p.querySelector("#pin").textContent="played to you";
|
| 162 |
+
p.querySelector("#pout").textContent="3,200 B";
|
| 163 |
+
p.querySelector("#psha").textContent=h.slice(0,16); }
|
| 164 |
+
toast("played Β· "+h.slice(0,10));
|
| 165 |
+
};
|
| 166 |
+
}
|
| 167 |
+
const cl=(v,a,b)=>Math.max(a,Math.min(b,v));
|
| 168 |
+
function stops(h){
|
| 169 |
+
h=(h||"")+"000000000000";
|
| 170 |
+
const p=i=>cl(parseInt(h.substr(i,2),16)||16,16,240);
|
| 171 |
+
const lum=c=>0.2126*c[0]+0.7152*c[1]+0.0722*c[2];
|
| 172 |
+
const fix=c=>{const L=lum(c);return (L>=32&&L<=224)?c:
|
| 173 |
+
c.map(x=>Math.round(cl(x*128/Math.max(1,L),0,255)))};
|
| 174 |
+
return [fix([p(0),p(2),p(4)]),fix([p(6),p(8),p(10)])];
|
| 175 |
+
}
|
| 176 |
+
const grad=h=>{const[a,b]=stops(h);
|
| 177 |
+
return "linear-gradient(90deg,rgb("+a+"),rgb("+b+"))"};
|
| 178 |
+
const ink=h=>"rgb("+stops(h)[1]+")";
|
| 179 |
+
const absorb=c=>{ if(!c||c===MYID||held.has(c)) return;
|
| 180 |
+
held.add(c); try{localStorage.setItem("ld.colours",JSON.stringify([...held]))}catch(e){}
|
| 181 |
+
refresh("people"); };
|
| 182 |
+
|
| 183 |
+
const toast=t=>{const e=$("#toast");e.textContent=t;e.style.opacity=1;
|
| 184 |
+
clearTimeout(e._t);e._t=setTimeout(()=>e.style.opacity=0,2300)};
|
| 185 |
+
|
| 186 |
+
/* =========================== panes ====================================== */
|
| 187 |
+
const open=new Map();
|
| 188 |
+
function pane(id,title,build,w,h){
|
| 189 |
+
if(open.has(id)){ const p=open.get(id); p.style.zIndex=++z; lit(); return p; }
|
| 190 |
+
const p=document.createElement("div"); p.className="pane"; p.style.zIndex=++z;
|
| 191 |
+
const n=open.size, vw=innerWidth, vh=innerHeight;
|
| 192 |
+
w=Math.min(w||330,vw-24); h=Math.min(h||300,vh-110);
|
| 193 |
+
p.style.left=Math.min(vw-w-12,18+n*26)+"px";
|
| 194 |
+
p.style.top =Math.min(vh-h-70,18+n*24)+"px";
|
| 195 |
+
p.style.width=w+"px"; p.style.height=h+"px";
|
| 196 |
+
p.innerHTML='<div class=bar><b></b>'+
|
| 197 |
+
'<button class="dot m"></button><button class="dot x"></button></div>'+
|
| 198 |
+
'<div class=body></div><div class=grip></div>';
|
| 199 |
+
p.querySelector("b").textContent=title;
|
| 200 |
+
build(p.querySelector(".body"),p);
|
| 201 |
+
p.onpointerdown=()=>{p.style.zIndex=++z;lit()};
|
| 202 |
+
drag(p.querySelector(".bar"),p); resize(p.querySelector(".grip"),p);
|
| 203 |
+
p.querySelector(".dot.x").onclick=e=>{e.stopPropagation();p.remove();open.delete(id);lit()};
|
| 204 |
+
p.querySelector(".dot.m").onclick=e=>{e.stopPropagation();
|
| 205 |
+
const b=p.querySelector(".body");
|
| 206 |
+
const min=b.style.display!=="none";
|
| 207 |
+
b.style.display=min?"none":"";p.querySelector(".grip").style.display=min?"none":"";
|
| 208 |
+
p._h=p._h||p.style.height; p.style.height=min?"auto":p._h};
|
| 209 |
+
$("#field").appendChild(p); open.set(id,p); lit();
|
| 210 |
+
return p;
|
| 211 |
+
}
|
| 212 |
+
function lit(){
|
| 213 |
+
document.querySelectorAll("#sill button[data-door]").forEach(b=>
|
| 214 |
+
b.classList.toggle("lit",open.has(b.dataset.door)));
|
| 215 |
+
}
|
| 216 |
+
function drag(h,p){
|
| 217 |
+
h.onpointerdown=e=>{ if(e.target.classList.contains("dot"))return;
|
| 218 |
+
h.setPointerCapture(e.pointerId); p.style.zIndex=++z;
|
| 219 |
+
const sx=e.clientX-p.offsetLeft, sy=e.clientY-p.offsetTop;
|
| 220 |
+
h.onpointermove=m=>{
|
| 221 |
+
p.style.left=cl(m.clientX-sx,-40,innerWidth-70)+"px";
|
| 222 |
+
p.style.top =cl(m.clientY-sy,0,innerHeight-70)+"px"; };
|
| 223 |
+
h.onpointerup=()=>{h.onpointermove=null;h.onpointerup=null}; };
|
| 224 |
+
}
|
| 225 |
+
function resize(g,p){
|
| 226 |
+
g.onpointerdown=e=>{ e.stopPropagation(); g.setPointerCapture(e.pointerId);
|
| 227 |
+
const sw=p.offsetWidth-e.clientX, sh=p.offsetHeight-e.clientY;
|
| 228 |
+
g.onpointermove=m=>{ p.style.width=Math.max(230,sw+m.clientX)+"px";
|
| 229 |
+
p.style.height=Math.max(150,sh+m.clientY)+"px"; p._h=p.style.height;
|
| 230 |
+
if(p._fit)p._fit(); };
|
| 231 |
+
g.onpointerup=()=>{g.onpointermove=null;g.onpointerup=null}; };
|
| 232 |
+
}
|
| 233 |
+
const refresh=id=>{ const p=open.get(id); if(p&&p._draw) p._draw(); };
|
| 234 |
+
|
| 235 |
+
/* =========================== the doors ================================== */
|
| 236 |
+
function doorPane(name){
|
| 237 |
+
pane("door:"+name,"door Β· "+name,(b,p)=>{
|
| 238 |
+
b.innerHTML='<div class=stream><div class=said></div>'+
|
| 239 |
+
'<input placeholder="say it into this door"></div>';
|
| 240 |
+
b.style.padding="8px";
|
| 241 |
+
const said=b.querySelector(".said"), inp=b.querySelector("input");
|
| 242 |
+
p._draw=()=>{
|
| 243 |
+
said.innerHTML=(doors.get(name)||[]).slice(-300).map(m=>
|
| 244 |
+
'<div><span class=key style="background:'+grad(m.from)+
|
| 245 |
+
'"></span> <i>'+(m.from===MYID?"you":m.from.slice(0,6))+'</i> '+
|
| 246 |
+
esc(m.body)+'</div>').join("");
|
| 247 |
+
said.scrollTop=said.scrollHeight; };
|
| 248 |
+
inp.onkeydown=e=>{ if(e.key!=="Enter")return;
|
| 249 |
+
const v=inp.value.trim(); if(!v)return; inp.value="";
|
| 250 |
+
speak(name,v); };
|
| 251 |
+
p._draw();
|
| 252 |
+
},380,300);
|
| 253 |
+
}
|
| 254 |
+
const esc=s=>s.replace(/[&<>]/g,c=>({"&":"&","<":"<",">":">"}[c]));
|
| 255 |
+
|
| 256 |
+
const DOORS={
|
| 257 |
+
doors:(b,p)=>{
|
| 258 |
+
p._draw=()=>{
|
| 259 |
+
b.innerHTML="<h4>your doors β a door is a label, not a socket</h4>"+
|
| 260 |
+
myDoors.map(d=>'<button class="btn g" data-d="'+d+'">'+d+'</button>').join("")+
|
| 261 |
+
'<h4>cut a new one</h4><input id=nd placeholder="name it β anything">'+
|
| 262 |
+
'<button class="btn" id=cut>cut the door</button>'+
|
| 263 |
+
'<p><small>Opening one costs nothing. There was never a socket per door β '+
|
| 264 |
+
'the name <b>is</b> the route, so the count has no ceiling.</small></p>';
|
| 265 |
+
b.querySelectorAll("[data-d]").forEach(x=>x.onclick=()=>doorPane(x.dataset.d));
|
| 266 |
+
b.querySelector("#cut").onclick=()=>{
|
| 267 |
+
const v=b.querySelector("#nd").value.trim().toLowerCase().replace(/\s+/g,"-");
|
| 268 |
+
if(!v)return; if(!myDoors.includes(v)){myDoors.push(v);saveDoors()}
|
| 269 |
+
p._draw(); doorPane(v); openDoorEverywhere(v); };
|
| 270 |
+
};
|
| 271 |
+
p._draw();
|
| 272 |
+
},
|
| 273 |
+
people:(b,p)=>{
|
| 274 |
+
p._draw=()=>{
|
| 275 |
+
const live=[...peers.keys()];
|
| 276 |
+
b.innerHTML="<h4>here now</h4>"+
|
| 277 |
+
(live.length?live.map(c=>'<span class=chip title="'+c+'" style="background:'+
|
| 278 |
+
grad(c)+'"></span>').join(""):"<small>nobody yet</small>")+
|
| 279 |
+
"<h4>colours you carry β "+held.size+"</h4>"+
|
| 280 |
+
(held.size?[...held].map(c=>'<span class=chip title="'+c+'" style="background:'+
|
| 281 |
+
grad(c)+'"></span>').join(""):"<small>none yet</small>")+
|
| 282 |
+
"<p><small>The colour outlives the link. Nothing here is a contact list β "+
|
| 283 |
+
"there is no entry to delete, because there was never an entry.</small></p>";
|
| 284 |
+
};
|
| 285 |
+
p._draw();
|
| 286 |
+
},
|
| 287 |
+
say:(b,p)=>{ b.innerHTML="";
|
| 288 |
+
const sel=document.createElement("select");
|
| 289 |
+
const redraw=()=>{ sel.innerHTML=myDoors.map(d=>"<option>"+d+"</option>").join("") };
|
| 290 |
+
redraw(); b.appendChild(sel);
|
| 291 |
+
const s=document.createElement("div"); s.className="said";
|
| 292 |
+
s.style.cssText="height:calc(100% - 92px);overflow:auto"; b.appendChild(s);
|
| 293 |
+
const i=document.createElement("input"); i.placeholder="say it"; b.appendChild(i);
|
| 294 |
+
p._draw=()=>{ redraw();
|
| 295 |
+
s.innerHTML=(doors.get(sel.value)||[]).slice(-300).map(m=>
|
| 296 |
+
'<div><span class=key style="background:'+grad(m.from)+'"></span> <i>'+
|
| 297 |
+
(m.from===MYID?"you":m.from.slice(0,6))+'</i> '+esc(m.body)+'</div>').join("");
|
| 298 |
+
s.scrollTop=s.scrollHeight; };
|
| 299 |
+
sel.onchange=p._draw;
|
| 300 |
+
i.onkeydown=e=>{ if(e.key!=="Enter")return; const v=i.value.trim();
|
| 301 |
+
if(!v)return; i.value=""; speak(sel.value,v); };
|
| 302 |
+
p._draw();
|
| 303 |
+
},
|
| 304 |
+
paint:(b,p)=>{
|
| 305 |
+
b.style.padding="6px"; b.innerHTML='<canvas class=paint></canvas>';
|
| 306 |
+
const c=b.querySelector("canvas"), g=c.getContext("2d");
|
| 307 |
+
let last=null;
|
| 308 |
+
p._fit=()=>{ const r=c.getBoundingClientRect();
|
| 309 |
+
c.width=Math.max(2,r.width*2); c.height=Math.max(2,r.height*2); p._draw(); };
|
| 310 |
+
p._draw=()=>{ g.fillStyle="#050309"; g.fillRect(0,0,c.width,c.height);
|
| 311 |
+
g.lineCap="round";
|
| 312 |
+
for(const s of (strokes.get("light")||[])){
|
| 313 |
+
g.strokeStyle=ink(s[4]); g.lineWidth=5;
|
| 314 |
+
g.beginPath(); g.moveTo(s[2]*c.width,s[3]*c.height);
|
| 315 |
+
g.lineTo(s[0]*c.width,s[1]*c.height); g.stroke(); } };
|
| 316 |
+
const at=e=>{const r=c.getBoundingClientRect();
|
| 317 |
+
return [(e.clientX-r.left)/r.width,(e.clientY-r.top)/r.height]};
|
| 318 |
+
c.onpointerdown=e=>{ c.setPointerCapture(e.pointerId); last=at(e); };
|
| 319 |
+
c.onpointermove=e=>{ if(!last)return; const n=at(e);
|
| 320 |
+
addStroke("light",[n[0],n[1],last[0],last[1],MYID]);
|
| 321 |
+
gossip("light","STROKE|from="+MYID+"|s="+
|
| 322 |
+
[n[0],n[1],last[0],last[1]].map(v=>v.toFixed(4)).join(",")+"|json=0");
|
| 323 |
+
last=n; };
|
| 324 |
+
c.onpointerup=()=>last=null;
|
| 325 |
+
setTimeout(p._fit,60);
|
| 326 |
+
},
|
| 327 |
+
carry:(b,p)=>{
|
| 328 |
+
p._draw=()=>{
|
| 329 |
+
b.innerHTML="<h4>hand something over</h4>"+
|
| 330 |
+
'<input type=file id=f multiple>'+
|
| 331 |
+
"<h4>what arrived β "+carried.length+"</h4>"+
|
| 332 |
+
(carried.length?carried.map((c,i)=>
|
| 333 |
+
'<div class=row><span>'+esc(c.name)+'</span>'+
|
| 334 |
+
'<a class=acc download="'+esc(c.name)+'" href="'+c.url+'">keep it</a></div>'
|
| 335 |
+
).join(""):"<small>nothing yet</small>")+
|
| 336 |
+
"<p><small>It goes down the same line as the words. Nobody hosts it, "+
|
| 337 |
+
"nobody stores it, and it never leaves the two machines.</small></p>";
|
| 338 |
+
b.querySelector("#f").onchange=e=>{ for(const f of e.target.files) handOver(f); };
|
| 339 |
+
};
|
| 340 |
+
p._draw();
|
| 341 |
+
},
|
| 342 |
+
rooms:(b,p)=>{
|
| 343 |
+
b.style.padding="8px";
|
| 344 |
+
b.innerHTML="<h4>a private room only people who know the word can enter</h4>"+
|
| 345 |
+
'<input id=rn placeholder="a word — or a whole phrase" value="light">'+
|
| 346 |
+
'<canvas id=rc style="width:100%;height:120px;display:block;'+
|
| 347 |
+
'border:1px solid var(--line);border-radius:8px;margin:6px 0"></canvas>'+
|
| 348 |
+
'<div class=row><span class=k>this room's address</span><span id=ra class=acc>—</span></div>'+
|
| 349 |
+
'<button class="btn" id=renter>enter this room</button>'+
|
| 350 |
+
'<button class="btn g" id=rgo>just look at it</button>'+
|
| 351 |
+
"<p><small>The room's address is <b>sha256 of your word</b>. Type the same "+
|
| 352 |
+
"word as someone else and you land in the <b>same room</b> — talk, draw, "+
|
| 353 |
+
"hand files over. Type a different word and you are somewhere else entirely. "+
|
| 354 |
+
"<b>Nobody who doesn't know the word can find it</b>: there is no list of "+
|
| 355 |
+
"rooms to browse, and the address gives the word away no more than a lock gives "+
|
| 356 |
+
"away its key. No server, no account, no directory.</small></p>"+
|
| 357 |
+
"<p><small>Same law as the registration office — a room 10^30 places along "+
|
| 358 |
+
"is reached as fast as the first, because there is no table to walk.</small></p>";
|
| 359 |
+
const CUBE="b40b3e1726b0e81d";
|
| 360 |
+
const c=b.querySelector("#rc"), g=c.getContext("2d");
|
| 361 |
+
p._fit=()=>{const r=c.getBoundingClientRect();
|
| 362 |
+
c.width=Math.max(2,r.width*2); c.height=Math.max(2,r.height*2); if(p._t)show(p._t);};
|
| 363 |
+
function show(t){ p._t=t;
|
| 364 |
+
const W=c.width,H=c.height,N=32,cw=W/N,ch=H/N;
|
| 365 |
+
g.fillStyle="#050309"; g.fillRect(0,0,W,H);
|
| 366 |
+
for(let i=0;i<1024;i++){
|
| 367 |
+
const R=Math.min(255,Math.abs(t[i]<<24>>24)*2), G=(15-t[1152+i])*17, B=t[2176+i];
|
| 368 |
+
const neg=(t[1024+(i>>3)]>>(i&7))&1;
|
| 369 |
+
g.fillStyle="rgb("+(neg?B:R)+","+G+","+(neg?R:B)+")";
|
| 370 |
+
g.fillRect((i%N)*cw,((i/N)|0)*ch,cw+1,ch+1);
|
| 371 |
+
}
|
| 372 |
+
}
|
| 373 |
+
async function addrOf(word){ return await sha(CUBE+"|"+word); }
|
| 374 |
+
async function look(word){
|
| 375 |
+
const full=await addrOf(word);
|
| 376 |
+
b.querySelector("#ra").textContent=full.slice(0,16);
|
| 377 |
+
show(quant(new TextEncoder().encode(full)));
|
| 378 |
+
return full;
|
| 379 |
+
}
|
| 380 |
+
async function enter(word){
|
| 381 |
+
if(!word){ toast("type a word first"); return; }
|
| 382 |
+
const full=await look(word);
|
| 383 |
+
const room="r."+full.slice(0,12); /* the door IS the address */
|
| 384 |
+
if(!myDoors.includes(room)){ myDoors.push(room); saveDoors(); }
|
| 385 |
+
if(!doors.has(room)) doors.set(room,[]);
|
| 386 |
+
doorPane(room); /* live, synced via the mesh */
|
| 387 |
+
openDoorEverywhere(room);
|
| 388 |
+
absorb(full.slice(0,16));
|
| 389 |
+
toast(peers.size ? "entered — "+peers.size+" boat(s) can meet you here"
|
| 390 |
+
: "entered — give this word to who you want here");
|
| 391 |
+
}
|
| 392 |
+
b.querySelector("#renter").onclick=()=>enter((b.querySelector("#rn").value||"").trim());
|
| 393 |
+
b.querySelector("#rgo").onclick=()=>look((b.querySelector("#rn").value||"light").trim());
|
| 394 |
+
b.querySelector("#rn").onkeydown=e=>{ if(e.key==="Enter") enter((e.target.value||"").trim()); };
|
| 395 |
+
setTimeout(()=>{p._fit(); look("light");},60);
|
| 396 |
+
},
|
| 397 |
+
play:(b,p)=>{
|
| 398 |
+
b.style.padding="8px";
|
| 399 |
+
b.innerHTML="<h4>hand anything in β it leaves as 3.1 KB</h4>"+
|
| 400 |
+
'<input type=file id=pf><input id=pt placeholder="or type something">'+
|
| 401 |
+
'<canvas id=pc style="width:100%;height:150px;display:block;'+
|
| 402 |
+
'border:1px solid var(--line);border-radius:8px;margin:6px 0"></canvas>'+
|
| 403 |
+
'<div class=row><span class=k>in</span><span id=pin>β</span></div>'+
|
| 404 |
+
'<div class=row><span class=k>out</span><span class=acc id=pout>β</span></div>'+
|
| 405 |
+
'<div class=row><span class=k>colour</span><span id=psha class=acc>β</span></div>'+
|
| 406 |
+
'<button class="btn" id=psend>play it to everyone</button>'+
|
| 407 |
+
"<p><small>The tuple is <b>3,200 bytes whatever you hand it</b> β the "+
|
| 408 |
+
"same four bands the engine writes. What crosses is the light, "+
|
| 409 |
+
"<b>not the file</b>. Nothing is saved on either side.</small></p>";
|
| 410 |
+
const c=b.querySelector("#pc"), g=c.getContext("2d");
|
| 411 |
+
p._fit=()=>{const r=c.getBoundingClientRect();
|
| 412 |
+
c.width=Math.max(2,r.width*2); c.height=Math.max(2,r.height*2); if(p._last)show(p._last)};
|
| 413 |
+
function show(t){
|
| 414 |
+
p._last=t;
|
| 415 |
+
const W=c.width,H=c.height,N=32,cw=W/N,ch=H/N;
|
| 416 |
+
g.fillStyle="#050309"; g.fillRect(0,0,W,H);
|
| 417 |
+
for(let i=0;i<1024;i++){
|
| 418 |
+
const R=Math.min(255,Math.abs(t[i]<<24>>24)*2); /* turbo, signed */
|
| 419 |
+
const G=(15-t[1152+i])*17; /* zeta */
|
| 420 |
+
const B=t[2176+i]; /* hist bytes */
|
| 421 |
+
const neg=(t[1024+(i>>3)]>>(i&7))&1; /* signs = the anti */
|
| 422 |
+
g.fillStyle="rgb("+(neg?B:R)+","+G+","+(neg?R:B)+")";
|
| 423 |
+
g.fillRect((i%N)*cw,((i/N)|0)*ch,cw+1,ch+1);
|
| 424 |
+
}
|
| 425 |
+
}
|
| 426 |
+
p._show=show;
|
| 427 |
+
async function take(bytes,label){
|
| 428 |
+
const t=quant(bytes);
|
| 429 |
+
const h=await shaBytes(t);
|
| 430 |
+
b.querySelector("#pin").textContent=label+" Β· "+bytes.length.toLocaleString()+" B";
|
| 431 |
+
b.querySelector("#pout").textContent=t.length.toLocaleString()+" B";
|
| 432 |
+
b.querySelector("#psha").textContent=h.slice(0,16);
|
| 433 |
+
show(t); p._t=t; absorb(h.slice(0,16));
|
| 434 |
+
}
|
| 435 |
+
b.querySelector("#pf").onchange=async e=>{ const f=e.target.files[0]; if(!f)return;
|
| 436 |
+
take(new Uint8Array(await f.arrayBuffer()), f.name); };
|
| 437 |
+
b.querySelector("#pt").onkeydown=e=>{ if(e.key!=="Enter")return;
|
| 438 |
+
take(new TextEncoder().encode(e.target.value), "typed"); };
|
| 439 |
+
b.querySelector("#psend").onclick=()=>{
|
| 440 |
+
if(!p._t){ toast("hand something in first"); return; }
|
| 441 |
+
if(!peers.size){ toast("nobody to play it to yet"); return; }
|
| 442 |
+
for(const [,rec] of peers){
|
| 443 |
+
const ch=rec.pc.createDataChannel("play:"+Date.now());
|
| 444 |
+
ch.binaryType="arraybuffer";
|
| 445 |
+
ch.onopen=()=>{ ch.send(p._t.buffer.slice(0)); setTimeout(()=>ch.close(),300); };
|
| 446 |
+
}
|
| 447 |
+
toast("played Β· 3,200 bytes to "+peers.size);
|
| 448 |
+
};
|
| 449 |
+
setTimeout(p._fit,60);
|
| 450 |
+
},
|
| 451 |
+
all:(b,p)=>{
|
| 452 |
+
const go=u=>"onclick=\"window.open('"+u+"','_blank','noopener')\"";
|
| 453 |
+
b.innerHTML=
|
| 454 |
+
"<h4>every light, one door</h4>"+
|
| 455 |
+
"<p>This hub is one room. <b>The bridge is the door to all of them</b> β "+
|
| 456 |
+
"every public light on the account in one place, so none of it gets lost "+
|
| 457 |
+
"like Tesla.</p>"+
|
| 458 |
+
'<button class="btn" '+go("https://jessebrown1980.github.io/the-bridge-to-all-of-it/")+
|
| 459 |
+
'>open the bridge → all 182</button>'+
|
| 460 |
+
"<h4>the nearest few</h4>"+
|
| 461 |
+
'<button class="btn g" '+go("https://jessebrown1980.github.io/lightDOORwsMs/")+'>this hub</button>'+
|
| 462 |
+
'<button class="btn g" '+go("https://jessebrown1980.github.io/the-light-boat/")+'>the light boat</button>'+
|
| 463 |
+
'<button class="btn g" '+go("https://jessebrown1980.github.io/Be-infinitly-express-spherically-free/")+'>the sphere</button>'+
|
| 464 |
+
'<button class="btn g" '+go("https://jessebrown1980.github.io/the-null-space-or-NO-jo0O/")+'>the null space</button>'+
|
| 465 |
+
'<button class="btn g" '+go("https://jessebrown1980.github.io/the-zebras-z-is-the-squared-wave/")+'>the zebra</button>'+
|
| 466 |
+
"<p><small>Photons are the same photon with different colours and oils; "+
|
| 467 |
+
"these are the same light with different doors. The bridge holds them all, "+
|
| 468 |
+
"and the bridge points back here — so from any one you can reach every "+
|
| 469 |
+
"other.</small></p>";
|
| 470 |
+
},
|
| 471 |
+
what:(b)=>{ b.innerHTML=
|
| 472 |
+
"<h4>a door is a label, not a socket</h4>"+
|
| 473 |
+
"<p>Cut as many as you like. The name <b>is</b> the route, so there is no "+
|
| 474 |
+
"count to run out of β that ceiling only exists where somebody hands out "+
|
| 475 |
+
"numbered slots.</p>"+
|
| 476 |
+
"<h4>the colour is the address</h4>"+
|
| 477 |
+
"<p>Your colour is the sha-256 of a secret this browser made for itself the "+
|
| 478 |
+
"first time you opened the page β two brown stops, banded 16β¦240, "+
|
| 479 |
+
"luminance-clamped 32β¦224. <b>The secret never leaves; only the colour does.</b> "+
|
| 480 |
+
"Nobody handed you the name, so nobody can take it away, and it does not "+
|
| 481 |
+
"change when a link does.</p>"+
|
| 482 |
+
"<h4>the trace is spent, the colour is kept</h4>"+
|
| 483 |
+
"<p>The block you pass at the first meeting is a <b>one-time introduction</b> "+
|
| 484 |
+
"and this page throws it away on contact. What you keep is the colour. Break "+
|
| 485 |
+
"every link at once and it reassembles, because what survived was never the "+
|
| 486 |
+
"wiring.</p>"+
|
| 487 |
+
"<h4>what is free, and the one thing that isn't</h4>"+
|
| 488 |
+
"<p>No server of ours touches any of it β words, drawings, files, all of it "+
|
| 489 |
+
"goes browser to browser with nothing in the middle to store or relay. The one "+
|
| 490 |
+
"thing that isn't free, stated rather than hidden: to find a route across the "+
|
| 491 |
+
"internet, browsers ask a public <b>STUN</b> server for their own outside "+
|
| 492 |
+
"address. Free ones. <b>They see that two addresses asked; they never see a "+
|
| 493 |
+
"word of what you say.</b> On the same wifi β or down a USB cable β it doesn't "+
|
| 494 |
+
"need them at all.</p>"+
|
| 495 |
+
"<h4>the first meeting is still by hand</h4>"+
|
| 496 |
+
"<p>Everything after it travels the links that already exist: you are "+
|
| 497 |
+
"introduced by somebody who already knows you both. That is the only "+
|
| 498 |
+
"directory there is, and it is not a list anyone can seize.</p>" }
|
| 499 |
+
};
|
| 500 |
+
|
| 501 |
+
/* =========================== traffic ==================================== */
|
| 502 |
+
function addStroke(door,s){
|
| 503 |
+
if(!strokes.has(door)) strokes.set(door,[]);
|
| 504 |
+
const a=strokes.get(door); a.push(s); if(a.length>4000)a.shift();
|
| 505 |
+
refresh("paint");
|
| 506 |
+
}
|
| 507 |
+
function record(door,from,body){
|
| 508 |
+
if(!doors.has(door)) doors.set(door,[]);
|
| 509 |
+
doors.get(door).push({from,body});
|
| 510 |
+
refresh("door:"+door); refresh("say");
|
| 511 |
+
}
|
| 512 |
+
async function mid(){ return (await sha(MYID+Math.random()+performance.now())).slice(0,10) }
|
| 513 |
+
async function speak(door,text){
|
| 514 |
+
const id=await mid(); seen.add(id);
|
| 515 |
+
record(door,MYID,text);
|
| 516 |
+
fan(door,"SAY|from="+MYID+"|mid="+id+"|body="+text+"|json=0");
|
| 517 |
+
}
|
| 518 |
+
async function gossip(door,row){
|
| 519 |
+
const id=await mid(); seen.add(id);
|
| 520 |
+
fan(door,row.replace("|json=0","|mid="+id+"|json=0"));
|
| 521 |
+
}
|
| 522 |
+
function fan(door,row,except){
|
| 523 |
+
for(const [pid,rec] of peers){ if(pid===except) continue;
|
| 524 |
+
let ch=rec.doors.get(door);
|
| 525 |
+
if(!ch){ ch=rec.pc.createDataChannel("door:"+door);
|
| 526 |
+
ch.onmessage=e=>onDoor(pid,door,e.data); rec.doors.set(door,ch); }
|
| 527 |
+
try{ if(ch.readyState==="open") ch.send(row) }catch(e){}
|
| 528 |
+
}
|
| 529 |
+
}
|
| 530 |
+
function openDoorEverywhere(door){ fan(door,"OPEN|from="+MYID+"|json=0") }
|
| 531 |
+
function onDoor(from,door,row){
|
| 532 |
+
const f={}; row.split("|").forEach(kv=>{const i=kv.indexOf("=");
|
| 533 |
+
if(i>0) f[kv.slice(0,i)]=kv.slice(i+1)});
|
| 534 |
+
if(f.mid){ if(seen.has(f.mid)) return; seen.add(f.mid); }
|
| 535 |
+
absorb(f.from);
|
| 536 |
+
const tag=row.split("|")[0];
|
| 537 |
+
if(tag==="SAY"){ record(door,f.from,f.body);
|
| 538 |
+
if(!open.has("door:"+door)&&!open.has("say")) toast(door+" Β· "+f.body.slice(0,44)); }
|
| 539 |
+
else if(tag==="STROKE"){ const n=f.s.split(",").map(Number);
|
| 540 |
+
addStroke(door,[n[0],n[1],n[2],n[3],f.from]); }
|
| 541 |
+
if(!myDoors.includes(door)){ myDoors.push(door); saveDoors(); refresh("doors"); }
|
| 542 |
+
fan(door,row,from);
|
| 543 |
+
}
|
| 544 |
+
|
| 545 |
+
/* ---- handing a thing over: its own channel, closed when done ---- */
|
| 546 |
+
async function handOver(file){
|
| 547 |
+
if(!peers.size){ toast("nobody to hand it to yet"); return }
|
| 548 |
+
const fid=(await sha(file.name+file.size+Math.random())).slice(0,10);
|
| 549 |
+
for(const [pid,rec] of peers){
|
| 550 |
+
const ch=rec.pc.createDataChannel("give:"+fid);
|
| 551 |
+
ch.binaryType="arraybuffer";
|
| 552 |
+
ch.onopen=async()=>{
|
| 553 |
+
ch.send("GIVE|name="+file.name+"|size="+file.size+"|json=0");
|
| 554 |
+
const buf=await file.arrayBuffer(); const CH=16384;
|
| 555 |
+
for(let o=0;o<buf.byteLength;o+=CH){
|
| 556 |
+
while(ch.bufferedAmount>1<<20) await new Promise(r=>setTimeout(r,25));
|
| 557 |
+
ch.send(buf.slice(o,o+CH));
|
| 558 |
+
}
|
| 559 |
+
ch.send("DONE"); setTimeout(()=>ch.close(),400);
|
| 560 |
+
};
|
| 561 |
+
}
|
| 562 |
+
toast("handed over Β· "+file.name);
|
| 563 |
+
}
|
| 564 |
+
function takeGift(ch){
|
| 565 |
+
ch.binaryType="arraybuffer";
|
| 566 |
+
let name="a thing", parts=[];
|
| 567 |
+
ch.onmessage=e=>{
|
| 568 |
+
if(typeof e.data==="string"){
|
| 569 |
+
if(e.data.startsWith("GIVE|")){
|
| 570 |
+
name=(e.data.match(/name=([^|]*)/)||[])[1]||name; return; }
|
| 571 |
+
if(e.data==="DONE"){
|
| 572 |
+
const url=URL.createObjectURL(new Blob(parts));
|
| 573 |
+
carried.push({name,url}); refresh("carry");
|
| 574 |
+
toast("arrived Β· "+name); parts=[]; }
|
| 575 |
+
return;
|
| 576 |
+
}
|
| 577 |
+
parts.push(e.data);
|
| 578 |
+
};
|
| 579 |
+
}
|
| 580 |
+
|
| 581 |
+
/* =========================== the mesh =================================== */
|
| 582 |
+
function reduce(d){
|
| 583 |
+
const s=d.sdp, g=re=>{const m=s.match(re);return m?m[1]:""};
|
| 584 |
+
const c=[...s.matchAll(/a=candidate:(\S+ \d+ \S+ \d+ \S+ \d+ typ \S+)/g)]
|
| 585 |
+
.map(m=>m[1].replace(/ generation.*$/,""));
|
| 586 |
+
return [d.type==="offer"?"O":"A",g(/a=ice-ufrag:(\S+)/),g(/a=ice-pwd:(\S+)/),
|
| 587 |
+
g(/a=fingerprint:sha-256 (\S+)/).replace(/:/g,""),g(/a=setup:(\S+)/),
|
| 588 |
+
c.join("~")].join("|");
|
| 589 |
+
}
|
| 590 |
+
function expand(str){
|
| 591 |
+
const [t,uf,pw,fp,su,c]=str.trim().split("|");
|
| 592 |
+
const cands=(c||"").split("~").filter(Boolean)
|
| 593 |
+
.map(x=>"a=candidate:"+x+" generation 0").join("\r\n");
|
| 594 |
+
return {type:t==="O"?"offer":"answer",sdp:
|
| 595 |
+
"v=0\r\no=- 1 2 IN IP4 127.0.0.1\r\ns=-\r\nt=0 0\r\na=group:BUNDLE 0\r\n"+
|
| 596 |
+
"a=extmap-allow-mixed\r\na=msid-semantic: WMS\r\n"+
|
| 597 |
+
"m=application 9 UDP/DTLS/SCTP webrtc-datachannel\r\nc=IN IP4 0.0.0.0\r\n"+
|
| 598 |
+
(cands?cands+"\r\n":"")+"a=ice-ufrag:"+uf+"\r\na=ice-pwd:"+pw+"\r\n"+
|
| 599 |
+
"a=ice-options:trickle\r\na=fingerprint:sha-256 "+fp.match(/.{2}/g).join(":")+
|
| 600 |
+
"\r\na=setup:"+su+"\r\na=mid:0\r\na=sctp-port:5000\r\n"+
|
| 601 |
+
"a=max-message-size:262144\r\n"};
|
| 602 |
+
}
|
| 603 |
+
const gather=pc=>new Promise(r=>{
|
| 604 |
+
if(pc.iceGatheringState==="complete") return r();
|
| 605 |
+
pc.onicecandidate=e=>{ if(!e.candidate) r() }; setTimeout(r,3000); });
|
| 606 |
+
const send=(c,t)=>{try{if(c&&c.readyState==="open")c.send(t)}catch(e){}};
|
| 607 |
+
|
| 608 |
+
function greet(pc,ctl){
|
| 609 |
+
const hi=()=>send(ctl,"IAM|id="+MYID+"|json=0");
|
| 610 |
+
ctl.onopen=hi; if(ctl.readyState==="open") hi();
|
| 611 |
+
ctl.onmessage=e=>{ const m=e.data.match(/^IAM\|id=([0-9a-f]+)/);
|
| 612 |
+
if(!m) return;
|
| 613 |
+
/* the same colour on both ends is not two boats β it is one machine
|
| 614 |
+
talking to itself (a second tab of the same browser). Say so. */
|
| 615 |
+
if(m[1]===MYID){ toast("that is this same browser β same colour, same boat");
|
| 616 |
+
try{pc.close()}catch(e){} return; }
|
| 617 |
+
if(!peers.has(m[1])) attach(m[1],pc,ctl); };
|
| 618 |
+
pc.ondatachannel=e=>{ const L=e.channel.label;
|
| 619 |
+
if(L.startsWith("give:")) takeGift(e.channel);
|
| 620 |
+
else if(L.startsWith("play:")) playIn(e.channel); };
|
| 621 |
+
}
|
| 622 |
+
function attach(id,pc,ctl){
|
| 623 |
+
const rec={pc,ctl,doors:new Map()}; peers.set(id,rec);
|
| 624 |
+
absorb(id); spend();
|
| 625 |
+
ctl.onmessage=e=>onCtl(id,e.data);
|
| 626 |
+
ctl.onclose=()=>{peers.delete(id);count();refresh("people");
|
| 627 |
+
toast(id.slice(0,6)+" unlinked Β· colour kept")};
|
| 628 |
+
pc.ondatachannel=e=>{
|
| 629 |
+
const L=e.channel.label;
|
| 630 |
+
if(L.startsWith("door:")){ const d=L.slice(5);
|
| 631 |
+
e.channel.onmessage=ev=>onDoor(id,d,ev.data); rec.doors.set(d,e.channel);
|
| 632 |
+
if(!myDoors.includes(d)){myDoors.push(d);saveDoors();refresh("doors")} }
|
| 633 |
+
else if(L.startsWith("give:")) takeGift(e.channel);
|
| 634 |
+
else if(L.startsWith("play:")) playIn(e.channel);
|
| 635 |
+
};
|
| 636 |
+
for(const d of myDoors) fan(d,"OPEN|from="+MYID+"|json=0");
|
| 637 |
+
count(); refresh("people"); toast(id.slice(0,6)+" Β· colour absorbed");
|
| 638 |
+
const others=[...peers.keys()].filter(x=>x!==id);
|
| 639 |
+
if(others.length) send(ctl,"HELLO|id="+MYID+"|peers="+others.join(",")+"|json=0");
|
| 640 |
+
}
|
| 641 |
+
function spend(){ const p=open.get("open");
|
| 642 |
+
if(p){ const a=p.querySelector("#mine"),b=p.querySelector("#theirs");
|
| 643 |
+
if(a)a.value=""; if(b)b.value=""; if(p._spent)p._spent(); } }
|
| 644 |
+
const count=()=>{ $("#mypeers").textContent=peers.size };
|
| 645 |
+
|
| 646 |
+
async function onCtl(from,row){
|
| 647 |
+
const f={}; row.split("|").forEach(kv=>{const i=kv.indexOf("=");
|
| 648 |
+
if(i>0) f[kv.slice(0,i)]=kv.slice(i+1)});
|
| 649 |
+
const tag=row.split("|")[0];
|
| 650 |
+
if(tag==="HELLO"){
|
| 651 |
+
for(const p of (f.peers||"").split(",").filter(Boolean)){
|
| 652 |
+
if(p===MYID||peers.has(p)||pending.has(p)) continue;
|
| 653 |
+
const pc=new RTCPeerConnection(ICE);
|
| 654 |
+
greet(pc,pc.createDataChannel("ctl"));
|
| 655 |
+
await pc.setLocalDescription(await pc.createOffer()); await gather(pc);
|
| 656 |
+
pending.set(p,pc);
|
| 657 |
+
send(peers.get(from).ctl,"INTRO|to="+p+"|from="+MYID+"|sdp="+
|
| 658 |
+
reduce(pc.localDescription)+"|json=0");
|
| 659 |
+
}
|
| 660 |
+
} else if(tag==="INTRO"){
|
| 661 |
+
if(f.to===MYID){
|
| 662 |
+
const pc=new RTCPeerConnection(ICE);
|
| 663 |
+
pc.ondatachannel=e=>{ if(e.channel.label==="ctl") greet(pc,e.channel) };
|
| 664 |
+
await pc.setRemoteDescription(expand(f.sdp));
|
| 665 |
+
await pc.setLocalDescription(await pc.createAnswer()); await gather(pc);
|
| 666 |
+
send(peers.get(from).ctl,"BACK|to="+f.from+"|from="+MYID+"|sdp="+
|
| 667 |
+
reduce(pc.localDescription)+"|json=0");
|
| 668 |
+
} else if(peers.has(f.to)) send(peers.get(f.to).ctl,row);
|
| 669 |
+
} else if(tag==="BACK"){
|
| 670 |
+
if(f.to===MYID&&pending.has(f.from)){
|
| 671 |
+
await pending.get(f.from).setRemoteDescription(expand(f.sdp));
|
| 672 |
+
pending.delete(f.from);
|
| 673 |
+
} else if(peers.has(f.to)) send(peers.get(f.to).ctl,row);
|
| 674 |
+
}
|
| 675 |
+
}
|
| 676 |
+
|
| 677 |
+
/* =========================== the opening ================================ */
|
| 678 |
+
function openPane(){
|
| 679 |
+
pane("open","the first meeting β by hand, once",(b,p)=>{
|
| 680 |
+
b.innerHTML=
|
| 681 |
+
"<p>Give <b>one</b> person your block. After that, everyone else arrives "+
|
| 682 |
+
"through the links that already exist.</p>"+
|
| 683 |
+
'<h4 id=lbl>yours β hand it over</h4><textarea id=mine readonly></textarea>'+
|
| 684 |
+
'<button class="btn g" id=copy>copy</button>'+
|
| 685 |
+
'<button class="btn g" id=mint>mint a fresh one</button>'+
|
| 686 |
+
'<h4>theirs β paste it</h4><textarea id=theirs></textarea>'+
|
| 687 |
+
'<button class="btn" id=take>take it</button>'+
|
| 688 |
+
'<div class=row style="margin-top:8px"><span class=k>traces kept</span>'+
|
| 689 |
+
'<span class=ok id=nt>0</span></div>';
|
| 690 |
+
p._spent=()=>{ b.querySelector("#nt").textContent="0";
|
| 691 |
+
b.querySelector("#lbl").textContent="spent β mint a fresh one for the next person"; };
|
| 692 |
+
b.querySelector("#copy").onclick=()=>{
|
| 693 |
+
b.querySelector("#mine").select(); document.execCommand("copy"); toast("copied"); };
|
| 694 |
+
b.querySelector("#mint").onclick=()=>mint(b);
|
| 695 |
+
b.querySelector("#take").onclick=()=>take(b);
|
| 696 |
+
mint(b);
|
| 697 |
+
},370,430);
|
| 698 |
+
}
|
| 699 |
+
async function mint(b){
|
| 700 |
+
isHost=true;
|
| 701 |
+
bootPc=new RTCPeerConnection(ICE);
|
| 702 |
+
greet(bootPc,bootPc.createDataChannel("ctl"));
|
| 703 |
+
await bootPc.setLocalDescription(await bootPc.createOffer());
|
| 704 |
+
await gather(bootPc);
|
| 705 |
+
const small=reduce(bootPc.localDescription);
|
| 706 |
+
b.querySelector("#mine").value=small;
|
| 707 |
+
b.querySelector("#nt").textContent="1";
|
| 708 |
+
b.querySelector("#lbl").textContent="yours β one use, for one person";
|
| 709 |
+
if(!MYID) await name(small);
|
| 710 |
+
}
|
| 711 |
+
async function take(b){
|
| 712 |
+
const raw=b.querySelector("#theirs").value.trim(); if(!raw) return;
|
| 713 |
+
let d; try{ d=expand(raw) }catch(e){ toast("that isn't a block"); return }
|
| 714 |
+
if(d.type==="answer"&&isHost&&bootPc){
|
| 715 |
+
try{ await bootPc.setRemoteDescription(d); toast("taken Β· opening") }
|
| 716 |
+
catch(e){ toast("that block was for a different opening") }
|
| 717 |
+
return;
|
| 718 |
+
}
|
| 719 |
+
const pc=new RTCPeerConnection(ICE);
|
| 720 |
+
pc.ondatachannel=e=>{ if(e.channel.label==="ctl") greet(pc,e.channel) };
|
| 721 |
+
await pc.setRemoteDescription(d);
|
| 722 |
+
await pc.setLocalDescription(await pc.createAnswer()); await gather(pc);
|
| 723 |
+
const small=reduce(pc.localDescription);
|
| 724 |
+
bootPc=pc; isHost=false;
|
| 725 |
+
if(!MYID) await name(small);
|
| 726 |
+
b.querySelector("#mine").value=small;
|
| 727 |
+
b.querySelector("#nt").textContent="1";
|
| 728 |
+
b.querySelector("#lbl").textContent="your answer β send it back to them";
|
| 729 |
+
toast("answer ready β send it back");
|
| 730 |
+
}
|
| 731 |
+
async function name(seed){
|
| 732 |
+
MYID=(await sha(seed)).slice(0,16);
|
| 733 |
+
$("#myid").textContent=MYID.slice(0,10);
|
| 734 |
+
$("#mykey").style.background=grad(MYID);
|
| 735 |
+
}
|
| 736 |
+
|
| 737 |
+
/* =========================== the field ================================== */
|
| 738 |
+
const sc=$("#stars"), sg=sc.getContext("2d");
|
| 739 |
+
let SW=0,SH=0,T=0;
|
| 740 |
+
function fitStars(){ SW=sc.width=innerWidth*2; SH=sc.height=innerHeight*2 }
|
| 741 |
+
addEventListener("resize",()=>{fitStars();for(const p of open.values())p._fit&&p._fit()});
|
| 742 |
+
fitStars();
|
| 743 |
+
function field(){
|
| 744 |
+
T+=0.006;
|
| 745 |
+
sg.clearRect(0,0,SW,SH);
|
| 746 |
+
const ids=[MYID,...peers.keys(),...held].filter(Boolean);
|
| 747 |
+
const uniq=[...new Set(ids)];
|
| 748 |
+
const cx=SW/2, cy=SH*0.42, R=Math.min(SW,SH)*0.34;
|
| 749 |
+
uniq.forEach((id,i)=>{
|
| 750 |
+
const a=T+i*(2*Math.PI/Math.max(1,uniq.length));
|
| 751 |
+
const live=id===MYID||peers.has(id);
|
| 752 |
+
const x=cx+Math.cos(a)*R*(live?1:1.35);
|
| 753 |
+
const y=cy+Math.sin(a)*R*0.5*(live?1:1.35);
|
| 754 |
+
const [ca,cb]=stops(id), r=id===MYID?26:live?18:11;
|
| 755 |
+
const g=sg.createRadialGradient(x,y,0,x,y,r*3);
|
| 756 |
+
g.addColorStop(0,"rgba("+ca+","+(live?.5:.2)+")");
|
| 757 |
+
g.addColorStop(1,"rgba(0,0,0,0)");
|
| 758 |
+
sg.fillStyle=g; sg.beginPath(); sg.arc(x,y,r*3,0,6.2832); sg.fill();
|
| 759 |
+
sg.globalAlpha=live?.85:.34; sg.fillStyle="rgb("+cb+")";
|
| 760 |
+
sg.beginPath(); sg.arc(x,y,r,0,6.2832); sg.fill(); sg.globalAlpha=1;
|
| 761 |
+
if(live&&id!==MYID){ sg.strokeStyle="rgba("+cb+",.22)"; sg.lineWidth=2;
|
| 762 |
+
sg.beginPath(); sg.moveTo(cx,cy); sg.lineTo(x,y); sg.stroke(); }
|
| 763 |
+
});
|
| 764 |
+
requestAnimationFrame(field);
|
| 765 |
+
}
|
| 766 |
+
requestAnimationFrame(field);
|
| 767 |
+
|
| 768 |
+
/* =========================== wiring ===================================== */
|
| 769 |
+
$("#d-open").onclick=openPane;
|
| 770 |
+
document.querySelectorAll("#sill button[data-door]").forEach(btn=>{
|
| 771 |
+
btn.onclick=()=>{ const d=btn.dataset.door;
|
| 772 |
+
if(open.has(d)){ open.get(d).remove(); open.delete(d); lit(); return; }
|
| 773 |
+
pane(d,d==="what"?"what is this":d,DOORS[d],
|
| 774 |
+
d==="paint"||d==="play"||d==="rooms"?420:340,
|
| 775 |
+
d==="what"?420:d==="play"||d==="rooms"?400:320); };
|
| 776 |
+
});
|
| 777 |
+
(async()=>{ /* a colour before anyone arrives, from this browser alone */
|
| 778 |
+
let s=null; try{s=localStorage.getItem("ld.seed")}catch(e){}
|
| 779 |
+
if(!s){ s=[...crypto.getRandomValues(new Uint8Array(24))]
|
| 780 |
+
.map(x=>x.toString(16).padStart(2,"0")).join("");
|
| 781 |
+
try{localStorage.setItem("ld.seed",s)}catch(e){} }
|
| 782 |
+
await name(s);
|
| 783 |
+
for(const d of myDoors) if(!doors.has(d)) doors.set(d,[]);
|
| 784 |
+
pane("what","what is this",DOORS.what,360,420);
|
| 785 |
+
toast("your colour is yours β nobody handed it to you");
|
| 786 |
+
})();
|
| 787 |
+
</script>
|
| 788 |
+
<!-- βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 789 |
+
THE CORRIDOR β every door reaches every other door.
|
| 790 |
+
Generated 2026-08-07 from a live measurement of all 27 doors.
|
| 791 |
+
Pure anchors. No src=, no <link href>, no script, no network at runtime:
|
| 792 |
+
this block renders with the network unplugged, and every line in it is
|
| 793 |
+
a way out. Paste it directly before </body>.
|
| 794 |
+
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ -->
|
| 795 |
+
<nav id="corridor" style="margin:3rem auto 2rem;max-width:60rem;padding:1.25rem 1.5rem;border-top:1px solid currentColor;font:14px/1.9 ui-monospace,SFMono-Regular,Menlo,monospace;opacity:.85">
|
| 796 |
+
<div style="letter-spacing:.14em;text-transform:uppercase;opacity:.6;margin-bottom:.75rem">the corridor — 27 doors</div>
|
| 797 |
+
<div style="display:grid;grid-template-columns:repeat(auto-fill,minmax(17rem,1fr));gap:.15rem .75rem">
|
| 798 |
+
<a href="https://jessebrown1980.github.io/all-the-doors/">all the doors</a>
|
| 799 |
+
<a href="https://jessebrown1980.github.io/asolaria-multiverse/">asolaria multiverse</a>
|
| 800 |
+
<a href="https://jessebrown1980.github.io/Be-infinitly-express-spherically-free/">Be infinitly express spherically free</a>
|
| 801 |
+
<a href="https://jessebrown1980.github.io/Browns-infinite-play-and-zoom/">Browns infinite play and zoom</a>
|
| 802 |
+
<a href="https://jessebrown1980.github.io/does-2-to-the-n-always-contain-a-digit-2-in-base-3/">does 2 to the n always contain a digit 2 in base 3</a>
|
| 803 |
+
<a href="https://jessebrown1980.github.io/does-the-closure-survive-81-levels/">does the closure survive 81 levels</a>
|
| 804 |
+
<a href="https://jessebrown1980.github.io/free-at-all-levels/">free at all levels</a>
|
| 805 |
+
<a href="https://jessebrown1980.github.io/Higgs-Bell-Hilbert-and-Brown-at-the-zero/">Higgs Bell Hilbert and Brown at the zero</a>
|
| 806 |
+
<a href="https://jessebrown1980.github.io/identity-kernel-registry/">identity kernel registry</a>
|
| 807 |
+
<a href="https://jessebrown1980.github.io/light-boat-engine-ships-with-oils/">light boat engine ships with oils</a>
|
| 808 |
+
<a href="https://jessebrown1980.github.io/lightDOORwsMs/">lightDOORwsMs</a>
|
| 809 |
+
<a href="https://jessebrown1980.github.io/one-click/">one click</a>
|
| 810 |
+
<a href="https://jessebrown1980.github.io/see-her-anywhere-so-not-lost-to-time/">see her anywhere so not lost to time</a>
|
| 811 |
+
<a href="https://jessebrown1980.github.io/the-bridge-you-need-three-languages/">the bridge you need three languages</a>
|
| 812 |
+
<a href="https://jessebrown1980.github.io/The-Brown-Light-erdos-Engine-block-powered-by-light/">The Brown Light erdos Engine block powered by light</a>
|
| 813 |
+
<a href="https://jessebrown1980.github.io/the-browns-solution-to-erods-o0O-nx3-6-for-1-with--1-3/">the browns solution to erods o0O nx3 6 for 1 with 1 3</a>
|
| 814 |
+
<a href="https://jessebrown1980.github.io/the-colour-qr/">the colour qr</a>
|
| 815 |
+
<a href="https://jessebrown1980.github.io/the-family-of-verses/">the family of verses</a>
|
| 816 |
+
<a href="https://jessebrown1980.github.io/the-human-based-understanding/">the human based understanding</a>
|
| 817 |
+
<a href="https://jessebrown1980.github.io/the-law-of-space-and-light-and-color-and-travel-at-the-speed-of-one/">the law of space and light and color and travel at the speed of one</a>
|
| 818 |
+
<a href="https://jessebrown1980.github.io/the-light-boat/">the light boat</a>
|
| 819 |
+
<a href="https://jessebrown1980.github.io/the-light-hub-http-0/">the light hub http 0</a>
|
| 820 |
+
<a href="https://jessebrown1980.github.io/the-null-space-or-NO-jo0O/">the null space or NO jo0O</a>
|
| 821 |
+
<a href="https://jessebrown1980.github.io/the-zebras-z-is-the-squared-wave/">the zebras z is the squared wave</a>
|
| 822 |
+
<a href="https://jessebrown1980.github.io/tribute-three-around-a-free-centre/">tribute three around a free centre</a>
|
| 823 |
+
<a href="https://jessebrown1980.github.io/two-fixed-points-have-an-interval/">two fixed points have an interval</a>
|
| 824 |
+
<a href="https://jessebrown1980.github.io/why-mycelium-grounded-and-the-ases/">why mycelium grounded and the ases</a>
|
| 825 |
+
</div>
|
| 826 |
+
<div style="margin-top:1rem;opacity:.5;font-size:12px">the light is the code. every door open, every door reachable.</div>
|
| 827 |
+
</nav>
|