Changeset 3866994 in Klonkt for src/assets/js


Ignore:
Timestamp:
08/12/2026 10:00:38 PM (3 months ago)
Author:
Robin <roboburr@…>
Parents:
5b6f93b
Message:

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@…>

File:
1 edited

Legend:

Unmodified
Added
Removed
  • src/assets/js/ward-sky.js

    r5b6f93b r3866994  
    254254  }
    255255
     256  /** De schaal binnen zijn grenzen. Eén plek, zodat wiel en pinch gelijk zijn. */
     257  _clampScale(s) { return Math.min(Math.max(s, 0.2), 2.5); }
     258
     259  /**
     260   * Zoomen ROND een schermpunt, niet rond het canvas-midden: de wereld onder
     261   * je vingers (of je muis) blijft staan, zodat de hemel zich als een kaart
     262   * gedraagt in plaats van onder je weg te schuiven.
     263   */
     264  _zoomAround(px, py, factor) {
     265    const next = this._clampScale(this.scale * factor);
     266    const cx = this.w / 2, cy = this.h / 2;
     267    // Het wereldpunt onder (px,py) mag na de zoom op dezelfde plek blijven.
     268    const worldX = (px - cx - this.panX) / this.scale;
     269    const worldY = (py - cy - this.panY) / this.scale;
     270    this.panX = px - cx - worldX * next;
     271    this.panY = py - cy - worldY * next;
     272    this.scale = next;
     273  }
     274
    256275  _wire() {
    257276    const c = this.canvas;
    258     let dragging = false, lastX = 0, lastY = 0, moved = 0;
    259     let pinch = null;
     277    // Alle vingers die nu op de hemel liggen. Eén = pannen of tikken, twee =
     278    // knijpen. Een Map want pointers komen en gaan in willekeurige volgorde.
     279    const pts = new Map();
     280    let moved = 0;                 // hoeveel de ENE vinger bewoog (tik vs sleep)
     281    let pinchDist = 0;             // vingerafstand bij het begin van een knijp
     282    let becamePinch = false;       // ooit twee vingers? dan geen tik aan het eind
     283
     284    const local = (e) => {
     285      const r = c.getBoundingClientRect();
     286      return { x: e.clientX - r.left, y: e.clientY - r.top };
     287    };
     288    const twoPointerState = () => {
     289      const [a, b] = [...pts.values()];
     290      return {
     291        dist: Math.hypot(a.x - b.x, a.y - b.y),
     292        mx: (a.x + b.x) / 2, my: (a.y + b.y) / 2,
     293      };
     294    };
    260295
    261296    c.addEventListener('pointerdown', (e) => {
    262297      if (!this.interactive) return;
    263       dragging = true; moved = 0;
    264       lastX = e.clientX; lastY = e.clientY;
    265298      c.setPointerCapture(e.pointerId);
     299      pts.set(e.pointerId, local(e));
     300      moved = 0;
     301      if (pts.size === 2) { becamePinch = true; pinchDist = twoPointerState().dist; }
    266302    });
     303
    267304    c.addEventListener('pointermove', (e) => {
    268       if (!this.interactive || !dragging) return;
    269       const dx = e.clientX - lastX, dy = e.clientY - lastY;
    270       moved += Math.abs(dx) + Math.abs(dy);
    271       this.panX += dx; this.panY += dy;
    272       lastX = e.clientX; lastY = e.clientY;
     305      if (!this.interactive || !pts.has(e.pointerId)) return;
     306      const prev = pts.get(e.pointerId);
     307      const cur = local(e);
     308      pts.set(e.pointerId, cur);
     309
     310      if (pts.size >= 2) {
     311        // Knijpen: schaal volgt de verhouding van de vingerafstand, en we
     312        // zoomen om het midden tussen de vingers.
     313        const { dist, mx, my } = twoPointerState();
     314        if (pinchDist > 0 && dist > 0) {
     315          this._zoomAround(mx, my, dist / pinchDist);
     316          pinchDist = dist;
     317        }
     318      } else {
     319        // Eén vinger: pannen.
     320        const dx = cur.x - prev.x, dy = cur.y - prev.y;
     321        moved += Math.abs(dx) + Math.abs(dy);
     322        this.panX += dx; this.panY += dy;
     323      }
    273324    });
    274     const end = (e) => {
    275       if (!this.interactive || !dragging) return;
    276       dragging = false;
    277       // Nauwelijks bewogen? Dan was het een tik en geen sleep.
    278       if (moved < 6) {
    279         const r = c.getBoundingClientRect();
    280         const x = e.clientX - r.left;
    281         const y = e.clientY - r.top;
     325
     326    const lift = (e) => {
     327      if (!pts.has(e.pointerId)) return;
     328      const cur = pts.get(e.pointerId);
     329      pts.delete(e.pointerId);
     330      // Nog één vinger over na een knijp: zijn afstand als nieuw ijkpunt, en
     331      // niet meteen als tik tellen.
     332      if (pts.size === 1) { pinchDist = 0; return; }
     333      if (pts.size > 1) return;
     334
     335      // Laatste vinger los. Een tik alleen als er nooit geknepen is en de
     336      // vinger nauwelijks bewoog.
     337      if (!becamePinch && moved < 6) {
     338        const { x, y } = cur;
    282339        const hit = nearestFace(x, y, this.spots, {
    283340          cx: this.w / 2, cy: this.h / 2, cell: CELL, scale: this.scale,
    284341          panX: this.panX, panY: this.panY,
    285342        });
    286         if (hit != null) { this.onPick?.(hit); return; }
    287         // Geen gezicht? Dan misschien de (+): zelfde middelpunt als getekend,
    288         // met een raakzone die meeschaalt zodat mikken bij elke zoom klopt.
    289         const s2 = Math.min(Math.max(this.scale, 0.5), 1.6);
    290         const d = Math.hypot(x - (this.w / 2 + this.panX), y - (this.h / 2 + this.panY));
    291         if (d <= 26 * s2) this.onNew?.();
     343        if (hit != null) { this.onPick?.(hit); }
     344        else {
     345          // Geen gezicht? Dan misschien de (+): zelfde middelpunt als getekend,
     346          // met een raakzone die meeschaalt zodat mikken bij elke zoom klopt.
     347          const s2 = Math.min(Math.max(this.scale, 0.5), 1.6);
     348          const d = Math.hypot(x - (this.w / 2 + this.panX), y - (this.h / 2 + this.panY));
     349          if (d <= 26 * s2) this.onNew?.();
     350        }
    292351      }
    293     };
    294     c.addEventListener('pointerup', end);
    295     c.addEventListener('pointercancel', () => { dragging = false; });
    296 
    297     // Knijpen op een telefoon, wiel op een desktop; allebei zoomen om het
    298     // midden, zodat de hemel zich als een kaart gedraagt.
     352      becamePinch = false;
     353    };
     354    c.addEventListener('pointerup', lift);
     355    c.addEventListener('pointercancel', (e) => { pts.delete(e.pointerId); if (pts.size < 2) pinchDist = 0; });
     356
     357    // Het wiel op een desktop: zoomen om de muis, dezelfde weg als de pinch.
    299358    c.addEventListener('wheel', (e) => {
    300359      if (!this.interactive) return;
    301360      e.preventDefault();
    302       this.scale = Math.min(Math.max(this.scale * (e.deltaY < 0 ? 1.1 : 0.9), 0.2), 2.5);
     361      const { x, y } = local(e);
     362      this._zoomAround(x, y, e.deltaY < 0 ? 1.1 : 0.9);
    303363    }, { passive: false });
    304364
Note: See TracChangeset for help on using the changeset viewer.