Files
GlowTrack/packages/viz/poc/index.html
Danilo Reyes 2f096d0265 feat: Add GlowTrack mood and habit wellbeing grid specifications
- 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.
2025-09-18 00:36:13 -06:00

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>