Anki/ts/routes/image-occlusion/tools/tool-polygon.ts
llama 174b199164
Add IO mask colour fill tool (#4048)
* add fill tool

* add fill tool logic

* open colour picker on fill tool activation

* refactor/add fill attr to io clozes

* fill masks in editor

* fill text and inactive masks in reviewer

* fix lint

* remove debug option
2025-06-04 11:45:34 +07:00

263 lines
7.1 KiB
TypeScript

// Copyright: Ankitects Pty Ltd and contributors
// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
import { fabric } from "fabric";
import { get } from "svelte/store";
import { opacityStateStore } from "../store";
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;
export const drawPolygon = (canvas: fabric.Canvas): void => {
// remove selectable for shapes
canvas.discardActiveObject();
canvas.forEachObject(function(o) {
o.selectable = false;
});
canvas.selectionColor = "rgba(0, 0, 0, 0)";
canvas.on("mouse:down", function(options) {
try {
if (options.target && options.target["id"] === pointsList[0]["id"]) {
generatePolygon(canvas, pointsList);
} else {
addPoint(canvas, options);
}
} catch (e) {
// Cannot read properties of undefined (reading 'id')
}
});
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({
x2: pointer.x,
y2: pointer.y,
});
const points = activeShape.get("points");
points[pointsList.length] = {
x: pointer.x,
y: pointer.y,
};
activeShape.set({ points });
}
canvas.renderAll();
});
};
const toggleDrawPolygon = (canvas: fabric.Canvas): void => {
drawMode = !drawMode;
if (drawMode) {
activeLine = null;
activeShape = null;
linesList = [];
pointsList = [];
drawMode = false;
canvas.selection = true;
} else {
drawMode = true;
canvas.selection = false;
}
};
const addPoint = (canvas: fabric.Canvas, options): void => {
const pointer = canvas.getPointer(options.e);
const origX = pointer.x;
const origY = pointer.y;
if (!isPointerInBoundingBox(pointer)) {
return;
}
const point = new fabric.Circle({
radius: 5,
fill: "transparent",
stroke: "#333333",
strokeWidth: 1.5,
originX: "center",
originY: "center",
left: origX,
top: origY,
selectable: false,
hasBorders: false,
hasControls: false,
objectCaching: false,
perPixelTargetFind: false,
});
if (pointsList.length === 0) {
point.set({
stroke: "red",
});
}
const linePoints = [origX, origY, origX, origY];
const line = new fabric.Line(linePoints, {
strokeWidth: 2,
fill: "#999999",
stroke: "#999999",
originX: "center",
originY: "center",
selectable: false,
hasBorders: false,
hasControls: false,
evented: false,
objectCaching: false,
});
line["class"] = "line";
if (activeShape) {
const pointer = canvas.getPointer(options.e);
const points = activeShape.get("points");
points.push({
x: pointer.x,
y: pointer.y,
});
const polygon = new fabric.Polygon(points, {
stroke: "#333333",
strokeWidth: 1,
fill: "#cccccc",
opacity: 0.3,
selectable: false,
hasBorders: false,
hasControls: false,
evented: false,
objectCaching: false,
});
canvas.remove(activeShape);
canvas.add(polygon);
activeShape = polygon;
canvas.renderAll();
} else {
const polyPoint = [{ x: origX, y: origY }];
const polygon = new fabric.Polygon(polyPoint, {
stroke: "#333333",
strokeWidth: 1,
fill: "#cccccc",
opacity: 0.3,
selectable: false,
hasBorders: false,
hasControls: false,
evented: false,
objectCaching: false,
});
activeShape = polygon;
canvas.add(polygon);
}
activeLine = line;
pointsList.push(point);
linesList.push(line);
canvas.add(line);
canvas.add(point);
canvas.renderAll();
};
const generatePolygon = (canvas: fabric.Canvas, pointsList): void => {
const points: { x: number; y: number }[] = [];
pointsList.forEach((point) => {
points.push({
x: point.left,
y: point.top,
});
canvas.remove(point);
});
linesList.forEach((line) => {
canvas.remove(line);
});
canvas.remove(activeShape).remove(activeLine);
const polygon = new fabric.Polygon(points, {
fill: SHAPE_MASK_COLOR,
objectCaching: false,
stroke: BORDER_COLOR,
strokeWidth: 1,
strokeUniform: true,
noScaleCache: false,
selectable: false,
opacity: get(opacityStateStore) ? 0.4 : 1,
});
polygon["id"] = "polygon-" + new Date().getTime();
if (polygon.width! > 5 && polygon.height! > 5) {
canvas.add(polygon);
canvas.setActiveObject(polygon);
// view undo redo tools
undoStack.onObjectAdded(polygon["id"]);
}
toggleDrawPolygon(canvas);
};
// https://github.com/fabricjs/fabric.js/issues/6522
export const modifiedPolygon = (canvas: fabric.Canvas, polygon: fabric.Polygon): void => {
const matrix = polygon.calcTransformMatrix();
const transformedPoints = polygon.get("points")!
.map(function(p) {
return new fabric.Point(p.x - polygon.pathOffset.x, p.y - polygon.pathOffset.y);
})
.map(function(p) {
return fabric.util.transformPoint(p, matrix);
});
const polygon1 = new fabric.Polygon(transformedPoints, {
fill: polygon.fill ?? SHAPE_MASK_COLOR,
objectCaching: false,
stroke: BORDER_COLOR,
strokeWidth: 1,
strokeUniform: true,
noScaleCache: false,
opacity: get(opacityStateStore) ? 0.4 : 1,
});
polygon1["id"] = polygon["id"];
canvas.remove(polygon);
canvas.add(polygon1);
};
/**
* Removes the currently unfinished polygon, if any, and reset internal state
* @returns whether or not such a polygon was removed and state was reset
*/
export const removeUnfinishedPolygon = (canvas: fabric.Canvas): boolean => {
if (!activeShape) {
// generatePolygon should've already removed points/lines and reset state
return false;
}
canvas.remove(activeShape).remove(activeLine);
pointsList.forEach((point) => {
canvas.remove(point);
});
linesList.forEach((line) => {
canvas.remove(line);
});
activeLine = null;
activeShape = null;
linesList = [];
pointsList = [];
drawMode = false;
canvas.selection = true;
return true;
};