Anki/ts/routes/image-occlusion/tools/tool-zoom.ts
2025-01-12 20:05:05 +11:00

248 lines
7.6 KiB
TypeScript

// Copyright: Ankitects Pty Ltd and contributors
// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
// https://codepen.io/amsunny/pen/XWGLxye
// canvas.viewportTransform = [ scaleX, skewX, skewY, scaleY, translateX, translateY ]
import type { fabric } from "fabric";
import Hammer from "hammerjs";
import { isDesktop } from "$lib/tslib/platform";
import type { Size } from "../types";
import { getBoundingBoxSize, redraw } from "./lib";
const minScale = 0.5;
const maxScale = 5;
let zoomScale = 1;
export let currentScale = 1;
export const enableZoom = (canvas: fabric.Canvas) => {
canvas.on("mouse:wheel", onMouseWheel);
};
export const enablePan = (canvas: fabric.Canvas) => {
canvas.on("mouse:down", onMouseDown);
canvas.on("mouse:move", onMouseMove);
canvas.on("mouse:up", onMouseUp);
};
export const disableZoom = (canvas: fabric.Canvas) => {
canvas.off("mouse:wheel", onMouseWheel);
};
export const disablePan = (canvas: fabric.Canvas) => {
canvas.off("mouse:down", onMouseDown);
canvas.off("mouse:move", onMouseMove);
canvas.off("mouse:up", onMouseUp);
};
export const zoomIn = (canvas: fabric.Canvas): void => {
let zoom = canvas.getZoom();
zoom = Math.min(maxScale, zoom * 1.1);
canvas.zoomToPoint({ x: canvas.width! / 2, y: canvas.height! / 2 }, zoom);
constrainBoundsAroundBgImage(canvas);
redraw(canvas);
};
export const zoomOut = (canvas): void => {
let zoom = canvas.getZoom();
zoom = Math.max(minScale, zoom / 1.1);
canvas.zoomToPoint({ x: canvas.width / 2, y: canvas.height / 2 }, zoom / 1.1);
constrainBoundsAroundBgImage(canvas);
redraw(canvas);
};
export const zoomReset = (canvas: fabric.Canvas): void => {
zoomResetInner(canvas);
// reset again to update the viewportTransform
zoomResetInner(canvas);
};
const zoomResetInner = (canvas: fabric.Canvas): void => {
fitCanvasVptScale(canvas);
const vpt = canvas.viewportTransform!;
canvas.zoomToPoint({ x: canvas.width! / 2, y: canvas.height! / 2 }, vpt[0]);
};
export const enablePinchZoom = (canvas: fabric.Canvas) => {
const hammer = new Hammer(upperCanvasElement(canvas));
hammer.get("pinch").set({ enable: true });
hammer.on("pinchin pinchout", ev => {
currentScale = Math.min(Math.max(minScale, ev.scale * zoomScale), maxScale);
canvas.zoomToPoint({ x: canvas.width! / 2, y: canvas.height! / 2 }, currentScale);
constrainBoundsAroundBgImage(canvas);
redraw(canvas);
});
hammer.on("pinchend pinchcancel", () => {
zoomScale = currentScale;
});
};
function upperCanvasElement(canvas: fabric.Canvas): HTMLElement {
return canvas["upperCanvasEl"] as HTMLElement;
}
export const disablePinchZoom = (canvas: fabric.Canvas) => {
const hammer = new Hammer(upperCanvasElement(canvas));
hammer.get("pinch").set({ enable: false });
hammer.off("pinch pinchmove pinchend pinchcancel");
};
export const onResize = (canvas: fabric.Canvas) => {
setCanvasSize(canvas);
zoomReset(canvas);
};
const onMouseWheel = (opt) => {
const canvas = globalThis.canvas;
const delta = opt.e.deltaY;
let zoom = canvas.getZoom();
zoom *= 0.999 ** delta;
zoom = Math.max(minScale, Math.min(zoom, maxScale));
canvas.zoomToPoint({ x: opt.pointer.x, y: opt.pointer.y }, zoom);
opt.e.preventDefault();
opt.e.stopPropagation();
constrainBoundsAroundBgImage(canvas);
redraw(canvas);
};
const onMouseDown = (opt) => {
const canvas = globalThis.canvas;
canvas.discardActiveObject();
const { e } = opt;
const clientX = e.type === "touchstart" ? e.touches[0].clientX : e.clientX;
const clientY = e.type === "touchstart" ? e.touches[0].clientY : e.clientY;
canvas.lastPosX = clientX;
canvas.lastPosY = clientY;
redraw(canvas);
};
export const onMouseMove = (opt) => {
const canvas = globalThis.canvas;
canvas.discardActiveObject();
if (!canvas.viewportTransform) {
return;
}
// handle pinch zoom and pan for mobile devices
if (onPinchZoom(opt)) {
return;
}
onDrag(canvas, opt);
};
export const onPinchZoom = (opt): boolean => {
const { e } = opt;
const canvas = globalThis.canvas;
if ((e.type === "touchmove") && (e.touches.length > 1)) {
onDrag(canvas, opt);
return true;
}
return false;
};
const onDrag = (canvas, opt) => {
const { e } = opt;
const clientX = e.type === "touchmove" ? e.touches[0].clientX : e.clientX;
const clientY = e.type === "touchmove" ? e.touches[0].clientY : e.clientY;
const vpt = canvas.viewportTransform;
vpt[4] += clientX - canvas.lastPosX;
vpt[5] += clientY - canvas.lastPosY;
canvas.lastPosX += clientX - canvas.lastPosX;
canvas.lastPosY += clientY - canvas.lastPosY;
constrainBoundsAroundBgImage(canvas);
redraw(canvas);
};
export const onWheelDrag = (canvas: fabric.Canvas, event: WheelEvent) => {
const deltaX = event.deltaX;
const deltaY = event.deltaY;
const vpt = canvas.viewportTransform!;
canvas["lastPosX"] = event.clientX;
canvas["lastPosY"] = event.clientY;
vpt[4] -= deltaX;
vpt[5] -= deltaY;
canvas["lastPosX"] -= deltaX;
canvas["lastPosY"] -= deltaY;
canvas.setViewportTransform(vpt);
constrainBoundsAroundBgImage(canvas);
redraw(canvas);
};
export const onWheelDragX = (canvas: fabric.Canvas, event: WheelEvent) => {
const delta = event.deltaY;
const vpt = canvas.viewportTransform!;
(canvas as any).lastPosY = event.clientY!;
vpt[4] -= delta;
(canvas as any).lastPosX -= delta;
canvas.setViewportTransform(vpt);
constrainBoundsAroundBgImage(canvas);
redraw(canvas);
};
const onMouseUp = () => {
const canvas = globalThis.canvas;
canvas.setViewportTransform(canvas.viewportTransform);
constrainBoundsAroundBgImage(canvas);
redraw(canvas);
};
export const constrainBoundsAroundBgImage = (canvas: fabric.Canvas) => {
const boundingBox = getBoundingBoxSize();
const ioImage = document.getElementById("image") as HTMLImageElement;
const width = boundingBox.width * canvas.getZoom();
const height = boundingBox.height * canvas.getZoom();
const left = canvas.viewportTransform![4];
const top = canvas.viewportTransform![5];
ioImage.width = width;
ioImage.height = height;
ioImage.style.left = `${left}px`;
ioImage.style.top = `${top}px`;
};
export const setCanvasSize = (canvas: fabric.Canvas) => {
const width = window.innerWidth - 39;
let height = window.innerHeight;
height = isDesktop() ? height - 76 : height - 46;
canvas.setHeight(height);
canvas.setWidth(width);
redraw(canvas);
};
const fitCanvasVptScale = (canvas: fabric.Canvas) => {
const boundingBox = getBoundingBoxSize();
const ratio = getScaleRatio(boundingBox);
const vpt = canvas.viewportTransform!;
const boundingBoxWidth = boundingBox.width * canvas.getZoom();
const boundingBoxHeight = boundingBox.height * canvas.getZoom();
const center = canvas.getCenter();
const translateX = center.left - (boundingBoxWidth / 2);
const translateY = center.top - (boundingBoxHeight / 2);
vpt[0] = ratio;
vpt[3] = ratio;
vpt[4] = Math.max(1, translateX);
vpt[5] = Math.max(1, translateY);
canvas.setViewportTransform(canvas.viewportTransform!);
constrainBoundsAroundBgImage(canvas);
redraw(canvas);
};
const getScaleRatio = (boundingBox: Size) => {
const h1 = boundingBox.height!;
const w1 = boundingBox.width!;
const w2 = innerWidth - 42;
let h2 = window.innerHeight;
h2 = isDesktop() ? h2 - 79 : h2 - 48;
return Math.min(w2 / w1, h2 / h1);
};