- Introduced export schema for JSON data structure. - Created renderer contract detailing canvas/SVG rendering requirements. - Defined IndexedDB storage schema and migration strategies. - Documented data model including entities and relationships. - Developed implementation plan outlining execution flow and project structure. - Provided quickstart guide for development environment setup. - Compiled research documentation on performance, accessibility, and theming. - Established feature specification with user scenarios and functional requirements.
158 lines
5.6 KiB
HTML
158 lines
5.6 KiB
HTML
<!doctype html>
|
|
<html lang="en">
|
|
<head>
|
|
<meta charset="utf-8" />
|
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
<title>GlowTrack Viz FPS Harness</title>
|
|
<style>
|
|
html, body { height: 100%; margin: 0; font-family: system-ui, sans-serif; }
|
|
#controls { padding: 8px; display: flex; gap: 12px; align-items: center; flex-wrap: wrap; }
|
|
#fps { font-weight: bold; }
|
|
canvas { display: block; width: 100%; height: calc(100% - 48px); background: #0b0b10; }
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<div id="controls">
|
|
<label>Tiles: <input id="tiles" type="number" min="30" max="365" value="365" /></label>
|
|
<label>Animate: <input id="animate" type="checkbox" checked /></label>
|
|
<label>Device Pixel Ratio: <input id="dpr" type="number" min="1" max="3" step="0.25" value="1" /></label>
|
|
<button id="runBench">Run Bench (30,90,180,365)</button>
|
|
<button id="download">Download JSON Report</button>
|
|
<span id="fps">FPS: --</span>
|
|
</div>
|
|
<canvas id="c"></canvas>
|
|
<script>
|
|
const canvas = document.getElementById('c');
|
|
const ctx = canvas.getContext('2d');
|
|
const tilesInput = document.getElementById('tiles');
|
|
const animateInput = document.getElementById('animate');
|
|
const dprInput = document.getElementById('dpr');
|
|
const fpsEl = document.getElementById('fps');
|
|
|
|
let tiles = +tilesInput.value;
|
|
let animate = animateInput.checked;
|
|
let dpr = +dprInput.value;
|
|
|
|
const state = [];
|
|
function seed() {
|
|
state.length = 0;
|
|
const today = new Date();
|
|
for (let i = 0; i < tiles; i++) {
|
|
const hue = (i * 11) % 360;
|
|
const net = Math.sin(i) * 2; // -2..2
|
|
state.push({ hue, net, pos: (i % 5), neg: (i % 3) });
|
|
}
|
|
}
|
|
|
|
function resize() {
|
|
const rect = canvas.getBoundingClientRect();
|
|
canvas.width = Math.floor(rect.width * dpr);
|
|
canvas.height = Math.floor(rect.height * dpr);
|
|
ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
|
|
}
|
|
|
|
function draw(time) {
|
|
ctx.clearRect(0, 0, canvas.width, canvas.height);
|
|
const padding = 8, gap = 2;
|
|
const cols = Math.ceil(Math.sqrt(tiles));
|
|
const rows = Math.ceil(tiles / cols);
|
|
const tileSize = Math.min((canvas.width/dpr - padding*2 - gap*(cols-1)) / cols,
|
|
(canvas.height/dpr - padding*2 - gap*(rows-1)) / rows);
|
|
let x = padding, y = padding;
|
|
for (let i = 0; i < tiles; i++) {
|
|
const s = state[i];
|
|
const glow = Math.max(0, Math.min(1, (s.net + (animate ? 0.5*Math.sin(time/500 + i) : 0)) / 3));
|
|
const lum = 20 + glow * 65; // luminance range 20..85
|
|
ctx.fillStyle = `hsl(${s.hue}deg 80% ${lum}%)`;
|
|
ctx.fillRect(x, y, tileSize, tileSize);
|
|
// static overlay for negative
|
|
if (s.neg > 0) {
|
|
ctx.globalAlpha = 0.08 + 0.03 * s.neg;
|
|
for (let n = 0; n < 10; n++) {
|
|
const rx = x + Math.random() * tileSize;
|
|
const ry = y + Math.random() * tileSize;
|
|
ctx.fillStyle = '#000000';
|
|
ctx.fillRect(rx, ry, 1, 1);
|
|
}
|
|
ctx.globalAlpha = 1;
|
|
}
|
|
// glyphs
|
|
ctx.fillStyle = '#fff';
|
|
for (let p = 0; p < s.pos; p++) {
|
|
ctx.fillRect(x + 2 + p*3, y + tileSize - 4, 2, 2);
|
|
}
|
|
ctx.fillStyle = '#bbb';
|
|
for (let n = 0; n < s.neg; n++) {
|
|
ctx.fillRect(x + tileSize - 2 - n*3, y + tileSize - 4, 2, 2);
|
|
}
|
|
x += tileSize + gap;
|
|
if ((i+1) % cols === 0) { x = padding; y += tileSize + gap; }
|
|
}
|
|
}
|
|
|
|
let last = performance.now();
|
|
let frames = 0;
|
|
const report = { runs: [] };
|
|
function loop(ts) {
|
|
draw(ts);
|
|
frames++;
|
|
if (ts - last > 1000) {
|
|
fpsEl.textContent = `FPS: ${frames}`;
|
|
frames = 0; last = ts;
|
|
}
|
|
requestAnimationFrame(loop);
|
|
}
|
|
|
|
tilesInput.oninput = () => { tiles = +tilesInput.value; seed(); };
|
|
animateInput.onchange = () => { animate = animateInput.checked; };
|
|
dprInput.oninput = () => { dpr = +dprInput.value; resize(); };
|
|
|
|
window.addEventListener('resize', resize);
|
|
seed(); resize(); requestAnimationFrame(loop);
|
|
|
|
async function runOnce(tCount){
|
|
tiles = tCount; tilesInput.value = tCount; seed();
|
|
return new Promise((resolve) => {
|
|
let f = 0; const tStart = performance.now();
|
|
const orig = requestAnimationFrame;
|
|
function sample(ts){
|
|
f++;
|
|
if (ts - tStart >= 1500) {
|
|
resolve({ tiles: tCount, fps: Math.round((f*1000)/(ts - tStart)) });
|
|
} else { orig(sample); }
|
|
}
|
|
orig(sample);
|
|
});
|
|
}
|
|
|
|
document.getElementById('runBench').onclick = async () => {
|
|
const dprPrev = dpr; const animPrev = animate;
|
|
const dprs = [1, 2];
|
|
const counts = [30, 90, 180, 365];
|
|
const results = [];
|
|
for (const d of dprs) {
|
|
dpr = d; dprInput.value = d; resize();
|
|
for (const a of [false, true]) {
|
|
animate = a; animateInput.checked = a;
|
|
for (const c of counts) {
|
|
const r = await runOnce(c);
|
|
results.push({ dpr: d, animate: a, ...r });
|
|
}
|
|
}
|
|
}
|
|
dpr = dprPrev; dprInput.value = dprPrev; animate = animPrev; animateInput.checked = animPrev; resize();
|
|
report.runs.push({ date: new Date().toISOString(), results });
|
|
alert('Benchmark complete');
|
|
};
|
|
|
|
document.getElementById('download').onclick = () => {
|
|
const blob = new Blob([JSON.stringify(report, null, 2)], { type: 'application/json' });
|
|
const url = URL.createObjectURL(blob);
|
|
const a = document.createElement('a');
|
|
a.href = url; a.download = 'viz-benchmark.json'; a.click();
|
|
URL.revokeObjectURL(url);
|
|
};
|
|
</script>
|
|
</body>
|
|
</html>
|