mirror of
https://github.com/ankitects/anki.git
synced 2025-09-18 22:12:21 -04:00
fix: blur in io, remove panzoom and use fabricjs for panzoom (#3052)
* fix: blur in io, remove panzoom and use fabricjs for panzoom - remove panzoom - implement panzoom using fabricjs - set background image for canvas - add bounding rect for canvas - draw or add point inside in bounding rect - update zoom tool * support pinch to zoom on mobile client * fix lagging of canvas, zoom in draw mode * panning in touch events
This commit is contained in:
parent
f2acf40221
commit
ea8f0c1491
18 changed files with 466 additions and 240 deletions
|
@ -63,7 +63,7 @@
|
|||
"lodash-es": "^4.17.21",
|
||||
"marked": "^5.1.0",
|
||||
"mathjax": "^3.1.2",
|
||||
"panzoom": "^9.4.3"
|
||||
"hammerjs": "^2.0.8"
|
||||
},
|
||||
"resolutions": {
|
||||
"canvas": "npm:empty-npm-package"
|
||||
|
|
|
@ -13,24 +13,20 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
|||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
import type { PanZoom } from "panzoom";
|
||||
import panzoom from "panzoom";
|
||||
import { createEventDispatcher, onDestroy, onMount } from "svelte";
|
||||
|
||||
import type { IOMode } from "./lib";
|
||||
import {
|
||||
type ImageLoadedEvent,
|
||||
setCanvasZoomRatio,
|
||||
setupMaskEditor,
|
||||
setupMaskEditorForEdit,
|
||||
} from "./mask-editor";
|
||||
import Toolbar from "./Toolbar.svelte";
|
||||
import { MaskEditorAPI } from "./tools/api";
|
||||
import { setCenterXForZoom } from "./tools/lib";
|
||||
import { onResize } from "./tools/tool-zoom";
|
||||
|
||||
export let mode: IOMode;
|
||||
const iconSize = 80;
|
||||
let instance: PanZoom;
|
||||
let innerWidth = 0;
|
||||
const startingTool = mode.kind === "add" ? "draw-rectangle" : "cursor";
|
||||
$: canvas = null;
|
||||
|
@ -51,26 +47,13 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
|||
|
||||
$: $changeSignal, onChange();
|
||||
|
||||
function init(node) {
|
||||
instance = panzoom(node, {
|
||||
bounds: true,
|
||||
maxZoom: 3,
|
||||
minZoom: 0.1,
|
||||
zoomDoubleClickSpeed: 1,
|
||||
smoothScroll: false,
|
||||
transformOrigin: { x: 0.5, y: 0.5 },
|
||||
});
|
||||
instance.pause();
|
||||
globalThis.panzoom = instance;
|
||||
|
||||
function init(_node: HTMLDivElement) {
|
||||
if (mode.kind == "add") {
|
||||
setupMaskEditor(mode.imagePath, instance, onChange, onImageLoaded).then(
|
||||
(canvas1) => {
|
||||
setupMaskEditor(mode.imagePath, onChange, onImageLoaded).then((canvas1) => {
|
||||
canvas = canvas1;
|
||||
},
|
||||
);
|
||||
});
|
||||
} else {
|
||||
setupMaskEditorForEdit(mode.noteId, instance, onChange, onImageLoaded).then(
|
||||
setupMaskEditorForEdit(mode.noteId, onChange, onImageLoaded).then(
|
||||
(canvas1) => {
|
||||
canvas = canvas1;
|
||||
},
|
||||
|
@ -79,21 +62,19 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
|||
}
|
||||
|
||||
onMount(() => {
|
||||
window.addEventListener("resize", () => {
|
||||
setCanvasZoomRatio(canvas, instance);
|
||||
setCenterXForZoom(canvas);
|
||||
});
|
||||
window.addEventListener("resize", resizeEvent);
|
||||
});
|
||||
|
||||
onDestroy(() => {
|
||||
window.removeEventListener("resize", () => {
|
||||
setCanvasZoomRatio(canvas, instance);
|
||||
setCenterXForZoom(canvas);
|
||||
});
|
||||
window.removeEventListener("resize", resizeEvent);
|
||||
});
|
||||
|
||||
const resizeEvent = () => {
|
||||
onResize(canvas);
|
||||
};
|
||||
</script>
|
||||
|
||||
<Toolbar {canvas} {instance} {iconSize} activeTool={startingTool} />
|
||||
<Toolbar {canvas} {iconSize} activeTool={startingTool} />
|
||||
<div class="editor-main" bind:clientWidth={innerWidth}>
|
||||
<div class="editor-container" use:init>
|
||||
<!-- svelte-ignore a11y-missing-attribute -->
|
||||
|
@ -111,7 +92,6 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
|||
right: 2px;
|
||||
border: 1px solid var(--border);
|
||||
overflow: auto;
|
||||
padding-bottom: 100px;
|
||||
outline: none !important;
|
||||
}
|
||||
|
||||
|
@ -125,6 +105,7 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
|||
height: 100%;
|
||||
position: relative;
|
||||
direction: ltr;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
#image {
|
||||
|
|
|
@ -28,11 +28,12 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
|||
} from "./tools/more-tools";
|
||||
import { toggleTranslucentKeyCombination } from "./tools/shortcuts";
|
||||
import { tools } from "./tools/tool-buttons";
|
||||
import { drawCursor } from "./tools/tool-cursor";
|
||||
import { removeUnfinishedPolygon } from "./tools/tool-polygon";
|
||||
import { undoRedoTools, undoStack } from "./tools/tool-undo-redo";
|
||||
import { disableZoom, enableZoom } from "./tools/tool-zoom";
|
||||
|
||||
export let canvas;
|
||||
export let instance;
|
||||
export let iconSize;
|
||||
export let activeTool = "cursor";
|
||||
let showAlignTools = false;
|
||||
|
@ -98,32 +99,37 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
|||
});
|
||||
window.addEventListener("keydown", (event) => {
|
||||
if (event.key == "Control" && activeTool != "magnify") {
|
||||
instance.resume();
|
||||
stopDraw(canvas);
|
||||
enableZoom(canvas);
|
||||
}
|
||||
});
|
||||
window.addEventListener("keyup", (event) => {
|
||||
if (event.key == "Control" && activeTool != "magnify") {
|
||||
instance.pause();
|
||||
disableFunctions();
|
||||
}
|
||||
});
|
||||
window.addEventListener("wheel", () => {
|
||||
if (clicked && move && wheel && !dbclicked) {
|
||||
enableMagnify();
|
||||
stopDraw(canvas);
|
||||
enableZoom(canvas);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// handle tool changes after initialization
|
||||
$: if (instance && canvas) {
|
||||
$: if (canvas) {
|
||||
disableFunctions();
|
||||
enableSelectable(canvas, true);
|
||||
// remove unfinished polygon when switching to other tools
|
||||
removeUnfinishedPolygon(canvas);
|
||||
|
||||
switch (activeTool) {
|
||||
case "cursor":
|
||||
drawCursor(canvas);
|
||||
break;
|
||||
case "magnify":
|
||||
enableZoom(canvas);
|
||||
enableSelectable(canvas, false);
|
||||
instance.resume();
|
||||
break;
|
||||
case "draw-rectangle":
|
||||
drawRectangle(canvas);
|
||||
|
@ -132,7 +138,7 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
|||
drawEllipse(canvas);
|
||||
break;
|
||||
case "draw-polygon":
|
||||
drawPolygon(canvas, instance);
|
||||
drawPolygon(canvas);
|
||||
break;
|
||||
case "draw-text":
|
||||
drawText(canvas);
|
||||
|
@ -143,21 +149,14 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
|||
}
|
||||
|
||||
const disableFunctions = () => {
|
||||
instance.pause();
|
||||
stopDraw(canvas);
|
||||
canvas.selectionColor = "rgba(100, 100, 255, 0.3)";
|
||||
disableZoom(canvas);
|
||||
};
|
||||
|
||||
function changeOcclusionType(occlusionType: "all" | "one"): void {
|
||||
$hideAllGuessOne = occlusionType === "all";
|
||||
emitChangeSignal();
|
||||
}
|
||||
const enableMagnify = () => {
|
||||
disableFunctions();
|
||||
enableSelectable(canvas, false);
|
||||
instance.resume();
|
||||
activeTool = "magnify";
|
||||
};
|
||||
</script>
|
||||
|
||||
<div class="tool-bar-container">
|
||||
|
@ -250,7 +249,7 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
|||
{iconSize}
|
||||
tooltip="{tool.tooltip()} ({getPlatformString(tool.shortcut)})"
|
||||
on:click={() => {
|
||||
tool.action(instance);
|
||||
tool.action(canvas);
|
||||
}}
|
||||
>
|
||||
{@html tool.icon}
|
||||
|
@ -259,7 +258,7 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
|||
<Shortcut
|
||||
keyCombination={tool.shortcut}
|
||||
on:action={() => {
|
||||
tool.action(instance);
|
||||
tool.action(canvas);
|
||||
}}
|
||||
/>
|
||||
{/if}
|
||||
|
|
|
@ -5,22 +5,16 @@ import { protoBase64 } from "@bufbuild/protobuf";
|
|||
import { getImageForOcclusion, getImageOcclusionNote } from "@tslib/backend";
|
||||
import * as tr from "@tslib/ftl";
|
||||
import { fabric } from "fabric";
|
||||
import type { PanZoom } from "panzoom";
|
||||
import { get } from "svelte/store";
|
||||
|
||||
import { optimumCssSizeForCanvas } from "./canvas-scale";
|
||||
import { notesDataStore, tagsWritable, zoomResetValue } from "./store";
|
||||
import { notesDataStore, tagsWritable } from "./store";
|
||||
import Toast from "./Toast.svelte";
|
||||
import { addShapesToCanvasFromCloze } from "./tools/add-from-cloze";
|
||||
import {
|
||||
enableSelectable,
|
||||
makeShapeRemainInCanvas,
|
||||
moveShapeToCanvasBoundaries,
|
||||
setCenterXForZoom,
|
||||
zoomReset,
|
||||
} from "./tools/lib";
|
||||
import { enableSelectable, makeShapeRemainInCanvas, moveShapeToCanvasBoundaries } from "./tools/lib";
|
||||
import { modifiedPolygon } from "./tools/tool-polygon";
|
||||
import { undoStack } from "./tools/tool-undo-redo";
|
||||
import { enablePinchZoom, onResize, setCanvasSize } from "./tools/tool-zoom";
|
||||
import type { Size } from "./types";
|
||||
|
||||
export interface ImageLoadedEvent {
|
||||
|
@ -30,7 +24,6 @@ export interface ImageLoadedEvent {
|
|||
|
||||
export const setupMaskEditor = async (
|
||||
path: string,
|
||||
instance: PanZoom,
|
||||
onChange: () => void,
|
||||
onImageLoaded: (event: ImageLoadedEvent) => void,
|
||||
): Promise<fabric.Canvas> => {
|
||||
|
@ -42,13 +35,10 @@ export const setupMaskEditor = async (
|
|||
image.src = getImageData(imageData.data!, path);
|
||||
image.onload = function() {
|
||||
const size = optimumCssSizeForCanvas({ width: image.width, height: image.height }, containerSize());
|
||||
canvas.setWidth(size.width);
|
||||
canvas.setHeight(size.height);
|
||||
image.height = size.height;
|
||||
image.width = size.width;
|
||||
setCanvasZoomRatio(canvas, instance);
|
||||
undoStack.reset();
|
||||
setCanvasSize(canvas);
|
||||
onImageLoaded({ path });
|
||||
setupBoundingBox(canvas, size);
|
||||
undoStack.reset();
|
||||
};
|
||||
|
||||
return canvas;
|
||||
|
@ -56,7 +46,6 @@ export const setupMaskEditor = async (
|
|||
|
||||
export const setupMaskEditorForEdit = async (
|
||||
noteId: number,
|
||||
instance: PanZoom,
|
||||
onChange: () => void,
|
||||
onImageLoaded: (event: ImageLoadedEvent) => void,
|
||||
): Promise<fabric.Canvas> => {
|
||||
|
@ -78,22 +67,17 @@ export const setupMaskEditorForEdit = async (
|
|||
|
||||
// get image width and height
|
||||
const image = document.getElementById("image") as HTMLImageElement;
|
||||
image.style.visibility = "hidden";
|
||||
image.src = getImageData(clozeNote.imageData!, clozeNote.imageFileName!);
|
||||
image.onload = function() {
|
||||
const size = optimumCssSizeForCanvas({ width: image.width, height: image.height }, containerSize());
|
||||
canvas.setWidth(size.width);
|
||||
canvas.setHeight(size.height);
|
||||
image.height = size.height;
|
||||
image.width = size.width;
|
||||
|
||||
setCanvasZoomRatio(canvas, instance);
|
||||
addShapesToCanvasFromCloze(canvas, clozeNote.occlusions);
|
||||
image.onload = async function() {
|
||||
const size = optimumCssSizeForCanvas({ width: image.width, height: image.height }, containerSize());
|
||||
setCanvasSize(canvas);
|
||||
const boundingBox = setupBoundingBox(canvas, size);
|
||||
addShapesToCanvasFromCloze(canvas, boundingBox, clozeNote.occlusions);
|
||||
enableSelectable(canvas, true);
|
||||
addClozeNotesToTextEditor(clozeNote.header, clozeNote.backExtra, clozeNote.tags);
|
||||
undoStack.reset();
|
||||
window.requestAnimationFrame(() => {
|
||||
image.style.visibility = "visible";
|
||||
onImageLoaded({ noteId: BigInt(noteId) });
|
||||
});
|
||||
};
|
||||
|
@ -114,13 +98,13 @@ function initCanvas(onChange: () => void): fabric.Canvas {
|
|||
canvas.uniScaleKey = "none";
|
||||
// disable rotation globally
|
||||
delete fabric.Object.prototype.controls.mtr;
|
||||
// disable object caching
|
||||
fabric.Object.prototype.objectCaching = false;
|
||||
// add a border to corner to handle blend of control
|
||||
fabric.Object.prototype.transparentCorners = false;
|
||||
fabric.Object.prototype.cornerStyle = "circle";
|
||||
fabric.Object.prototype.cornerStrokeColor = "#000000";
|
||||
fabric.Object.prototype.padding = 8;
|
||||
moveShapeToCanvasBoundaries(canvas);
|
||||
makeShapeRemainInCanvas(canvas);
|
||||
canvas.on("object:modified", (evt) => {
|
||||
if (evt.target instanceof fabric.Polygon) {
|
||||
modifiedPolygon(canvas, evt.target);
|
||||
|
@ -129,10 +113,33 @@ function initCanvas(onChange: () => void): fabric.Canvas {
|
|||
onChange();
|
||||
});
|
||||
canvas.on("object:removed", onChange);
|
||||
setCenterXForZoom(canvas);
|
||||
return canvas;
|
||||
}
|
||||
|
||||
const setupBoundingBox = (canvas: fabric.Canvas, size: Size): fabric.Rect => {
|
||||
const boundingBox = new fabric.Rect({
|
||||
id: "boundingBox",
|
||||
fill: "transparent",
|
||||
width: size.width,
|
||||
height: size.height,
|
||||
hasBorders: false,
|
||||
hasControls: false,
|
||||
lockMovementX: true,
|
||||
lockMovementY: true,
|
||||
selectable: false,
|
||||
evented: false,
|
||||
stroke: "red",
|
||||
});
|
||||
|
||||
canvas.add(boundingBox);
|
||||
onResize(canvas);
|
||||
makeShapeRemainInCanvas(canvas, boundingBox);
|
||||
moveShapeToCanvasBoundaries(canvas, boundingBox);
|
||||
// enable pinch zoom for mobile devices
|
||||
enablePinchZoom(canvas);
|
||||
return boundingBox;
|
||||
};
|
||||
|
||||
const getImageData = (imageData, path): string => {
|
||||
const b64encoded = protoBase64.enc(imageData);
|
||||
const extension = path.split(".").pop();
|
||||
|
@ -150,17 +157,6 @@ const getImageData = (imageData, path): string => {
|
|||
return `data:image/${type};base64,${b64encoded}`;
|
||||
};
|
||||
|
||||
export const setCanvasZoomRatio = (
|
||||
canvas: fabric.Canvas,
|
||||
instance: PanZoom,
|
||||
): void => {
|
||||
const zoomRatioW = (innerWidth - 40) / canvas.width!;
|
||||
const zoomRatioH = (innerHeight - 100) / canvas.height!;
|
||||
const zoomRatio = zoomRatioW < zoomRatioH ? zoomRatioW : zoomRatioH;
|
||||
zoomResetValue.set(zoomRatio);
|
||||
zoomReset(instance);
|
||||
};
|
||||
|
||||
const addClozeNotesToTextEditor = (header: string, backExtra: string, tags: string[]) => {
|
||||
const noteFieldsData: { id: string; title: string; divValue: string; textareaValue: string }[] = get(
|
||||
notesDataStore,
|
||||
|
@ -195,16 +191,16 @@ export async function resetIOImage(path: string, onImageLoaded: (event: ImageLoa
|
|||
image.src = getImageData(imageData.data!, path);
|
||||
const canvas = globalThis.canvas;
|
||||
|
||||
image.onload = function() {
|
||||
image.onload = async function() {
|
||||
const size = optimumCssSizeForCanvas(
|
||||
{ width: image.naturalWidth, height: image.naturalHeight },
|
||||
containerSize(),
|
||||
);
|
||||
canvas.setWidth(size.width);
|
||||
canvas.setHeight(size.height);
|
||||
image.height = size.height;
|
||||
image.width = size.width;
|
||||
image.height = size.height;
|
||||
setCanvasSize(canvas);
|
||||
onImageLoaded({ path });
|
||||
setupBoundingBox(canvas, size);
|
||||
};
|
||||
}
|
||||
globalThis.resetIOImage = resetIOImage;
|
||||
|
|
|
@ -3,6 +3,7 @@
|
|||
|
||||
import type { Canvas, Object as FabricObject } from "fabric";
|
||||
import { fabric } from "fabric";
|
||||
import { getBoundingBox } from "image-occlusion/tools/lib";
|
||||
import { cloneDeep } from "lodash-es";
|
||||
|
||||
import type { Size } from "../types";
|
||||
|
@ -21,6 +22,14 @@ export function exportShapesToClozeDeletions(occludeInactive: boolean): {
|
|||
let clozes = "";
|
||||
let index = 0;
|
||||
shapes.forEach((shapeOrShapes) => {
|
||||
// shapes with width or height less than 5 are not valid
|
||||
if (shapeOrShapes === null) {
|
||||
return;
|
||||
}
|
||||
// if shape is Rect and fill is transparent, skip it
|
||||
if (shapeOrShapes instanceof Rectangle && shapeOrShapes.fill === "transparent") {
|
||||
return;
|
||||
}
|
||||
clozes += shapeOrShapesToCloze(shapeOrShapes, index, occludeInactive);
|
||||
if (!(shapeOrShapes instanceof Text)) {
|
||||
index++;
|
||||
|
@ -41,6 +50,7 @@ export function baseShapesFromFabric(): ShapeOrShapes[] {
|
|||
? activeObject
|
||||
: null;
|
||||
const objects = canvas.getObjects() as FabricObject[];
|
||||
const boundingBox = getBoundingBox();
|
||||
return objects
|
||||
.map((object) => {
|
||||
// If the object is in the active selection containing multiple objects,
|
||||
|
@ -48,8 +58,11 @@ export function baseShapesFromFabric(): ShapeOrShapes[] {
|
|||
const parent = selectionContainingMultipleObjects?.contains(object)
|
||||
? selectionContainingMultipleObjects
|
||||
: undefined;
|
||||
if (object.width < 5 || object.height < 5) {
|
||||
return null;
|
||||
}
|
||||
return fabricObjectToBaseShapeOrShapes(
|
||||
canvas,
|
||||
boundingBox,
|
||||
object,
|
||||
parent,
|
||||
);
|
||||
|
@ -107,6 +120,10 @@ function fabricObjectToBaseShapeOrShapes(
|
|||
shape.top = newPosition.y;
|
||||
}
|
||||
|
||||
if (size == undefined) {
|
||||
size = { width: 0, height: 0 };
|
||||
}
|
||||
|
||||
shape = shape.toNormal(size);
|
||||
return shape;
|
||||
}
|
||||
|
|
|
@ -5,16 +5,12 @@ import { writable } from "svelte/store";
|
|||
|
||||
// it stores note's data for generate.ts, when function generate() is called it will be used to generate the note
|
||||
export const notesDataStore = writable({ id: "", title: "", divValue: "", textareaValue: "" }[0]);
|
||||
// it stores the value of zoom ratio for canvas
|
||||
export const zoomResetValue = writable(1);
|
||||
// it stores the tags for the note in note editor
|
||||
export const tagsWritable = writable([""]);
|
||||
// it stores the visibility of mask editor
|
||||
export const ioMaskEditorVisible = writable(true);
|
||||
// it store hide all or hide one mode
|
||||
export const hideAllGuessOne = writable(true);
|
||||
// store initial value of x for zoom reset
|
||||
export const zoomResetX = writable(0);
|
||||
// ioImageLoadedStore is used to store the image loaded event
|
||||
export const ioImageLoadedStore = writable(false);
|
||||
// store opacity state of objects in canvas
|
||||
|
|
|
@ -10,13 +10,14 @@ import { redraw } from "./lib";
|
|||
|
||||
export const addShapesToCanvasFromCloze = (
|
||||
canvas: fabric.Canvas,
|
||||
boundingBox: fabric.Rect,
|
||||
occlusions: GetImageOcclusionNoteResponse_ImageOcclusion[],
|
||||
): void => {
|
||||
for (const shapeOrShapes of extractShapesFromClozedField(occlusions)) {
|
||||
if (Array.isArray(shapeOrShapes)) {
|
||||
addShapeGroup(canvas, shapeOrShapes);
|
||||
addShapeGroup(canvas, boundingBox, shapeOrShapes);
|
||||
} else {
|
||||
addShape(canvas, shapeOrShapes);
|
||||
addShape(canvas, boundingBox, shapeOrShapes);
|
||||
}
|
||||
}
|
||||
redraw(canvas);
|
||||
|
|
|
@ -27,12 +27,12 @@ export class MaskEditorAPI {
|
|||
this.canvas = canvas;
|
||||
}
|
||||
|
||||
addShape(shape: Shape): void {
|
||||
addShape(this.canvas, shape);
|
||||
addShape(bounding, shape: Shape): void {
|
||||
addShape(this.canvas, bounding, shape);
|
||||
}
|
||||
|
||||
addShapeGroup(shapes: Shape[]): void {
|
||||
addShapeGroup(this.canvas, shapes);
|
||||
addShapeGroup(bounding, shapes: Shape[]): void {
|
||||
addShapeGroup(this.canvas, bounding, shapes);
|
||||
}
|
||||
|
||||
getClozes(occludeInactive: boolean): ClozeExportResult {
|
||||
|
|
|
@ -8,9 +8,10 @@ import { addBorder, enableUniformScaling } from "./lib";
|
|||
|
||||
export const addShape = (
|
||||
canvas: fabric.Canvas,
|
||||
boundingBox: fabric.Rect,
|
||||
shape: Shape,
|
||||
): void => {
|
||||
const fabricShape = shape.toFabric(canvas);
|
||||
const fabricShape = shape.toFabric(boundingBox);
|
||||
addBorder(fabricShape);
|
||||
if (fabricShape.type === "i-text") {
|
||||
enableUniformScaling(canvas, fabricShape);
|
||||
|
@ -20,11 +21,12 @@ export const addShape = (
|
|||
|
||||
export const addShapeGroup = (
|
||||
canvas: fabric.Canvas,
|
||||
boundingBox: fabric.Rect,
|
||||
shapes: Shape[],
|
||||
): void => {
|
||||
const group = new fabric.Group();
|
||||
shapes.map((shape) => {
|
||||
const fabricShape = shape.toFabric(canvas);
|
||||
const fabricShape = shape.toFabric(boundingBox);
|
||||
addBorder(fabricShape);
|
||||
group.addWithUpdate(fabricShape);
|
||||
});
|
||||
|
|
|
@ -2,10 +2,9 @@
|
|||
// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||
|
||||
import { fabric } from "fabric";
|
||||
import type { PanZoom } from "panzoom";
|
||||
import { get } from "svelte/store";
|
||||
|
||||
import { opacityStateStore, zoomResetValue, zoomResetX } from "../store";
|
||||
import { opacityStateStore } from "../store";
|
||||
|
||||
export const SHAPE_MASK_COLOR = "#ffeba2";
|
||||
export const BORDER_COLOR = "#212121";
|
||||
|
@ -27,6 +26,9 @@ export const enableSelectable = (
|
|||
): void => {
|
||||
canvas.selection = select;
|
||||
canvas.forEachObject(function(o) {
|
||||
if (o.fill === "transparent") {
|
||||
return;
|
||||
}
|
||||
o.selectable = select;
|
||||
});
|
||||
canvas.renderAll();
|
||||
|
@ -41,6 +43,7 @@ export const deleteItem = (canvas: fabric.Canvas): void => {
|
|||
canvas.discardActiveObject().renderAll();
|
||||
}
|
||||
}
|
||||
redraw(canvas);
|
||||
};
|
||||
|
||||
export const duplicateItem = (canvas: fabric.Canvas): void => {
|
||||
|
@ -92,38 +95,6 @@ export const unGroupShapes = (canvas: fabric.Canvas): void => {
|
|||
redraw(canvas);
|
||||
};
|
||||
|
||||
export const zoomIn = (instance: PanZoom): void => {
|
||||
const center = getCanvasCenter();
|
||||
instance.smoothZoom(center.x, center.y, 1.25);
|
||||
};
|
||||
|
||||
export const zoomOut = (instance: PanZoom): void => {
|
||||
const center = getCanvasCenter();
|
||||
instance.smoothZoom(center.x, center.y, 0.8);
|
||||
};
|
||||
|
||||
export const zoomReset = (instance: PanZoom): void => {
|
||||
setCenterXForZoom(globalThis.canvas);
|
||||
instance.moveTo(get(zoomResetX), 0);
|
||||
instance.smoothZoomAbs(get(zoomResetX), 0, get(zoomResetValue));
|
||||
};
|
||||
|
||||
export const getCanvasCenter = () => {
|
||||
const canvas = globalThis.canvas.getElement();
|
||||
const rect = canvas.getBoundingClientRect();
|
||||
const centerX = rect.x + rect.width / 2;
|
||||
const centerY = rect.y + rect.height / 2;
|
||||
return { x: centerX, y: centerY };
|
||||
};
|
||||
|
||||
export const setCenterXForZoom = (canvas: fabric.Canvas) => {
|
||||
const editor = document.querySelector(".editor-main")!;
|
||||
const editorWidth = editor.clientWidth;
|
||||
const canvasWidth = canvas.getElement().offsetWidth;
|
||||
const centerX = editorWidth / 2 - canvasWidth / 2;
|
||||
zoomResetX.set(centerX);
|
||||
};
|
||||
|
||||
const copyItem = (canvas: fabric.Canvas): void => {
|
||||
if (!canvas.getActiveObject()) {
|
||||
return;
|
||||
|
@ -184,26 +155,26 @@ export const makeMaskTransparent = (
|
|||
canvas.renderAll();
|
||||
};
|
||||
|
||||
export const moveShapeToCanvasBoundaries = (canvas: fabric.Canvas): void => {
|
||||
export const moveShapeToCanvasBoundaries = (canvas: fabric.Canvas, boundingBox: fabric.Rect): void => {
|
||||
canvas.on("object:modified", function(o) {
|
||||
const activeObject = o.target;
|
||||
if (!activeObject) {
|
||||
return;
|
||||
}
|
||||
if (activeObject.type === "rect") {
|
||||
modifiedRectangle(canvas, activeObject);
|
||||
modifiedRectangle(boundingBox, activeObject);
|
||||
}
|
||||
if (activeObject.type === "ellipse") {
|
||||
modifiedEllipse(canvas, activeObject);
|
||||
modifiedEllipse(boundingBox, activeObject);
|
||||
}
|
||||
if (activeObject.type === "i-text") {
|
||||
modifiedText(canvas, activeObject);
|
||||
modifiedText(boundingBox, activeObject);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const modifiedRectangle = (
|
||||
canvas: fabric.Canvas,
|
||||
boundingBox: fabric.Rect,
|
||||
object: fabric.Object,
|
||||
): void => {
|
||||
const newWidth = object.width * object.scaleX;
|
||||
|
@ -215,11 +186,11 @@ const modifiedRectangle = (
|
|||
scaleX: 1,
|
||||
scaleY: 1,
|
||||
});
|
||||
setShapePosition(canvas, object);
|
||||
setShapePosition(boundingBox, object);
|
||||
};
|
||||
|
||||
const modifiedEllipse = (
|
||||
canvas: fabric.Canvas,
|
||||
boundingBox: fabric.Rect,
|
||||
object: fabric.Object,
|
||||
): void => {
|
||||
const newRx = object.rx * object.scaleX;
|
||||
|
@ -235,15 +206,15 @@ const modifiedEllipse = (
|
|||
scaleX: 1,
|
||||
scaleY: 1,
|
||||
});
|
||||
setShapePosition(canvas, object);
|
||||
setShapePosition(boundingBox, object);
|
||||
};
|
||||
|
||||
const modifiedText = (canvas: fabric.Canvas, object: fabric.Object): void => {
|
||||
setShapePosition(canvas, object);
|
||||
const modifiedText = (boundingBox: fabric.Rect, object: fabric.Object): void => {
|
||||
setShapePosition(boundingBox, object);
|
||||
};
|
||||
|
||||
const setShapePosition = (
|
||||
canvas: fabric.Canvas,
|
||||
boundingBox: fabric.Rect,
|
||||
object: fabric.Object,
|
||||
): void => {
|
||||
if (object.left < 0) {
|
||||
|
@ -252,11 +223,11 @@ const setShapePosition = (
|
|||
if (object.top < 0) {
|
||||
object.set({ top: 0 });
|
||||
}
|
||||
if (object.left + object.width * object.scaleX + object.strokeWidth > canvas.width) {
|
||||
object.set({ left: canvas.width - object.width * object.scaleX });
|
||||
if (object.left + object.width * object.scaleX + object.strokeWidth > boundingBox.width) {
|
||||
object.set({ left: boundingBox.width - object.width * object.scaleX });
|
||||
}
|
||||
if (object.top + object.height * object.scaleY + object.strokeWidth > canvas.height) {
|
||||
object.set({ top: canvas.height - object.height * object.scaleY });
|
||||
if (object.top + object.height * object.scaleY + object.strokeWidth > boundingBox.height) {
|
||||
object.set({ top: boundingBox.height - object.height * object.scaleY });
|
||||
}
|
||||
object.setCoords();
|
||||
};
|
||||
|
@ -287,33 +258,25 @@ export const clear = (canvas: fabric.Canvas): void => {
|
|||
canvas.clear();
|
||||
};
|
||||
|
||||
export const makeShapeRemainInCanvas = (canvas: fabric.Canvas) => {
|
||||
export const makeShapeRemainInCanvas = (canvas: fabric.Canvas, boundingBox: fabric.Rect) => {
|
||||
canvas.on("object:moving", function(e) {
|
||||
const obj = e.target;
|
||||
if (obj.getScaledHeight() > obj.canvas.height || obj.getScaledWidth() > obj.canvas.width) {
|
||||
if (obj.getScaledHeight() > boundingBox.height || obj.getScaledWidth() > boundingBox.width) {
|
||||
return;
|
||||
}
|
||||
|
||||
obj.setCoords();
|
||||
|
||||
if (obj.getBoundingRect().top < 0 || obj.getBoundingRect().left < 0) {
|
||||
obj.top = Math.max(obj.top, obj.top - obj.getBoundingRect().top);
|
||||
obj.left = Math.max(obj.left, obj.left - obj.getBoundingRect().left);
|
||||
}
|
||||
const top = obj.top;
|
||||
const left = obj.left;
|
||||
|
||||
if (
|
||||
obj.getBoundingRect().top + obj.getBoundingRect().height > obj.canvas.height
|
||||
|| obj.getBoundingRect().left + obj.getBoundingRect().width > obj.canvas.width
|
||||
) {
|
||||
obj.top = Math.min(
|
||||
obj.top,
|
||||
obj.canvas.height - obj.getBoundingRect().height + obj.top - obj.getBoundingRect().top,
|
||||
);
|
||||
obj.left = Math.min(
|
||||
obj.left,
|
||||
obj.canvas.width - obj.getBoundingRect().width + obj.left - obj.getBoundingRect().left,
|
||||
);
|
||||
}
|
||||
const topBound = boundingBox.top;
|
||||
const bottomBound = topBound + boundingBox.height;
|
||||
const leftBound = boundingBox.left;
|
||||
const rightBound = leftBound + boundingBox.width;
|
||||
|
||||
obj.left = Math.min(Math.max(left, leftBound), rightBound - obj.width);
|
||||
obj.top = Math.min(Math.max(top, topBound), bottomBound - obj.height);
|
||||
});
|
||||
};
|
||||
|
||||
|
@ -325,3 +288,21 @@ export const selectAllShapes = (canvas: fabric.Canvas) => {
|
|||
canvas.setActiveObject(sel);
|
||||
redraw(canvas);
|
||||
};
|
||||
|
||||
export const isPointerInBoundingBox = (pointer): boolean => {
|
||||
const boundingBox = getBoundingBox();
|
||||
if (
|
||||
pointer.x < boundingBox.left
|
||||
|| pointer.x > boundingBox.left + boundingBox.width
|
||||
|| pointer.y < boundingBox.top
|
||||
|| pointer.y > boundingBox.top + boundingBox.height
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
};
|
||||
|
||||
export const getBoundingBox = () => {
|
||||
const canvas = globalThis.canvas;
|
||||
return canvas.getObjects().find((obj) => obj.fill === "transparent");
|
||||
};
|
||||
|
|
|
@ -19,16 +19,7 @@ import {
|
|||
mdiZoomOut,
|
||||
mdiZoomReset,
|
||||
} from "../icons";
|
||||
import {
|
||||
deleteItem,
|
||||
duplicateItem,
|
||||
groupShapes,
|
||||
selectAllShapes,
|
||||
unGroupShapes,
|
||||
zoomIn,
|
||||
zoomOut,
|
||||
zoomReset,
|
||||
} from "./lib";
|
||||
import { deleteItem, duplicateItem, groupShapes, selectAllShapes, unGroupShapes } from "./lib";
|
||||
import {
|
||||
alignBottomKeyCombination,
|
||||
alignHorizontalCenterKeyCombination,
|
||||
|
@ -53,6 +44,7 @@ import {
|
|||
alignTop,
|
||||
alignVerticalCenter,
|
||||
} from "./tool-aligns";
|
||||
import { zoomIn, zoomOut, zoomReset } from "./tool-zoom";
|
||||
|
||||
export const groupUngroupTools = [
|
||||
{
|
||||
|
|
24
ts/image-occlusion/tools/tool-cursor.ts
Normal file
24
ts/image-occlusion/tools/tool-cursor.ts
Normal file
|
@ -0,0 +1,24 @@
|
|||
// Copyright: Ankitects Pty Ltd and contributors
|
||||
// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||
|
||||
import type { fabric } from "fabric";
|
||||
|
||||
import { stopDraw } from "./lib";
|
||||
import { onPinchZoom } from "./tool-zoom";
|
||||
|
||||
export const drawCursor = (canvas: fabric.Canvas): void => {
|
||||
canvas.selectionColor = "rgba(100, 100, 255, 0.3)";
|
||||
stopDraw(canvas);
|
||||
|
||||
canvas.on("mouse:down", function(o) {
|
||||
if (o.target) {
|
||||
return;
|
||||
}
|
||||
});
|
||||
|
||||
canvas.on("mouse:move", function(o) {
|
||||
if (onPinchZoom(o)) {
|
||||
return;
|
||||
}
|
||||
});
|
||||
};
|
|
@ -5,8 +5,9 @@ import { fabric } from "fabric";
|
|||
import { opacityStateStore } from "image-occlusion/store";
|
||||
import { get } from "svelte/store";
|
||||
|
||||
import { BORDER_COLOR, SHAPE_MASK_COLOR, stopDraw } from "./lib";
|
||||
import { BORDER_COLOR, isPointerInBoundingBox, SHAPE_MASK_COLOR, stopDraw } from "./lib";
|
||||
import { undoStack } from "./tool-undo-redo";
|
||||
import { onPinchZoom } from "./tool-zoom";
|
||||
|
||||
export const drawEllipse = (canvas: fabric.Canvas): void => {
|
||||
canvas.selectionColor = "rgba(0, 0, 0, 0)";
|
||||
|
@ -24,6 +25,11 @@ export const drawEllipse = (canvas: fabric.Canvas): void => {
|
|||
origX = pointer.x;
|
||||
origY = pointer.y;
|
||||
|
||||
if (!isPointerInBoundingBox(pointer)) {
|
||||
isDown = false;
|
||||
return;
|
||||
}
|
||||
|
||||
ellipse = new fabric.Ellipse({
|
||||
id: "ellipse-" + new Date().getTime(),
|
||||
left: origX,
|
||||
|
@ -45,6 +51,12 @@ export const drawEllipse = (canvas: fabric.Canvas): void => {
|
|||
});
|
||||
|
||||
canvas.on("mouse:move", function(o) {
|
||||
if (onPinchZoom(o)) {
|
||||
canvas.remove(ellipse);
|
||||
canvas.renderAll();
|
||||
return;
|
||||
}
|
||||
|
||||
if (!isDown) {
|
||||
return;
|
||||
}
|
||||
|
|
|
@ -3,20 +3,19 @@
|
|||
|
||||
import { fabric } from "fabric";
|
||||
import { opacityStateStore } from "image-occlusion/store";
|
||||
import type { PanZoom } from "panzoom";
|
||||
import { get } from "svelte/store";
|
||||
|
||||
import { BORDER_COLOR, SHAPE_MASK_COLOR } from "./lib";
|
||||
import { BORDER_COLOR, isPointerInBoundingBox, SHAPE_MASK_COLOR } from "./lib";
|
||||
import { undoStack } from "./tool-undo-redo";
|
||||
import { onPinchZoom } from "./tool-zoom";
|
||||
|
||||
let activeLine;
|
||||
let activeShape;
|
||||
let linesList: fabric.Line = [];
|
||||
let pointsList: fabric.Circle = [];
|
||||
let drawMode = false;
|
||||
let zoomValue = 1;
|
||||
|
||||
export const drawPolygon = (canvas: fabric.Canvas, panzoom: PanZoom): void => {
|
||||
export const drawPolygon = (canvas: fabric.Canvas): void => {
|
||||
// remove selectable for shapes
|
||||
canvas.discardActiveObject();
|
||||
canvas.forEachObject(function(o) {
|
||||
|
@ -29,7 +28,7 @@ export const drawPolygon = (canvas: fabric.Canvas, panzoom: PanZoom): void => {
|
|||
if (options.target && options.target.id === pointsList[0].id) {
|
||||
generatePolygon(canvas, pointsList);
|
||||
} else {
|
||||
addPoint(canvas, options, panzoom);
|
||||
addPoint(canvas, options);
|
||||
}
|
||||
} catch (e) {
|
||||
// Cannot read properties of undefined (reading 'id')
|
||||
|
@ -37,6 +36,12 @@ export const drawPolygon = (canvas: fabric.Canvas, panzoom: PanZoom): void => {
|
|||
});
|
||||
|
||||
canvas.on("mouse:move", function(options) {
|
||||
// if pinch zoom is active, remove all points and lines
|
||||
if (onPinchZoom(options)) {
|
||||
removeUnfinishedPolygon(canvas);
|
||||
return;
|
||||
}
|
||||
|
||||
if (activeLine && activeLine.class === "line") {
|
||||
const pointer = canvas.getPointer(options.e);
|
||||
activeLine.set({
|
||||
|
@ -71,15 +76,14 @@ const toggleDrawPolygon = (canvas: fabric.Canvas): void => {
|
|||
}
|
||||
};
|
||||
|
||||
const addPoint = (canvas: fabric.Canvas, options, panzoom): void => {
|
||||
zoomValue = panzoom.getTransform().scale;
|
||||
const addPoint = (canvas: fabric.Canvas, options): void => {
|
||||
const pointer = canvas.getPointer(options.e);
|
||||
const origX = pointer.x;
|
||||
const origY = pointer.y;
|
||||
|
||||
const canvasContainer = document.querySelector(".canvas-container")!.getBoundingClientRect()!;
|
||||
let clientX = options.e.touches ? options.e.touches[0].clientX : options.e.clientX;
|
||||
let clientY = options.e.touches ? options.e.touches[0].clientY : options.e.clientY;
|
||||
|
||||
clientX = (clientX - canvasContainer.left) / zoomValue;
|
||||
clientY = (clientY - canvasContainer.top) / zoomValue;
|
||||
if (!isPointerInBoundingBox(pointer)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const point = new fabric.Circle({
|
||||
radius: 5,
|
||||
|
@ -88,8 +92,8 @@ const addPoint = (canvas: fabric.Canvas, options, panzoom): void => {
|
|||
strokeWidth: 0.5,
|
||||
originX: "left",
|
||||
originY: "top",
|
||||
left: clientX,
|
||||
top: clientY,
|
||||
left: origX,
|
||||
top: origY,
|
||||
selectable: false,
|
||||
hasBorders: false,
|
||||
hasControls: false,
|
||||
|
@ -102,7 +106,7 @@ const addPoint = (canvas: fabric.Canvas, options, panzoom): void => {
|
|||
});
|
||||
}
|
||||
|
||||
const linePoints = [clientX, clientY, clientX, clientY];
|
||||
const linePoints = [origX, origY, origX, origY];
|
||||
|
||||
const line = new fabric.Line(linePoints, {
|
||||
strokeWidth: 2,
|
||||
|
@ -143,7 +147,7 @@ const addPoint = (canvas: fabric.Canvas, options, panzoom): void => {
|
|||
activeShape = polygon;
|
||||
canvas.renderAll();
|
||||
} else {
|
||||
const polyPoint = [{ x: clientX, y: clientY }];
|
||||
const polyPoint = [{ x: origX, y: origY }];
|
||||
const polygon = new fabric.Polygon(polyPoint, {
|
||||
stroke: "#333333",
|
||||
strokeWidth: 1,
|
||||
|
@ -166,6 +170,7 @@ const addPoint = (canvas: fabric.Canvas, options, panzoom): void => {
|
|||
|
||||
canvas.add(line);
|
||||
canvas.add(point);
|
||||
canvas.renderAll();
|
||||
};
|
||||
|
||||
const generatePolygon = (canvas: fabric.Canvas, pointsList): void => {
|
||||
|
|
|
@ -5,8 +5,9 @@ import { fabric } from "fabric";
|
|||
import { opacityStateStore } from "image-occlusion/store";
|
||||
import { get } from "svelte/store";
|
||||
|
||||
import { BORDER_COLOR, SHAPE_MASK_COLOR, stopDraw } from "./lib";
|
||||
import { BORDER_COLOR, isPointerInBoundingBox, SHAPE_MASK_COLOR, stopDraw } from "./lib";
|
||||
import { undoStack } from "./tool-undo-redo";
|
||||
import { onPinchZoom } from "./tool-zoom";
|
||||
|
||||
export const drawRectangle = (canvas: fabric.Canvas): void => {
|
||||
canvas.selectionColor = "rgba(0, 0, 0, 0)";
|
||||
|
@ -24,6 +25,11 @@ export const drawRectangle = (canvas: fabric.Canvas): void => {
|
|||
origX = pointer.x;
|
||||
origY = pointer.y;
|
||||
|
||||
if (!isPointerInBoundingBox(pointer)) {
|
||||
isDown = false;
|
||||
return;
|
||||
}
|
||||
|
||||
rect = new fabric.Rect({
|
||||
id: "rect-" + new Date().getTime(),
|
||||
left: origX,
|
||||
|
@ -46,6 +52,12 @@ export const drawRectangle = (canvas: fabric.Canvas): void => {
|
|||
});
|
||||
|
||||
canvas.on("mouse:move", function(o) {
|
||||
if (onPinchZoom(o)) {
|
||||
canvas.remove(rect);
|
||||
canvas.renderAll();
|
||||
return;
|
||||
}
|
||||
|
||||
if (!isDown) {
|
||||
return;
|
||||
}
|
||||
|
|
|
@ -5,19 +5,34 @@ import { fabric } from "fabric";
|
|||
import { opacityStateStore, textEditingState } from "image-occlusion/store";
|
||||
import { get } from "svelte/store";
|
||||
|
||||
import { enableUniformScaling, stopDraw, TEXT_BACKGROUND_COLOR, TEXT_FONT_FAMILY, TEXT_PADDING } from "./lib";
|
||||
import {
|
||||
enableUniformScaling,
|
||||
isPointerInBoundingBox,
|
||||
stopDraw,
|
||||
TEXT_BACKGROUND_COLOR,
|
||||
TEXT_FONT_FAMILY,
|
||||
TEXT_PADDING,
|
||||
} from "./lib";
|
||||
import { undoStack } from "./tool-undo-redo";
|
||||
import { onPinchZoom } from "./tool-zoom";
|
||||
|
||||
export const drawText = (canvas: fabric.Canvas): void => {
|
||||
canvas.selectionColor = "rgba(0, 0, 0, 0)";
|
||||
stopDraw(canvas);
|
||||
|
||||
let text;
|
||||
|
||||
canvas.on("mouse:down", function(o) {
|
||||
if (o.target) {
|
||||
return;
|
||||
}
|
||||
const pointer = canvas.getPointer(o.e);
|
||||
const text = new fabric.IText("text", {
|
||||
|
||||
if (!isPointerInBoundingBox(pointer)) {
|
||||
return;
|
||||
}
|
||||
|
||||
text = new fabric.IText("text", {
|
||||
id: "text-" + new Date().getTime(),
|
||||
left: pointer.x,
|
||||
top: pointer.y,
|
||||
|
@ -35,10 +50,17 @@ export const drawText = (canvas: fabric.Canvas): void => {
|
|||
canvas.add(text);
|
||||
canvas.setActiveObject(text);
|
||||
undoStack.onObjectAdded(text.id);
|
||||
text.enterEditing();
|
||||
text.selectAll();
|
||||
});
|
||||
|
||||
canvas.on("mouse:move", function(o) {
|
||||
if (onPinchZoom(o)) {
|
||||
canvas.remove(text);
|
||||
canvas.renderAll();
|
||||
return;
|
||||
}
|
||||
});
|
||||
|
||||
canvas.on("text:editing:entered", function() {
|
||||
textEditingState.set(true);
|
||||
});
|
||||
|
|
212
ts/image-occlusion/tools/tool-zoom.ts
Normal file
212
ts/image-occlusion/tools/tool-zoom.ts
Normal file
|
@ -0,0 +1,212 @@
|
|||
// 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 { getBoundingBox, redraw } from "./lib";
|
||||
|
||||
let isDragging = false;
|
||||
|
||||
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);
|
||||
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);
|
||||
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 => {
|
||||
canvas.zoomToPoint({ x: canvas.width / 2, y: canvas.height / 2 }, 1);
|
||||
fitCanvasVptScale(canvas);
|
||||
constrainBoundsAroundBgImage(canvas);
|
||||
};
|
||||
|
||||
export const enablePinchZoom = (canvas: fabric.Canvas) => {
|
||||
const hammer = new Hammer(canvas.upperCanvasEl);
|
||||
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;
|
||||
});
|
||||
};
|
||||
|
||||
export const disablePinchZoom = (canvas: fabric.Canvas) => {
|
||||
const hammer = new Hammer(canvas.upperCanvasEl);
|
||||
hammer.get("pinch").set({ enable: false });
|
||||
hammer.off("pinch pinchmove pinchend pinchcancel");
|
||||
};
|
||||
|
||||
export const onResize = (canvas: fabric.Canvas) => {
|
||||
setCanvasSize(canvas);
|
||||
constrainBoundsAroundBgImage(canvas);
|
||||
fitCanvasVptScale(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) => {
|
||||
isDragging = true;
|
||||
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;
|
||||
if (isDragging) {
|
||||
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;
|
||||
});
|
||||
|
||||
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.lastPosY = clientY;
|
||||
constrainBoundsAroundBgImage(canvas);
|
||||
redraw(canvas);
|
||||
};
|
||||
|
||||
const onMouseUp = () => {
|
||||
isDragging = false;
|
||||
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) => {
|
||||
canvas.setHeight(window.innerHeight - 76);
|
||||
canvas.setWidth(window.innerWidth - 39);
|
||||
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 h2 = innerHeight - 79;
|
||||
const w2 = innerWidth - 42;
|
||||
|
||||
return Math.min(w2 / w1, h2 / h1);
|
||||
};
|
36
yarn.lock
36
yarn.lock
|
@ -1406,13 +1406,6 @@ ajv@^6.12.4:
|
|||
json-schema-traverse "^0.4.1"
|
||||
uri-js "^4.2.2"
|
||||
|
||||
amator@^1.1.0:
|
||||
version "1.1.0"
|
||||
resolved "https://registry.yarnpkg.com/amator/-/amator-1.1.0.tgz#08c6b60bc93aec2b61bbfc0c4d677d30323cc0f1"
|
||||
integrity sha512-V5+aH8pe+Z3u/UG3L3pG3BaFQGXAyXHVQDroRwjPHdh08bcUEchAVsU1MCuJSCaU5o60wTK6KaE6te5memzgYw==
|
||||
dependencies:
|
||||
bezier-easing "^2.0.3"
|
||||
|
||||
ansi-escapes@^4.2.1:
|
||||
version "4.3.2"
|
||||
resolved "https://registry.yarnpkg.com/ansi-escapes/-/ansi-escapes-4.3.2.tgz#6b2291d1db7d98b6521d5f1efa42d0f3a9feb65e"
|
||||
|
@ -1657,11 +1650,6 @@ balanced-match@^1.0.0:
|
|||
resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.2.tgz#e83e3a7e3f300b34cb9d87f615fa0cbf357690ee"
|
||||
integrity sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==
|
||||
|
||||
bezier-easing@^2.0.3:
|
||||
version "2.1.0"
|
||||
resolved "https://registry.yarnpkg.com/bezier-easing/-/bezier-easing-2.1.0.tgz#c04dfe8b926d6ecaca1813d69ff179b7c2025d86"
|
||||
integrity sha512-gbIqZ/eslnUFC1tjEvtz0sgx+xTK20wDnYMIA27VA04R7w6xxXQPZDbibjA9DTWZRA2CXtwHykkVzlCaAJAZig==
|
||||
|
||||
binary-extensions@^2.0.0:
|
||||
version "2.2.0"
|
||||
resolved "https://registry.yarnpkg.com/binary-extensions/-/binary-extensions-2.2.0.tgz#75f502eeaf9ffde42fc98829645be4ea76bd9e2d"
|
||||
|
@ -3038,6 +3026,11 @@ graphemer@^1.4.0:
|
|||
resolved "https://registry.yarnpkg.com/graphemer/-/graphemer-1.4.0.tgz#fb2f1d55e0e3a1849aeffc90c4fa0dd53a0e66c6"
|
||||
integrity sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==
|
||||
|
||||
hammerjs@^2.0.8:
|
||||
version "2.0.8"
|
||||
resolved "https://registry.yarnpkg.com/hammerjs/-/hammerjs-2.0.8.tgz#04ef77862cff2bb79d30f7692095930222bf60f1"
|
||||
integrity sha512-tSQXBXS/MWQOn/RKckawJ61vvsDpCom87JgxiYdGwHdOa0ht0vzUWDlfioofFCRU0L+6NGDt6XzbgoJvZkMeRQ==
|
||||
|
||||
has-bigints@^1.0.1, has-bigints@^1.0.2:
|
||||
version "1.0.2"
|
||||
resolved "https://registry.yarnpkg.com/has-bigints/-/has-bigints-1.0.2.tgz#0871bd3e3d51626f6ca0966668ba35d5602d6eaa"
|
||||
|
@ -4170,11 +4163,6 @@ natural-compare@^1.4.0:
|
|||
resolved "https://registry.yarnpkg.com/natural-compare/-/natural-compare-1.4.0.tgz#4abebfeed7541f2c27acfb29bdbbd15c8d5ba4f7"
|
||||
integrity sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==
|
||||
|
||||
ngraph.events@^1.2.2:
|
||||
version "1.2.2"
|
||||
resolved "https://registry.yarnpkg.com/ngraph.events/-/ngraph.events-1.2.2.tgz#3ceb92d676a04a4e7ce60a09fa8e17a4f0346d7f"
|
||||
integrity sha512-JsUbEOzANskax+WSYiAPETemLWYXmixuPAlmZmhIbIj6FH/WDgEGCGnRwUQBK0GjOnVm8Ui+e5IJ+5VZ4e32eQ==
|
||||
|
||||
node-int64@^0.4.0:
|
||||
version "0.4.0"
|
||||
resolved "https://registry.yarnpkg.com/node-int64/-/node-int64-0.4.0.tgz#87a9065cdb355d3182d8f94ce11188b825c68a3b"
|
||||
|
@ -4332,15 +4320,6 @@ p-try@^2.0.0:
|
|||
resolved "https://registry.yarnpkg.com/p-try/-/p-try-2.2.0.tgz#cb2868540e313d61de58fafbe35ce9004d5540e6"
|
||||
integrity sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==
|
||||
|
||||
panzoom@^9.4.3:
|
||||
version "9.4.3"
|
||||
resolved "https://registry.yarnpkg.com/panzoom/-/panzoom-9.4.3.tgz#195c4031bb643f2e6c42f1de0ca87cc10e224042"
|
||||
integrity sha512-xaxCpElcRbQsUtIdwlrZA90P90+BHip4Vda2BC8MEb4tkI05PmR6cKECdqUCZ85ZvBHjpI9htJrZBxV5Gp/q/w==
|
||||
dependencies:
|
||||
amator "^1.1.0"
|
||||
ngraph.events "^1.2.2"
|
||||
wheel "^1.0.0"
|
||||
|
||||
parent-module@^1.0.0:
|
||||
version "1.0.1"
|
||||
resolved "https://registry.yarnpkg.com/parent-module/-/parent-module-1.0.1.tgz#691d2709e78c79fae3a156622452d00762caaaa2"
|
||||
|
@ -5389,11 +5368,6 @@ whatwg-url@^11.0.0:
|
|||
tr46 "^3.0.0"
|
||||
webidl-conversions "^7.0.0"
|
||||
|
||||
wheel@^1.0.0:
|
||||
version "1.0.0"
|
||||
resolved "https://registry.yarnpkg.com/wheel/-/wheel-1.0.0.tgz#6cf46e06a854181adb8649228077f8b0d5c574ce"
|
||||
integrity sha512-XiCMHibOiqalCQ+BaNSwRoZ9FDTAvOsXxGHXChBugewDj7HC8VBIER71dEOiRH1fSdLbRCQzngKTSiZ06ZQzeA==
|
||||
|
||||
which-boxed-primitive@^1.0.2:
|
||||
version "1.0.2"
|
||||
resolved "https://registry.yarnpkg.com/which-boxed-primitive/-/which-boxed-primitive-1.0.2.tgz#13757bc89b209b049fe5d86430e21cf40a89a8e6"
|
||||
|
|
Loading…
Reference in a new issue