Spaces:
Paused
Paused
File size: 4,811 Bytes
34ea8bc 19ca109 34ea8bc 19ca109 34ea8bc 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 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 | /* ============================================================
Camera: pans and zooms the view over the map instead of squeezing
the whole map into one screen. Pure view-space math — never reads or
mutates game state, just converts between screen (CSS pixel, origin
at the canvas's top-left) and world (map) coordinates.
============================================================ */
"use strict";
export const MIN_ZOOM = 0.6;
export const MAX_ZOOM = 2.5;
const PAN_SPEED = 900; // world units/sec of keyboard pan at zoom 1
// How far out the wheel can zoom: the usual MIN_ZOOM, but never so tight that
// a big map can't be pulled fully into view. On a Small map this is just
// MIN_ZOOM; on a Gigantic one it drops low enough to fit the whole thing.
function minZoomFor(map, vw, vh) {
if (!vw || !vh) return MIN_ZOOM;
return Math.min(MIN_ZOOM, vw / map.width, vh / map.height);
}
export function createCamera(map) {
return { x: map.width / 2, y: map.height / 2, zoom: 1 };
}
export function screenToWorld(camera, vw, vh, sx, sy) {
return {
x: camera.x + (sx - vw / 2) / camera.zoom,
y: camera.y + (sy - vh / 2) / camera.zoom,
};
}
export function worldToScreen(camera, vw, vh, wx, wy) {
return {
x: vw / 2 + (wx - camera.x) * camera.zoom,
y: vh / 2 + (wy - camera.y) * camera.zoom,
};
}
// Keeps the camera from panning past the map edge into empty space. Once
// the viewport is wider/taller than the map (zoomed out far enough, or a
// tiny window), that axis just centers instead of "clamping" to a
// backwards range.
export function clampCamera(camera, map, vw, vh) {
const halfW = vw / (2 * camera.zoom);
const halfH = vh / (2 * camera.zoom);
camera.x = map.width <= 2 * halfW ? map.width / 2 : Math.min(Math.max(camera.x, halfW), map.width - halfW);
camera.y = map.height <= 2 * halfH ? map.height / 2 : Math.min(Math.max(camera.y, halfH), map.height - halfH);
}
// Zooms by `factor` while keeping the world point under (sx, sy) fixed on
// screen, so scrolling the wheel feels like it's zooming toward the
// cursor instead of the map drifting out from under it.
export function zoomAt(camera, map, vw, vh, sx, sy, factor) {
const before = screenToWorld(camera, vw, vh, sx, sy);
camera.zoom = Math.min(MAX_ZOOM, Math.max(minZoomFor(map, vw, vh), camera.zoom * factor));
const after = screenToWorld(camera, vw, vh, sx, sy);
camera.x += before.x - after.x;
camera.y += before.y - after.y;
clampCamera(camera, map, vw, vh);
}
// dx/dy is a direction (not necessarily normalized) — e.g. from held
// keys — scaled by dt and the current zoom so panning covers the same
// screen distance per second regardless of zoom level.
export function panCamera(camera, map, vw, vh, dx, dy, dt) {
if (dx === 0 && dy === 0) return;
const len = Math.hypot(dx, dy);
camera.x += (dx / len) * PAN_SPEED * dt / camera.zoom;
camera.y += (dy / len) * PAN_SPEED * dt / camera.zoom;
clampCamera(camera, map, vw, vh);
}
// Drag the camera by a raw screen-space delta (touch two-finger pan, or a
// drag-pan): move the view so the world stays glued to the fingers. dsx/dsy is
// the finger movement in CSS pixels; dividing by zoom converts it to world
// units, and the sign is inverted because dragging content right (positive dsx)
// must decrease camera.x to bring the left-of-view world into frame.
export function dragCamera(camera, map, vw, vh, dsx, dsy) {
camera.x -= dsx / camera.zoom;
camera.y -= dsy / camera.zoom;
clampCamera(camera, map, vw, vh);
}
// One combined two-finger gesture step. `prev`/`cur` each hold the two touch
// points in screen space ({ ax, ay, bx, by }); this pinch-zooms by how their
// spread changed and pans by how their midpoint moved — with the zoom anchored
// on the current midpoint so the world under the pinch stays put. Pure view
// math (same clamps as the wheel), so a test can assert it without a browser.
export function pinchZoomPan(camera, map, vw, vh, prev, cur) {
const prevMidX = (prev.ax + prev.bx) / 2, prevMidY = (prev.ay + prev.by) / 2;
const curMidX = (cur.ax + cur.bx) / 2, curMidY = (cur.ay + cur.by) / 2;
const prevDist = Math.hypot(prev.ax - prev.bx, prev.ay - prev.by) || 1;
const curDist = Math.hypot(cur.ax - cur.bx, cur.ay - cur.by) || 1;
// Zoom about the current midpoint (keep that world point fixed on screen).
const before = screenToWorld(camera, vw, vh, curMidX, curMidY);
camera.zoom = Math.min(MAX_ZOOM, Math.max(minZoomFor(map, vw, vh), camera.zoom * (curDist / prevDist)));
const after = screenToWorld(camera, vw, vh, curMidX, curMidY);
camera.x += before.x - after.x;
camera.y += before.y - after.y;
// ...then pan by the midpoint's own movement.
dragCamera(camera, map, vw, vh, curMidX - prevMidX, curMidY - prevMidY);
}
|