source: Klonkt/test/ward-sky.test.js@ 5218863

Last change on this file since 5218863 was 084b046, checked in by Robin <roboburr@…>, 3 months ago

De Ward PWA: de avatarhemel achter de kaarten

Plak 3. Robins correctie: niet de hemel IN PLAATS VAN de kaarten, maar
allebei, met de hemel er altijd achter. Zelfde beweging als in Shaer: de
kaarten vallen weg en wat eronder lag was er al.

MODEL EN RENDERER APART, en die naad is het punt. orbitPlacement en
nearestFace zijn pure wiskunde en bestaan nu drie keer (Swift, Kotlin,
hier). Drie keer kans op drift, en drift zie je niet aan een screenshot:
je ziet het als gezichten ergens anders hangen dan in de app. Daarom in
een eigen module, met dezelfde eigenschappen bewaakt als AvatarSkyTest.kt.

CANVAS, GEEN ENGINE, en dat is een afweging en geen luiheid. De rem is
niet het aantal sprites maar het aantal texturen: alle avatars gaan een
keer in een atlas, daarna is elk gezicht een drawImage uit datzelfde
plaatje. Dat haalt duizenden. Wil het ooit meer, dan vervangt een
WebGL-variant alleen SkyRenderer en merkt de rest er niets van. De echte
schaaloplossing is trouwens niet sneller tekenen maar clusteren; dat
staat als bead voor later.

New file:
src/assets/js/ward-sky.js

  • orbitPlacement + nearestFace, letterlijke port
  • SkyRenderer: atlas, ademhaling, pan, zoom, tik

test/ward-sky.test.js

  • vers dichterbij dan oud, deterministisch, geen overlap, mikken kiest de dichtstbijzijnde en schaalt mee met de zoom

Changed files:
src/routes/ward.js

  • /api/conversations: per tegenpartij het jongste bericht
  • /sky.js als eigen asset

src/views/pages/ward.ejs

  • canvas achter alles, wisselknop, app.js als module

src/assets/js/ward.js

  • de wissel: kaarten of hemel, hemel draait altijd

src/assets/css/ward.css

  • hemel op z-index 0, kaarten erboven

src/services/i18n.js

  • drie sleutels in nl, en, de

remarks: 895 tests groen. Een tik op een gezicht wijst naar /ward/thread,
en die route bestaat nog niet; dat is de volgende plak.

-robo
Co-Authored-By: Claude Opus 4.8 <noreply@…>

  • Property mode set to 100644
File size: 3.1 KB
Line 
1// De avatarhemel: het MODEL, niet de tekening.
2//
3// Deze wiskunde bestaat nu drie keer: Swift, Kotlin en hier. Dat is drie keer
4// kans op drift, en drift zie je niet aan een screenshot — je ziet het als
5// gezichten op een andere plek hangen dan in de app. Daarom staan hier
6// dezelfde eigenschappen bewaakt als in AvatarSkyTest.kt: vers hangt dichterbij
7// dan oud, dezelfde invoer geeft dezelfde uitkomst, en niemand overlapt.
8import { test } from 'node:test';
9import assert from 'node:assert/strict';
10
11const { orbitPlacement, nearestFace } = await import('../src/assets/js/ward-sky.js');
12
13const r = (s) => Math.hypot(s.x, s.y);
14
15test('vers hangt dichter bij het midden dan oud', () => {
16 // De afstand tot het midden IS de leeftijd van het laatste bericht. Zou dat
17 // omdraaien, dan leest de hele hemel verkeerd.
18 const spots = orbitPlacement([0, 3600000, 86400000, 30 * 86400000]);
19 assert.ok(r(spots[0]) < r(spots[3]), 'het verste gezicht was niet het oudste');
20});
21
22test('dezelfde invoer geeft dezelfde uitkomst', () => {
23 // Zonder dit springt de hemel bij elke verversing door elkaar en herken je
24 // niemand meer aan zijn plek.
25 const ages = Array.from({ length: 40 }, (_, i) => i * 7200000);
26 assert.deepEqual(orbitPlacement(ages), orbitPlacement(ages));
27});
28
29test('niemand overlapt, ook niet bij gelijke leeftijd', () => {
30 // Twintig gesprekken van exact even oud: zonder de push-apart vallen die op
31 // één punt samen en zie je één gezicht in plaats van twintig.
32 const spots = orbitPlacement(Array(20).fill(3600000));
33 for (let i = 0; i < spots.length; i++) {
34 for (let j = i + 1; j < spots.length; j++) {
35 const d = Math.hypot(spots[j].x - spots[i].x, spots[j].y - spots[i].y);
36 assert.ok(d > 1.0, `gezicht ${i} en ${j} liggen op elkaar (${d.toFixed(3)})`);
37 }
38 }
39});
40
41test('leeg blijft leeg, en één gezicht staat niet in het midden', () => {
42 assert.deepEqual(orbitPlacement([]), []);
43 // Het midden is de deur naar iemand nieuw; daar hoort geen gezicht te staan.
44 const [solo] = orbitPlacement([0]);
45 assert.ok(r(solo) >= 1.4, 'het enige gezicht stond bovenop de (+)');
46});
47
48test('mikken kiest het dichtstbijzijnde gezicht, of niets', () => {
49 const spots = orbitPlacement([0, 86400000, 30 * 86400000]);
50 const view = { cx: 200, cy: 200, cell: 92, scale: 1, panX: 0, panY: 0 };
51 // Precies op het eerste gezicht mikken.
52 const p = { x: view.cx + spots[0].x * 92, y: view.cy + spots[0].y * 92 };
53 assert.equal(nearestFace(p.x, p.y, spots, view), 0);
54 // Ver ernaast is GEEN treffer: liever niets openen dan de verkeerde persoon.
55 assert.equal(nearestFace(-500, -500, spots, view), null);
56});
57
58test('de treffer schaalt mee met de zoom', () => {
59 // Uitgezoomd liggen de gezichten dichter bij elkaar; het mikpunt moet dan
60 // meebewegen, anders open je bij het uitzoomen steeds de verkeerde.
61 const spots = orbitPlacement([0, 3600000, 7200000]);
62 const at = (scale) => {
63 const v = { cx: 300, cy: 300, cell: 92, scale, panX: 0, panY: 0 };
64 return nearestFace(v.cx + spots[1].x * 92 * scale, v.cy + spots[1].y * 92 * scale, spots, v);
65 };
66 assert.equal(at(1), 1);
67 assert.equal(at(0.6), 1);
68 assert.equal(at(1.5), 1);
69});
Note: See TracBrowser for help on using the repository browser.