40 lines
2.4 KiB
TypeScript
40 lines
2.4 KiB
TypeScript
export type CanvasSize = { width: number; height: number };
|
||
export type CanvasView = { zoom: number; x: number; y: number };
|
||
export type CanvasPosition = { x: number; y: number };
|
||
export const MIN_CANVAS_ZOOM = 0.25;
|
||
export const MAX_CANVAS_ZOOM = 8;
|
||
export const FIT_CANVAS_VIEW: CanvasView = { zoom: 1, x: 0, y: 0 };
|
||
|
||
export function fitCanvasImage(image: CanvasSize, viewport: CanvasSize): CanvasSize {
|
||
if (image.width <= 0 || image.height <= 0 || viewport.width <= 0 || viewport.height <= 0) return { width: 0, height: 0 };
|
||
const scale = Math.min(Math.max(1, viewport.width - 20) / image.width, Math.max(1, viewport.height - 20) / image.height);
|
||
return { width: image.width * scale, height: image.height * scale };
|
||
}
|
||
|
||
export function constrainCanvasView(view: CanvasView, fittedImage: CanvasSize, viewport: CanvasSize): CanvasView {
|
||
const zoom = Math.max(MIN_CANVAS_ZOOM, Math.min(MAX_CANVAS_ZOOM, Number.isFinite(view.zoom) ? view.zoom : 1));
|
||
const maxX = Math.max(0, (fittedImage.width * zoom - viewport.width) / 2);
|
||
const maxY = Math.max(0, (fittedImage.height * zoom - viewport.height) / 2);
|
||
return {
|
||
zoom,
|
||
x: Math.max(-maxX, Math.min(maxX, Number.isFinite(view.x) ? view.x : 0)),
|
||
y: Math.max(-maxY, Math.min(maxY, Number.isFinite(view.y) ? view.y : 0))
|
||
};
|
||
}
|
||
|
||
// The anchor is relative to the viewport center. Preserve the image point
|
||
// under the cursor when zooming, except where an image edge limits panning.
|
||
export function zoomCanvasAt(view: CanvasView, requestedZoom: number, anchor: CanvasPosition, fittedImage: CanvasSize, viewport: CanvasSize): CanvasView {
|
||
const zoom = Math.max(MIN_CANVAS_ZOOM, Math.min(MAX_CANVAS_ZOOM, requestedZoom));
|
||
const factor = zoom / view.zoom;
|
||
return constrainCanvasView({ zoom, x: anchor.x - (anchor.x - view.x) * factor, y: anchor.y - (anchor.y - view.y) * factor }, fittedImage, viewport);
|
||
}
|
||
|
||
// Use the *transformed image bounds*, never the clipped viewport bounds.
|
||
// Persisted annotations and provider prompts remain in the 0–1000 space.
|
||
export function canvasAnnotationPoint(point: CanvasPosition, bounds: CanvasSize & { left: number; top: number }): CanvasPosition | null {
|
||
if (bounds.width <= 0 || bounds.height <= 0) return null;
|
||
const clamp = (value: number) => Math.max(0, Math.min(1000, Math.round(value)));
|
||
return { x: clamp((point.x - bounds.left) / bounds.width * 1000), y: clamp((point.y - bounds.top) / bounds.height * 1000) };
|
||
}
|