73 lines
2.8 KiB
TypeScript
73 lines
2.8 KiB
TypeScript
import { content } from "@deadhead/content";
|
||
import { describe, expect, it } from "vitest";
|
||
import { DEFAULT_CAMERA, pan, toScreen, zoomAt } from "../src/map/camera";
|
||
import { locationMapPoint, radialScale, toMapPoint } from "../src/map/projection";
|
||
|
||
describe("radial scale", () => {
|
||
it("is monotonic: farther in reality is farther on screen", () => {
|
||
let previous = radialScale(0);
|
||
for (let km = 0.5; km <= 1200; km += 0.5) {
|
||
const next = radialScale(km);
|
||
expect(next).toBeGreaterThan(previous);
|
||
previous = next;
|
||
}
|
||
});
|
||
|
||
it("is compressed: Jupiter is much less than 5× Earth's distance on screen", () => {
|
||
expect(radialScale(780) / radialScale(150)).toBeLessThan(2.5);
|
||
expect(radialScale(1000)).toBe(1);
|
||
});
|
||
|
||
it("keeps a position's direction from the Sun", () => {
|
||
const p = toMapPoint({ x: 300, y: 400 });
|
||
expect(Math.atan2(-p.y, p.x)).toBeCloseTo(Math.atan2(400, 300), 10);
|
||
expect(Math.hypot(p.x, p.y)).toBeCloseTo(radialScale(500), 10);
|
||
});
|
||
});
|
||
|
||
describe("map fits a phone", () => {
|
||
// A 375 px wide phone, minus header and tab bar.
|
||
const viewport = { width: 375, height: 690 };
|
||
|
||
it("shows all five locations on screen at default zoom, across 12 years", () => {
|
||
for (let month = 0; month < 12 * 12; month++) {
|
||
for (const location of content.locations) {
|
||
const p = toScreen(locationMapPoint(location), viewport, DEFAULT_CAMERA);
|
||
expect(p.x).toBeGreaterThanOrEqual(0);
|
||
expect(p.x).toBeLessThanOrEqual(viewport.width);
|
||
expect(p.y).toBeGreaterThanOrEqual(0);
|
||
expect(p.y).toBeLessThanOrEqual(viewport.height);
|
||
}
|
||
}
|
||
});
|
||
|
||
it("draws the Moon apart from Earth", () => {
|
||
const earth = locationMapPoint(content.location("earth")!);
|
||
const moon = locationMapPoint(content.location("moon")!);
|
||
const a = toScreen(earth, viewport, DEFAULT_CAMERA);
|
||
const b = toScreen(moon, viewport, DEFAULT_CAMERA);
|
||
expect(Math.hypot(a.x - b.x, a.y - b.y)).toBeGreaterThan(5);
|
||
});
|
||
});
|
||
|
||
describe("camera", () => {
|
||
const viewport = { width: 400, height: 600 };
|
||
|
||
it("keeps the map point under the fingers fixed while zooming", () => {
|
||
const anchor = { x: 300, y: 150 };
|
||
const camera = pan(DEFAULT_CAMERA, 20, -40);
|
||
const point = { x: 0.37, y: -0.21 };
|
||
const before = toScreen(point, viewport, camera);
|
||
const zoomed = zoomAt(camera, 3, anchor, viewport);
|
||
const after = toScreen(point, viewport, zoomed);
|
||
// Distance from the anchor scales by the zoom factor; the anchor itself stays put.
|
||
expect(after.x - anchor.x).toBeCloseTo((before.x - anchor.x) * 3, 6);
|
||
expect(after.y - anchor.y).toBeCloseTo((before.y - anchor.y) * 3, 6);
|
||
});
|
||
|
||
it("clamps zoom", () => {
|
||
expect(zoomAt(DEFAULT_CAMERA, 1000, { x: 0, y: 0 }, viewport).zoom).toBe(40);
|
||
expect(zoomAt(DEFAULT_CAMERA, 0.0001, { x: 0, y: 0 }, viewport).zoom).toBe(0.5);
|
||
});
|
||
});
|