acer Claude Opus 5 (1M context) commited on
Commit
4444df5
Β·
1 Parent(s): e8ef9cc

open the door - the corridor goes in

Browse files

27 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>

Files changed (1) hide show
  1. 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=>({"&":"&amp;","<":"&lt;",">":"&gt;"}[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 &mdash; 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&#39;s address</span><span id=ra class=acc>&mdash;</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&#39;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> &mdash; talk, draw, "+
353
- "hand files over. Type a different word and you are somewhere else entirely. "+
354
- "<b>Nobody who doesn&#39;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 &mdash; 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 &mdash; "+peers.size+" boat(s) can meet you here"
390
- : "entered &mdash; 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 &rarr; 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 &mdash; 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=>({"&":"&amp;","<":"&lt;",">":"&gt;"}[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 &mdash; 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&#39;s address</span><span id=ra class=acc>&mdash;</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&#39;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> &mdash; talk, draw, "+
353
+ "hand files over. Type a different word and you are somewhere else entirely. "+
354
+ "<b>Nobody who doesn&#39;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 &mdash; 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 &mdash; "+peers.size+" boat(s) can meet you here"
390
+ : "entered &mdash; 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 &rarr; 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 &mdash; 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 &mdash; 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>