mirror of
https://github.com/ankitects/anki.git
synced 2025-11-08 21:57:12 -05:00
* Update to latest Node LTS * Add sveltekit * Split tslib into separate @generated and @tslib components SvelteKit's path aliases don't support multiple locations, so our old approach of using @tslib to refer to both ts/lib and out/ts/lib will no longer work. Instead, all generated sources and their includes are placed in a separate out/ts/generated folder, and imported via @generated instead. This also allows us to generate .ts files, instead of needing to output separate .d.ts and .js files. * Switch package.json to module type * Avoid usage of baseUrl Incompatible with SvelteKit * Move sass into ts; use relative links SvelteKit's default sass support doesn't allow overriding loadPaths * jest->vitest, graphs example working with yarn dev * most pages working in dev mode * Some fixes after rebasing * Fix/silence some svelte-check errors * Get image-occlusion working with Fabric types * Post-rebase lock changes * Editor is now checked * SvelteKit build integrated into ninja * Use the new SvelteKit entrypoint for pages like congrats/deck options/etc * Run eslint once for ts/**; fix some tests * Fix a bunch of issues introduced when rebasing over latest main * Run eslint fix * Fix remaining eslint+pylint issues; tests now all pass * Fix some issues with a clean build * Latest bufbuild no longer requires @__PURE__ hack * Add a few missed dependencies * Add yarn.bat to fix Windows build * Fix pages failing to show when ANKI_API_PORT not defined * Fix svelte-check and vitest on Windows * Set node path in ./yarn * Move svelte-kit output to ts/.svelte-kit Sadly, I couldn't figure out a way to store it in out/ if out/ is a symlink, as it breaks module resolution when SvelteKit is run. * Allow HMR inside Anki * Skip SvelteKit build when HMR is defined * Fix some post-rebase issues I should have done a normal merge instead.
265 lines
8.2 KiB
TypeScript
265 lines
8.2 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 { getBoundingBox, 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)) as any;
|
|
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);
|
|
};
|
|
|
|
// initializes lastPosX and lastPosY because it is undefined in touchmove event
|
|
document.addEventListener("touchstart", (e) => {
|
|
const canvas = globalThis.canvas;
|
|
canvas.lastPosX = e.touches[0].clientX;
|
|
canvas.lastPosY = e.touches[0].clientY;
|
|
});
|
|
|
|
// initializes lastPosX and lastPosY because it is undefined before mousemove event
|
|
document.addEventListener("mousemove", (event) => {
|
|
document.addEventListener("keydown", (e) => {
|
|
if (e.key === " ") {
|
|
const canvas = globalThis.canvas;
|
|
canvas.lastPosX = event.clientX;
|
|
canvas.lastPosY = event.clientY;
|
|
}
|
|
});
|
|
});
|
|
|
|
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 = getBoundingBox();
|
|
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 = getBoundingBox();
|
|
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: fabric.Rect) => {
|
|
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);
|
|
};
|