File size: 4,325 Bytes
e7bc56d
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
import { test } from "node:test";
import assert from "node:assert/strict";
import {
  createCamera, screenToWorld, worldToScreen, clampCamera, zoomAt,
  dragCamera, pinchZoomPan, MIN_ZOOM, MAX_ZOOM,
} from "../camera.js";

const MAP = { width: 1600, height: 1000 };
const VW = 800, VH = 500;

test("screenToWorld and worldToScreen are inverses", () => {
  const cam = { x: 700, y: 420, zoom: 1.4 };
  const w = screenToWorld(cam, VW, VH, 123, 456);
  const s = worldToScreen(cam, VW, VH, w.x, w.y);
  assert.ok(Math.abs(s.x - 123) < 1e-9 && Math.abs(s.y - 456) < 1e-9, "round-trips back to the same screen point");
  // The view centre maps to the camera's world focus.
  const c = screenToWorld(cam, VW, VH, VW / 2, VH / 2);
  assert.ok(Math.abs(c.x - cam.x) < 1e-9 && Math.abs(c.y - cam.y) < 1e-9);
});

test("clampCamera keeps the view inside the map, and centres an axis smaller than the viewport", () => {
  const cam = { x: -500, y: 5000, zoom: 1 };   // way off both edges
  clampCamera(cam, MAP, VW, VH);
  assert.ok(cam.x >= VW / 2 && cam.x <= MAP.width - VW / 2, "x is pulled back inside the horizontal range");
  assert.ok(cam.y >= VH / 2 && cam.y <= MAP.height - VH / 2, "y is pulled back inside the vertical range");

  // Zoomed far out so the viewport is wider than the map -> that axis centres.
  const wide = { x: 10, y: 10, zoom: 0.1 };
  clampCamera(wide, MAP, VW, VH);
  assert.equal(wide.x, MAP.width / 2, "an over-wide viewport centres x");
  assert.equal(wide.y, MAP.height / 2, "an over-tall viewport centres y");
});

test("zoomAt keeps the world point under the cursor fixed and respects the zoom limits", () => {
  const cam = createCamera(MAP);
  const before = screenToWorld(cam, VW, VH, 600, 300);
  zoomAt(cam, MAP, VW, VH, 600, 300, 1.5);
  const after = screenToWorld(cam, VW, VH, 600, 300);
  assert.ok(Math.abs(before.x - after.x) < 1e-6 && Math.abs(before.y - after.y) < 1e-6, "the point under the cursor doesn't drift");
  assert.ok(cam.zoom > 1, "it zoomed in");

  for (let i = 0; i < 40; i++) zoomAt(cam, MAP, VW, VH, 400, 250, 2);
  assert.ok(cam.zoom <= MAX_ZOOM + 1e-9, "never past MAX_ZOOM");
  for (let i = 0; i < 40; i++) zoomAt(cam, MAP, VW, VH, 400, 250, 0.5);
  assert.ok(cam.zoom >= Math.min(MIN_ZOOM, VW / MAP.width, VH / MAP.height) - 1e-9, "never below the fit-the-map minimum");
});

test("dragCamera moves the world opposite the finger, scaled by zoom, and clamps", () => {
  const cam = { x: 800, y: 500, zoom: 2 };
  dragCamera(cam, MAP, VW, VH, 100, -40);   // fingers drag right/up
  assert.equal(cam.x, 800 - 100 / 2, "content follows the finger: +dx screen -> -dx/zoom world");
  assert.equal(cam.y, 500 - (-40) / 2);

  const edge = { x: MAP.width / 2, y: MAP.height / 2, zoom: 1 };
  dragCamera(edge, MAP, VW, VH, 100000, 0);   // shove hard past the edge
  assert.ok(edge.x <= MAP.width - VW / 2 + 1e-9, "a drag can't push the view off the map");
});

test("pinchZoomPan zooms by the spread ratio about the pinch midpoint", () => {
  const cam = { x: 800, y: 500, zoom: 1 };
  // Both fingers straddle the exact view centre and spread 2x apart, midpoint
  // fixed -> pure zoom-in of ~2x, and the world point at the centre stays put.
  const centreWorldBefore = screenToWorld(cam, VW, VH, VW / 2, VH / 2);
  const prev = { ax: VW / 2 - 50, ay: VH / 2, bx: VW / 2 + 50, by: VH / 2 };
  const cur  = { ax: VW / 2 - 100, ay: VH / 2, bx: VW / 2 + 100, by: VH / 2 };
  pinchZoomPan(cam, MAP, VW, VH, prev, cur);
  assert.ok(Math.abs(cam.zoom - 2) < 1e-6, "spread doubled -> zoom doubled");
  const centreWorldAfter = screenToWorld(cam, VW, VH, VW / 2, VH / 2);
  assert.ok(Math.abs(centreWorldBefore.x - centreWorldAfter.x) < 1e-6
    && Math.abs(centreWorldBefore.y - centreWorldAfter.y) < 1e-6, "the pinch midpoint's world point is anchored");
});

test("pinchZoomPan also pans when the two fingers translate together", () => {
  const cam = { x: 800, y: 500, zoom: 1 };
  // Same spread (no zoom), both fingers slide +60px in x -> a pure pan.
  const prev = { ax: 300, ay: 250, bx: 400, by: 250 };
  const cur  = { ax: 360, ay: 250, bx: 460, by: 250 };
  pinchZoomPan(cam, MAP, VW, VH, prev, cur);
  assert.ok(Math.abs(cam.zoom - 1) < 1e-9, "equal spread -> no zoom");
  assert.equal(cam.x, 800 - 60 / 1, "the view pans opposite the shared finger movement");
});