c28c29a6c6
Render maps at natural resolution inside a CSS-transform world layer (zoom, pan, fit-to-view) instead of shrinking to maxWidth — large maps are now usable. Pure viewport math in src/lib/map/viewport.ts (unit-tested). New TokenPalette places party PCs / active-encounter combatants / blanks in one click with dup detection; tokens gain optional combatantId (Dexie v9) for live encounter HP. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
45 lines
1.7 KiB
TypeScript
45 lines
1.7 KiB
TypeScript
/** Pure pan/zoom viewport math for the map canvas (no DOM, unit-tested). */
|
|
import type { Point } from './types';
|
|
|
|
export interface Viewport {
|
|
/** multiplier; 1 = natural image resolution */
|
|
zoom: number;
|
|
/** translation in screen pixels, applied before scale */
|
|
panX: number;
|
|
panY: number;
|
|
}
|
|
|
|
export const MIN_ZOOM = 0.1;
|
|
export const MAX_ZOOM = 6;
|
|
|
|
export function clampZoom(z: number): number {
|
|
return Math.max(MIN_ZOOM, Math.min(MAX_ZOOM, z));
|
|
}
|
|
|
|
/**
|
|
* Zoom by `factor` keeping the world point under (cx, cy) fixed on screen.
|
|
* cx/cy are cursor coordinates relative to the viewport's top-left.
|
|
*/
|
|
export function zoomToPoint(vp: Viewport, factor: number, cx: number, cy: number): Viewport {
|
|
const zoom = clampZoom(vp.zoom * factor);
|
|
const wx = (cx - vp.panX) / vp.zoom;
|
|
const wy = (cy - vp.panY) / vp.zoom;
|
|
return { zoom, panX: cx - wx * zoom, panY: cy - wy * zoom };
|
|
}
|
|
|
|
/** Center a natural-size image inside a viewport, never upscaling past 1:1. */
|
|
export function fitViewport(vw: number, vh: number, naturalW: number, naturalH: number): Viewport {
|
|
if (naturalW <= 0 || naturalH <= 0 || vw <= 0 || vh <= 0) return { zoom: 1, panX: 0, panY: 0 };
|
|
const zoom = Math.min(vw / naturalW, vh / naturalH, 1);
|
|
return { zoom, panX: (vw - naturalW * zoom) / 2, panY: (vh - naturalH * zoom) / 2 };
|
|
}
|
|
|
|
/**
|
|
* Convert a screen point to world (natural image) pixels, given the bounding rect
|
|
* of the already-transformed world layer. Because the layer's rect reflects the
|
|
* CSS transform, dividing by zoom is all that's needed.
|
|
*/
|
|
export function worldFromScreen(zoom: number, clientX: number, clientY: number, rectLeft: number, rectTop: number): Point {
|
|
return { x: (clientX - rectLeft) / zoom, y: (clientY - rectTop) / zoom };
|
|
}
|