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

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

Ward-PWA: echte pinch-to-zoom op de avatarhemel

De hemel deed zoom met het muiswiel, maar de belofte 'knijpen op een
telefoon' in de comment was nog niet waar: er stond alleen wielcode en
een ongebruikte pinch-variabele. Nu echt twee-vinger pinch.

Pointers in een Map: één vinger pant of tikt, twee vingers knijpen. De
schaal volgt de verhouding van de vingerafstand, en we zoomen ROND het
midden tussen de vingers, niet rond het canvas-midden. Dat is het
verschil tussen een kaart en beeld dat onder je weg schuift: het
wereldpunt onder je vingers blijft staan. Het wiel op desktop gebruikt
nu dezelfde weg, om de muis.

Zorgvuldig met de tik: pas een tik als er NOOIT twee vingers lagen en de
ene vinger nauwelijks bewoog. Anders opent het einde van een knijp per
ongeluk een gesprek. En bij het loslaten van de tweede vinger wordt de
afstand opnieuw geijkt, zodat de overgebleven vinger niet met een sprong
verder pant. touch-action:none stond al aan in berichtenstand, dus de
browser laat de pinch aan ons.

Changed files:
src/assets/js/ward-sky.js

  • _zoomAround(px,py,factor): zoom om een schermpunt
  • _clampScale: de grenzen op één plek
  • _wire herschreven: multi-pointer pan/pinch/tik

test/ward-sky.test.js

  • zoomen houdt het punt onder je vingers op zijn plek; de zoom blijft binnen zijn grenzen

remarks: 908 tests groen. De rekenkant is getest; het gebaar zelf met
twee echte vingers is aan Robin op dev.

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

  • Property mode set to 100644
File size: 4.5 KB
RevLine 
[084b046]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
[3866994]11const { orbitPlacement, nearestFace, SkyRenderer } = await import('../src/assets/js/ward-sky.js');
[084b046]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});
[3866994]70
71test('zoomen houdt het punt onder je vingers op zijn plek', () => {
72 // De kern van pinch-to-zoom: de wereld onder het midden tussen je vingers
73 // mag NIET wegschuiven. We rekenen _zoomAround na zonder een echt canvas,
74 // door de renderer een minimale staat te geven.
75 // We hebben geen DOM, dus bouwen een kaal object met alleen wat _zoomAround
76 // aanraakt en lenen de methode.
77 const fake = { w: 400, h: 400, scale: 1, panX: 0, panY: 0,
78 _clampScale: SkyRenderer.prototype._clampScale };
79 const zoom = SkyRenderer.prototype._zoomAround;
80
81 // Welk wereldpunt ligt nu onder (px,py)?
82 const worldUnder = (o, px, py) => ({
83 x: (px - o.w / 2 - o.panX) / o.scale,
84 y: (py - o.h / 2 - o.panY) / o.scale,
85 });
86 const px = 300, py = 120;
87 const before = worldUnder(fake, px, py);
88 zoom.call(fake, px, py, 1.6); // inzoomen om dat punt
89 const after = worldUnder(fake, px, py);
90
91 assert.ok(Math.abs(before.x - after.x) < 1e-6, 'x schoof weg onder de vingers');
92 assert.ok(Math.abs(before.y - after.y) < 1e-6, 'y schoof weg onder de vingers');
93 assert.ok(fake.scale > 1, 'er is niet ingezoomd');
94});
95
96test('de zoom blijft binnen zijn grenzen', () => {
97 const clamp = SkyRenderer.prototype._clampScale;
98 assert.equal(clamp(10), 2.5); // niet eindeloos in
99 assert.equal(clamp(0.01), 0.2); // niet eindeloos uit
100});
Note: See TracBrowser for help on using the repository browser.