SpaceCities / test /camera.test.js
Claude
Touch / mobile controls — play the whole game by finger
e7bc56d unverified
Raw History Blame Contribute Delete
4.33 kB
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");
});