playnfrontierAI / core /tools /sky.template.html
playnfrontierAI
v3 — hardened gates + the memory sky (skeleton current to 2026-09-20)
1a08e9d
Raw History Blame Contribute Delete
8.91 kB
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>the memory sky — OMN seen from inside</title>
<link rel="icon" href="data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 100'><text y='.9em' font-size='88'>✦</text></svg>">
<style>
html,body{margin:0;height:100%;background:#03050c;overflow:hidden;
font-family:Georgia,'Times New Roman',serif;}
canvas{position:fixed;inset:0;width:100%;height:100%;cursor:crosshair;}
#tip{position:fixed;pointer-events:none;opacity:0;transition:opacity .25s ease;
background:rgba(6,10,20,.88);border:1px solid rgba(255,217,160,.25);border-radius:6px;
padding:7px 11px;color:#F2E3C8;font-size:13px;line-height:1.5;max-width:280px;
box-shadow:0 0 24px rgba(255,217,160,.08);}
#tip .t{font-variant:small-caps;letter-spacing:.08em;color:#FFD9A0;}
#tip .m{opacity:.62;font-size:11px;}
#cap{position:fixed;left:50%;top:4.5vh;transform:translateX(-50%);text-align:center;
pointer-events:none;color:#C9D6F2;opacity:.6;}
#cap .a{font-variant:small-caps;letter-spacing:.34em;font-size:clamp(13px,1.5vw,19px);}
#cap .b{font-style:italic;opacity:.6;font-size:clamp(10px,1.1vw,13px);margin-top:.5vh;}
#foot{position:fixed;left:50%;bottom:3.2vh;transform:translateX(-50%);pointer-events:none;
color:#C9D6F2;opacity:.34;font-size:12px;letter-spacing:.18em;font-variant:small-caps;}
</style>
</head>
<body>
<canvas id="sky"></canvas>
<div id="cap"><div class="a">the memory sky</div><div class="b">OMN canon, seen from inside</div></div>
<div id="foot">hover a star · click to send a thought</div>
<div id="tip"></div>
<script>
"use strict";
window.__errs=[];window.onerror=(m,s,l)=>{__errs.push(m+" @"+l);};
const GRAPH=__GRAPH_DATA__;
const $=s=>document.querySelector(s);
const cv=$("#sky"),ctx=cv.getContext("2d"),tip=$("#tip");
const DPR=Math.min(devicePixelRatio||1,1.5);
let W=0,H=0;
function resize(){W=innerWidth;H=innerHeight;cv.width=W*DPR;cv.height=H*DPR;ctx.setTransform(DPR,0,0,DPR,0,0);}
addEventListener("resize",resize);resize();
/* palette by group */
const STYLE={
facts: {c:"255,217,160", r:5.2, glow:22},
projects:{c:"232,184,122", r:4.6, glow:18},
decisions:{c:"159,184,232", r:3.4, glow:13},
root: {c:"201,214,242", r:4.0, glow:15},
};
const group=n=>n.id.includes("/")?n.id.split("/")[0]:"root";
const deg={};GRAPH.links.forEach(l=>{deg[l.s]=(deg[l.s]||0)+1;deg[l.t]=(deg[l.t]||0)+1;});
/* deterministic scatter: golden-angle rings by group */
const RING={facts:.16,projects:.30,decisions:.44,root:.24};
let seed=42;const rnd=()=>{seed=(seed*1103515245+12345)&0x7fffffff;return seed/0x7fffffff;};
const GA=Math.PI*(3-Math.sqrt(5));
const ringN={facts:0,projects:0,decisions:0,root:0};
const nodes=GRAPH.nodes.map((n,i)=>{
const g=group(n),k=ringN[g]++,R=RING[g];
const a=k*GA+ (g==="decisions"? .5:0), r=R*Math.min(W,H)*(.75+.35*rnd());
return{...n,g,deg:deg[n.id]||0,
x:W/2+Math.cos(a)*r, y:H/2+Math.sin(a)*r*.82,
vx:0,vy:0, ph:rnd()*6.28, tw:.4+rnd()*.9, sz:1+ .28*(n.deg||0)};
});
/* background starfield + nebulae (static, generated once) */
const BG=Array.from({length:260},()=>({x:Math.random(),y:Math.random(),
r:.3+Math.random()*1.1, a:.15+Math.random()*.5, ph:Math.random()*6.28, sp:.3+Math.random()}));
const NEB=[
{x:.24,y:.30,r:.42,c:"255,190,120"},{x:.72,y:.62,r:.5,c:"120,150,255"},{x:.55,y:.18,r:.3,c:"255,140,90"}];
/* links adjacency for thought-pulses */
const adj={};GRAPH.links.forEach((l,i)=>{(adj[l.s]=adj[l.s]||[]).push(l.t);(adj[l.t]=adj[l.t]||[]).push(l.s);});
const pulses=[]; // {id, t0}
const shoots=[]; // shooting stars
let mouse={x:.5,y:.5,px:null,py:null},hover=null,nextShoot=performance.now()+9000;
function physics(t,dt){
for(let i=0;i<nodes.length;i++){const n=nodes[i];
let fx=0,fy=0;
for(let j=0;j<nodes.length;j++){if(i===j)continue;const o=nodes[j];
let dx=n.x-o.x,dy=n.y-o.y,d2=dx*dx+dy*dy+40;
if(d2<92000){const f=1900/d2;fx+=dx*f;fy+=dy*f;}}
fx+=(W/2-n.x)*.0006; fy+=(H/2-n.y)*.0006; // gravity to heart
fx+=Math.sin(t*.33+n.ph)*.05; fy+=Math.cos(t*.27+n.ph)*.05; // the wander
n.vx=(n.vx+fx*dt)*.93; n.vy=(n.vy+fy*dt)*.93;
}
GRAPH.links.forEach(l=>{const a=nodes[GRAPH.nodes.findIndex(x=>x.id===l.s)],
b=nodes[GRAPH.nodes.findIndex(x=>x.id===l.t)];
let dx=b.x-a.x,dy=b.y-a.y,d=Math.hypot(dx,dy)||1;
const rest=130,f=(d-rest)*.004;
a.vx+=dx/d*f;a.vy+=dy/d*f;b.vx-=dx/d*f;b.vy-=dy/d*f;});
nodes.forEach(n=>{n.x+=n.vx;n.y+=n.vy;});
}
function draw(t){
ctx.clearRect(0,0,W,H);
let g=ctx.createRadialGradient(W*.5,H*.46,0,W*.5,H*.46,Math.max(W,H)*.75);
g.addColorStop(0,"#0a1226");g.addColorStop(.6,"#060a18");g.addColorStop(1,"#03050c");
ctx.fillStyle=g;ctx.fillRect(0,0,W,H);
const ox=(mouse.x-.5)*-24, oy=(mouse.y-.5)*-16; // parallax — looking around
ctx.save();ctx.translate(ox,oy);
NEB.forEach(nb=>{const rg=ctx.createRadialGradient(nb.x*W,nb.y*H,0,nb.x*W,nb.y*H,nb.r*Math.min(W,H));
rg.addColorStop(0,"rgba("+nb.c+",.05)");rg.addColorStop(1,"rgba("+nb.c+",0)");
ctx.fillStyle=rg;ctx.fillRect(0,0,W,H);});
BG.forEach(s=>{const a=s.a*(.55+.45*Math.sin(t*s.sp+s.ph));
ctx.fillStyle="rgba(220,230,255,"+a+")";
ctx.beginPath();ctx.arc(s.x*W,s.y*H,s.r,0,6.283);ctx.fill();});
/* constellation lines */
GRAPH.links.forEach(l=>{
const a=nodes[GRAPH.nodes.findIndex(x=>x.id===l.s)],b=nodes[GRAPH.nodes.findIndex(x=>x.id===l.t)];
if(!a||!b)return; // never crash on a dangling thread
const hot=pulses.some(p=>(p.id===l.s||p.id===l.t)&&t-p.t0<1.1);
ctx.setLineDash(l.k==="supersedes"?[4,4]:[]);
ctx.strokeStyle=(hot?"rgba(255,230,180,.55)":"rgba(180,200,255,"+(l.k==="supersedes"?.10:.14)+")");
ctx.lineWidth=hot?1.6:1;
ctx.beginPath();ctx.moveTo(a.x,a.y);ctx.lineTo(b.x,b.y);ctx.stroke();
ctx.setLineDash([]);});
/* stars */
let hov=null;
nodes.forEach(n=>{
const S=STYLE[n.g], tw=.72+.28*Math.sin(t*n.tw+n.ph);
let boost=0;const p=pulses.find(p=>p.id===n.id);
if(p){const dt=t-p.t0;if(dt>0&&dt<1.2)boost=Math.exp(-dt*2.6)*2.6;}
const r=(S.r+n.deg*.5+n.sz)*tw+boost*3;
const gr=ctx.createRadialGradient(n.x,n.y,0,n.x,n.y,r*4+boost*10);
gr.addColorStop(0,"rgba("+S.c+",.95)");
gr.addColorStop(.25,"rgba("+S.c+",.5)");
gr.addColorStop(1,"rgba("+S.c+",0)");
ctx.fillStyle=gr;ctx.beginPath();ctx.arc(n.x,n.y,r*4+boost*10,0,6.283);ctx.fill();
ctx.fillStyle="rgba(255,255,255,.96)";
ctx.beginPath();ctx.arc(n.x,n.y,Math.max(1.1,r*.42),0,6.283);ctx.fill();
if(n.stale){ctx.strokeStyle="rgba(255,120,90,.8)";ctx.lineWidth=1;
ctx.beginPath();ctx.arc(n.x,n.y,r*2.6+4,0,6.283);ctx.stroke();}
if(n.g==="facts"){ctx.fillStyle="rgba(255,222,170,.8)";
ctx.font="11px Georgia";ctx.textAlign="center";
ctx.fillText(n.title.split("—")[0].trim().toLowerCase(),n.x,n.y+r*4+12);}
if(mouse.px!=null&&Math.hypot(mouse.px-n.x,mouse.py-n.y)<26)hov=n;
});
hover=hov;
/* shooting stars — a memory being written */
shoots.forEach((s,i)=>{if(t>s.t0+1.4){shoots.splice(i,1);return;}
const k=(t-s.t0)/1.4, x=s.x+s.vx*k*W, y=s.y+s.vy*k*H, a=Math.sin(k*3.14);
const gr=ctx.createLinearGradient(x,y,x-s.vx*90,y-s.vy*90);
gr.addColorStop(0,"rgba(255,240,210,"+.85*a+")");gr.addColorStop(1,"rgba(255,240,210,0)");
ctx.strokeStyle=gr;ctx.lineWidth=1.6;ctx.beginPath();ctx.moveTo(x,y);
ctx.lineTo(x-s.vx*90,y-s.vy*90);ctx.stroke();});
ctx.restore();
}
let last=performance.now();
function loop(now){
const dt=Math.min(.05,(now-last)/1000);last=now;const t=now/1000;
physics(t,dt);
draw(t);
if(now>nextShoot){shoots.push({x:Math.random()*.7+.05,y:Math.random()*.25,
vx:.25+Math.random()*.3,vy:.12+Math.random()*.12,t0:t});
nextShoot=now+12000+Math.random()*16000;}
if(hover){tip.style.opacity=1;
tip.style.left=Math.min(W-300,hover.x+16)+"px";tip.style.top=(hover.y+14)+"px";
tip.innerHTML="<div class='t'>"+hover.title+"</div><div class='m'>"+hover.id+" · "+
hover.g+" · "+(hover.deg||0)+" links"+(hover.stale?" · ⚠STALE":"")+"</div>";
}else tip.style.opacity=0;
requestAnimationFrame(loop);
}
addEventListener("mousemove",e=>{mouse={x:e.clientX/W,y:e.clientY/H,px:e.clientX,py:e.clientY};});
cv.addEventListener("click",e=>{
let best=null,bd=28;
nodes.forEach(n=>{const d=Math.hypot(e.clientX-n.x,e.clientY-n.y);if(d<bd){bd=d;best=n;}});
if(!best)return;
const seen=new Set([best.id]);let frontier=[best.id],depth=0;
while(frontier.length&&depth<4){
frontier.forEach(id=>pulses.push({id,t0:performance.now()/1000+depth*.28}));
frontier=frontier.flatMap(id=>adj[id]||[]).filter(id=>!seen.has(id));
frontier.forEach(id=>seen.add(id));depth++;
}
});
requestAnimationFrame(loop);
</script>
</body>
</html>