SUNDIAL solar dial v0.3: round screen, NOAA solar math, day/night arc

- 466px round layout, designWidth 466 (S4 41mm 466x466)
- solar position + dayProgress in src/common/solar.js (pure, TDD'd)
- sunrise/sunset sweep, night moon, countdown, 30s refresh
- tests: solar math, page logic, circle geometry/contrast
This commit is contained in:
2026-09-09 15:09:14 +01:00
commit 1a3a2566e3
16 changed files with 10991 additions and 0 deletions
+34
View File
@@ -0,0 +1,34 @@
// Circle geometry: sun marker orbit, horizon line, center text block — all inside 466px circle.
const fs = require('node:fs');
const assert = require('node:assert/strict');
const ux = fs.readFileSync('src/pages/index/index.ux', 'utf8');
const num = (re) => { const m = ux.match(re); assert.ok(m, 'missing: '+re); return Number(m[1]); };
const SCREEN = 466, R = 233;
const skyD = num(/\.sky\s*{[^}]*width:\s*(\d+)px/);
assert.ok(skyD <= SCREEN - 40, 'sky dial should leave bezel margin: ' + skyD);
// horizon line spans within circle at y=233 (widest point) → width <= 466
const hzW = num(/\.horizon\s*{[^}]*width:\s*(\d+)px/);
assert.ok(hzW <= SCREEN, 'horizon width fits');
// Actual template-bound coordinates are exercised in marker.cjs.
const orbit = num(/orbitR:\s*(\d+)/);
assert.ok(orbit <= skyD / 2 - 30, `sun orbit ${orbit} must stay within sky dial ${skyD/2} minus marker radius`);
// center text block vertical span inside circle
const cTop = num(/\.center\s*{[^}]*top:\s*(\d+)px/);
const bigH = num(/\.big\s*{[^}]*height:\s*(\d+)px/);
const phaseH = num(/\.phase\s*{[^}]*height:\s*(\d+)px/);
const countH = num(/\.count\s*{[^}]*height:\s*(\d+)px/);
const cBot = cTop + bigH + phaseH + countH;
// text content is centered and narrow (~240px wide worst case = clock at 66px font); check half-width 120px fits chord
const TEXT_HALF = 120;
for (const y of [cTop, cBot]) {
const halfW = Math.sqrt(Math.max(0, R*R - Math.abs(y-R)**2));
assert.ok(TEXT_HALF <= halfW, `center text at y=${y} exceeds chord: needs ${TEXT_HALF}, has ${halfW.toFixed(0)}`);
}
// contrast
const lum = h => { const c=h.match(/\w\w/g).map(x=>parseInt(x,16)/255).map(v=>v<=0.03928?v/12.92:((v+0.055)/1.055)**2.4); return .2126*c[0]+.7152*c[1]+.0722*c[2]; };
const ratio = (a,b)=>(Math.max(lum(a),lum(b))+.05)/(Math.min(lum(a),lum(b))+.05);
assert.ok(ratio('#ffffff','#000000') >= 4.5, 'clock contrast');
assert.ok(ratio('#f3a633','#000000') >= 4.5, 'phase contrast');
assert.ok(ratio('#9aa0aa','#000000') >= 4.5, 'count contrast');
console.log('GEOMETRY ESTIMATES PASS (not renderer validation): dial', skyD, 'orbit', orbit, 'center block y', cTop, '-', cBot);