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.
This commit is contained in:
157
packages/viz/poc/index.html
Normal file
157
packages/viz/poc/index.html
Normal file
@@ -0,0 +1,157 @@
|
||||
<!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>
|
||||
Reference in New Issue
Block a user