Spaces:
Paused
Paused
Download camera.js from Almaatla/SpaceCities: direct link, hf CLI and curl.
- Browser
- Download file 4.81 kB
-
https://huggingface.co/spaces/Almaatla/SpaceCities/resolve/main/camera.js
- Command line
-
hf download hf://spaces/Almaatla/SpaceCities/camera.js
-
curl -L -o camera.js https://huggingface.co/spaces/Almaatla/SpaceCities/resolve/main/camera.js
4.81 kB
| /* ============================================================ | |
| 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. | |
| ============================================================ */ | |
| ; | |
| 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); | |
| } | |