// De avatarhemel: het MODEL, niet de tekening. // // Deze wiskunde bestaat nu drie keer: Swift, Kotlin en hier. Dat is drie keer // kans op drift, en drift zie je niet aan een screenshot — je ziet het als // gezichten op een andere plek hangen dan in de app. Daarom staan hier // dezelfde eigenschappen bewaakt als in AvatarSkyTest.kt: vers hangt dichterbij // dan oud, dezelfde invoer geeft dezelfde uitkomst, en niemand overlapt. import { test } from 'node:test'; import assert from 'node:assert/strict'; const { orbitPlacement, nearestFace, SkyRenderer } = await import('../src/assets/js/ward-sky.js'); const r = (s) => Math.hypot(s.x, s.y); test('vers hangt dichter bij het midden dan oud', () => { // De afstand tot het midden IS de leeftijd van het laatste bericht. Zou dat // omdraaien, dan leest de hele hemel verkeerd. const spots = orbitPlacement([0, 3600000, 86400000, 30 * 86400000]); assert.ok(r(spots[0]) < r(spots[3]), 'het verste gezicht was niet het oudste'); }); test('dezelfde invoer geeft dezelfde uitkomst', () => { // Zonder dit springt de hemel bij elke verversing door elkaar en herken je // niemand meer aan zijn plek. const ages = Array.from({ length: 40 }, (_, i) => i * 7200000); assert.deepEqual(orbitPlacement(ages), orbitPlacement(ages)); }); test('niemand overlapt, ook niet bij gelijke leeftijd', () => { // Twintig gesprekken van exact even oud: zonder de push-apart vallen die op // één punt samen en zie je één gezicht in plaats van twintig. const spots = orbitPlacement(Array(20).fill(3600000)); for (let i = 0; i < spots.length; i++) { for (let j = i + 1; j < spots.length; j++) { const d = Math.hypot(spots[j].x - spots[i].x, spots[j].y - spots[i].y); assert.ok(d > 1.0, `gezicht ${i} en ${j} liggen op elkaar (${d.toFixed(3)})`); } } }); test('leeg blijft leeg, en één gezicht staat niet in het midden', () => { assert.deepEqual(orbitPlacement([]), []); // Het midden is de deur naar iemand nieuw; daar hoort geen gezicht te staan. const [solo] = orbitPlacement([0]); assert.ok(r(solo) >= 1.4, 'het enige gezicht stond bovenop de (+)'); }); test('mikken kiest het dichtstbijzijnde gezicht, of niets', () => { const spots = orbitPlacement([0, 86400000, 30 * 86400000]); const view = { cx: 200, cy: 200, cell: 92, scale: 1, panX: 0, panY: 0 }; // Precies op het eerste gezicht mikken. const p = { x: view.cx + spots[0].x * 92, y: view.cy + spots[0].y * 92 }; assert.equal(nearestFace(p.x, p.y, spots, view), 0); // Ver ernaast is GEEN treffer: liever niets openen dan de verkeerde persoon. assert.equal(nearestFace(-500, -500, spots, view), null); }); test('de treffer schaalt mee met de zoom', () => { // Uitgezoomd liggen de gezichten dichter bij elkaar; het mikpunt moet dan // meebewegen, anders open je bij het uitzoomen steeds de verkeerde. const spots = orbitPlacement([0, 3600000, 7200000]); const at = (scale) => { const v = { cx: 300, cy: 300, cell: 92, scale, panX: 0, panY: 0 }; return nearestFace(v.cx + spots[1].x * 92 * scale, v.cy + spots[1].y * 92 * scale, spots, v); }; assert.equal(at(1), 1); assert.equal(at(0.6), 1); assert.equal(at(1.5), 1); }); test('zoomen houdt het punt onder je vingers op zijn plek', () => { // De kern van pinch-to-zoom: de wereld onder het midden tussen je vingers // mag NIET wegschuiven. We rekenen _zoomAround na zonder een echt canvas, // door de renderer een minimale staat te geven. // We hebben geen DOM, dus bouwen een kaal object met alleen wat _zoomAround // aanraakt en lenen de methode. const fake = { w: 400, h: 400, scale: 1, panX: 0, panY: 0, _clampScale: SkyRenderer.prototype._clampScale }; const zoom = SkyRenderer.prototype._zoomAround; // Welk wereldpunt ligt nu onder (px,py)? const worldUnder = (o, px, py) => ({ x: (px - o.w / 2 - o.panX) / o.scale, y: (py - o.h / 2 - o.panY) / o.scale, }); const px = 300, py = 120; const before = worldUnder(fake, px, py); zoom.call(fake, px, py, 1.6); // inzoomen om dat punt const after = worldUnder(fake, px, py); assert.ok(Math.abs(before.x - after.x) < 1e-6, 'x schoof weg onder de vingers'); assert.ok(Math.abs(before.y - after.y) < 1e-6, 'y schoof weg onder de vingers'); assert.ok(fake.scale > 1, 'er is niet ingezoomd'); }); test('de zoom blijft binnen zijn grenzen', () => { const clamp = SkyRenderer.prototype._clampScale; assert.equal(clamp(10), 2.5); // niet eindeloos in assert.equal(clamp(0.01), 0.2); // niet eindeloos uit });