- dedicated git repo at /home/avi/Projects/Lumen (main)
- TypeScript strict (target ES2022, bundler, exactOptionalPropertyTypes, noUncheckedIndexedAccess)
- ESLint 9 + typescript-eslint strictTypeChecked + eslint-plugin-boundaries for B-1..B-7, no-restricted-globals/syntax for B-1/B-7
- Prettier 3.5
- Structure per IMPLEMENTATION-CONTRACT.md §4 (src/platform/idb|cache|sw, storage, data, sync/{transport,verifier}, domain/{emergency,schedule,map,festival,readiness,clock,favorites}, ui/{components,views,router,render}, app, emergency-baseline, assets, public, content, pipeline, tests, scripts)
- CI: .github/workflows/ci.yml (typecheck + lint + format + test)
- Boundary tests: tests/unit/boundaries.test.ts (4 tests) + scripts/check-boundaries.ts
- No feature code, no PWA/IDB/sync/mesh/accounts per contract Stage 1
137 lines
5.9 KiB
HTML
137 lines
5.9 KiB
HTML
<!doctype html>
|
|
<!--
|
|
EXP-3 — Map representation synthetic benchmark (SPIKE-03)
|
|
Disposable experiment. NOT application code. No dependencies.
|
|
|
|
Headless-desktop ONLY. Numbers describe relative JS/frame-work cost on a
|
|
dev machine; they are NOT representative of low-end mobile GPUs, memory,
|
|
or compositors. Final validation requires the real-device protocol in
|
|
SPIKE-03 §7.
|
|
|
|
Variants:
|
|
A) Raster base image + DOM POI overlay + CSS-transform pan/zoom
|
|
B) Full SVG (ellipses + circle markers) with CSS-transform pan/zoom
|
|
C) Canvas full redraw every frame
|
|
Metric: average synchronous JS work per animation frame (ms), 240 frames,
|
|
plus setup time and JS heap delta (Chromium-only).
|
|
-->
|
|
<html><head><meta charset="utf-8"><title>mapbench</title>
|
|
<style>
|
|
html,body{margin:0;background:#111}
|
|
#stage{position:fixed;inset:0;overflow:hidden;touch-action:none}
|
|
.poi{position:absolute;width:14px;height:14px;margin:-7px 0 0 -7px;border-radius:50%;background:#f43f5e;will-change:transform}
|
|
svg,canvas{display:block}
|
|
</style></head><body>
|
|
<div id="stage"></div><pre id="out">RUNNING</pre>
|
|
<script>
|
|
'use strict';
|
|
const W=2048,H=1536,POI_COUNTS=[100,300],FRAMES=240;
|
|
const stage=document.getElementById('stage');
|
|
const out=document.getElementById('out');
|
|
const results={env:'headless-desktop',variants:[]};
|
|
|
|
function makeBaseImage(){
|
|
const c=document.createElement('canvas');c.width=W;c.height=H;
|
|
const x=c.getContext('2d');
|
|
x.fillStyle='#1c3326';x.fillRect(0,0,W,H);
|
|
for(let i=0;i<400;i++){x.fillStyle=`rgba(${60+(i%60)},${120+(i%80)},${70+(i%50)},.35)`;
|
|
x.beginPath();x.ellipse((i*97)%W,(i*211)%H,30+(i%90),20+(i%60),i,0,7);x.fill();}
|
|
x.strokeStyle='#c9a86a';x.lineWidth=10;
|
|
for(let r=0;r<6;r++){x.beginPath();x.moveTo(0,200+r*220);x.bezierCurveTo(W*.3,150+r*220,W*.6,260+r*220,W,200+r*220);x.stroke();}
|
|
return c;
|
|
}
|
|
const BASE=makeBaseImage();
|
|
|
|
function animate(step,done){
|
|
let frames=0,work=0;
|
|
function loop(){
|
|
if(frames>=FRAMES){done(frames,work);return;}
|
|
const p=frames/FRAMES;
|
|
const t0=performance.now();
|
|
step(p);
|
|
work+=performance.now()-t0;
|
|
frames++;
|
|
setTimeout(loop,1);
|
|
}
|
|
setTimeout(loop,1);
|
|
}
|
|
const easeScale=(p)=>1+0.8*Math.abs(Math.sin(p*Math.PI*2));
|
|
const panX=(p)=>80*Math.sin(p*Math.PI*4);
|
|
const panY=(p)=>60*Math.cos(p*Math.PI*4);
|
|
const mem=()=>performance.memory?performance.memory.usedJSHeapSize:NaN;
|
|
const metrics=(variant,n,s0,m0,frames,work)=>({
|
|
variant,pois:n,frames,
|
|
setupMs:Math.round(performance.now()-s0-(work||0)),
|
|
avgJsMsPerFrame:+(work/frames).toFixed(3),
|
|
heapDeltaMB:Math.round((mem()-m0)/1048576)
|
|
});
|
|
|
|
// ---------- Variant A: raster base + DOM overlay + transform ----------
|
|
function variantA(n,done){
|
|
const m0=mem(),s0=performance.now();
|
|
const wrap=document.createElement('div');
|
|
wrap.style.cssText='transform-origin:0 0;will-change:transform';
|
|
const img=document.createElement('img');
|
|
img.style.cssText=`width:${W/2}px;height:${H/2}px`;
|
|
wrap.appendChild(img);
|
|
for(let i=0;i<n;i++){const d=document.createElement('div');d.className='poi';
|
|
d.style.left=((i*197)%W)/2+'px';d.style.top=((i*263)%H)/2+'px';wrap.appendChild(d);}
|
|
stage.replaceChildren(wrap);
|
|
const start=()=>animate((p)=>{
|
|
wrap.style.transform=`translate(${panX(p)}px,${panY(p)}px) scale(${easeScale(p)})`;
|
|
},(frames,work)=>done(metrics('A-raster+dom-overlay',n,s0,m0,frames,work)));
|
|
img.src=BASE.toDataURL('image/webp',0.8);
|
|
if(img.complete&&img.naturalWidth>0)start();else{img.onload=start;img.onerror=start;}
|
|
}
|
|
|
|
// ---------- Variant B: full SVG ----------
|
|
function variantB(n,done){
|
|
const m0=mem(),s0=performance.now();
|
|
const NS='http://www.w3.org/2000/svg';
|
|
const svg=document.createElementNS(NS,'svg');
|
|
svg.setAttribute('viewBox',`0 0 ${W} ${H}`);
|
|
svg.style.cssText=`width:${W/2}px;height:${H/2}px;transform-origin:0 0;will-change:transform`;
|
|
const g=document.createElementNS(NS,'g');
|
|
for(let i=0;i<300;i++){const e=document.createElementNS(NS,'ellipse');
|
|
e.setAttribute('cx',(i*97)%W);e.setAttribute('cy',(i*211)%H);
|
|
e.setAttribute('rx',30+(i%90));e.setAttribute('ry',20+(i%60));
|
|
e.setAttribute('fill',`rgba(${60+(i%60)},${120+(i%80)},${70+(i%50)},.35)`);g.appendChild(e);}
|
|
for(let i=0;i<n;i++){const c=document.createElementNS(NS,'circle');
|
|
c.setAttribute('cx',(i*197)%W);c.setAttribute('cy',(i*263)%H);
|
|
c.setAttribute('r',10);c.setAttribute('fill','#f43f5e');g.appendChild(c);}
|
|
svg.appendChild(g);stage.replaceChildren(svg);
|
|
setTimeout(()=>animate((p)=>{
|
|
svg.style.transform=`translate(${panX(p)}px,${panY(p)}px) scale(${easeScale(p)})`;
|
|
},(frames,work)=>done(metrics('B-full-svg',n,s0,m0,frames,work))));
|
|
}
|
|
|
|
// ---------- Variant C: canvas redraw ----------
|
|
function variantC(n,done){
|
|
const m0=mem(),s0=performance.now();
|
|
const cv=document.createElement('canvas');
|
|
const dw=Math.min(innerWidth,W/2),dh=dw*H/W;
|
|
cv.width=dw*devicePixelRatio;cv.height=dh*devicePixelRatio;
|
|
cv.style.cssText=`width:${dw}px;height:${dh}px`;
|
|
const x=cv.getContext('2d');stage.replaceChildren(cv);
|
|
const pts=Array.from({length:n},(_,i)=>[(i*197)%W,(i*263)%H]);
|
|
animate((p)=>{
|
|
x.setTransform(1,0,0,1,0,0);x.clearRect(0,0,cv.width,cv.height);
|
|
const s=easeScale(p)*devicePixelRatio*(dw/W);
|
|
x.setTransform(s,0,0,s,panX(p)*devicePixelRatio,panY(p)*devicePixelRatio);
|
|
x.drawImage(BASE,0,0);
|
|
x.fillStyle='#f43f5e';
|
|
for(const[px,py]of pts){x.beginPath();x.arc(px,py,10/s*devicePixelRatio,0,7);x.fill();}
|
|
},(frames,work)=>done(metrics('C-canvas-redraw',n,s0,m0,frames,work)));
|
|
}
|
|
|
|
const queue=[];
|
|
for(const n of POI_COUNTS){queue.push(()=>variantA(n,next),()=>variantB(n,next),()=>variantC(n,next));}
|
|
function next(r){results.variants.push(r);document.title='step:'+r.variant+'-'+r.pois;run();}
|
|
function run(){
|
|
const f=queue.shift();
|
|
if(!f){out.textContent='RESULTS:'+JSON.stringify(results,null,1);document.title='DONE';return;}
|
|
setTimeout(()=>{try{f();}catch(e){out.textContent='ERROR:'+(e&&e.stack||e);document.title='ERROR';}},50);
|
|
}
|
|
document.title='started';
|
|
run();
|
|
</script></body></html>
|