9e147c6335
* Add text tool to IO * Remove unnecessary parentheses * Fix text objects always grouped * Remove log * Fix text objects hidden on back side * Implement text scaling * Add inverse text outline * Warn about IO notes with only text objects This will result in a different error message than the case where no objects are added at all though, and the user can bypass the warning. Maybe this is better to avoid discarding the user's work if they have spent some time adding text. * Add isValidType * Use matches! * Lock aspect ratio of text objects * Reword misleading comment The confusion probably comes from the Fabric docs, which apparently need updating: http://fabricjs.com/docs/fabric.Canvas.html#uniformScaling * Do not count text objects when calculating current index * Make text objects respond to size changes * Fix uniform scaling not working when editing * Use Arial font * Escape colons and unify parsing * Handle scale factor when restricting shape to view * Use 'cloned' * Add text background * Tweak drawShape's params
260 lines
6.7 KiB
TypeScript
260 lines
6.7 KiB
TypeScript
// 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 type { PanZoom } from "panzoom";
|
|
import { get } from "svelte/store";
|
|
|
|
import { zoomResetValue } from "../store";
|
|
|
|
export const SHAPE_MASK_COLOR = "#ffeba2";
|
|
export const BORDER_COLOR = "#212121";
|
|
export const TEXT_FONT_FAMILY = "Arial";
|
|
export const TEXT_PADDING = 5;
|
|
|
|
let _clipboard;
|
|
|
|
export const stopDraw = (canvas: fabric.Canvas): void => {
|
|
canvas.off("mouse:down");
|
|
canvas.off("mouse:up");
|
|
canvas.off("mouse:move");
|
|
};
|
|
|
|
export const enableSelectable = (
|
|
canvas: fabric.Canvas,
|
|
select: boolean,
|
|
): void => {
|
|
canvas.selection = select;
|
|
canvas.forEachObject(function(o) {
|
|
o.selectable = select;
|
|
});
|
|
canvas.renderAll();
|
|
};
|
|
|
|
export const deleteItem = (canvas: fabric.Canvas): void => {
|
|
const active = canvas.getActiveObject();
|
|
if (active) {
|
|
canvas.remove(active);
|
|
if (active.type == "activeSelection") {
|
|
active.getObjects().forEach((x) => canvas.remove(x));
|
|
canvas.discardActiveObject().renderAll();
|
|
}
|
|
}
|
|
};
|
|
|
|
export const duplicateItem = (canvas: fabric.Canvas): void => {
|
|
if (!canvas.getActiveObject()) {
|
|
return;
|
|
}
|
|
copyItem(canvas);
|
|
pasteItem(canvas);
|
|
};
|
|
|
|
export const groupShapes = (canvas: fabric.Canvas): void => {
|
|
if (
|
|
!canvas.getActiveObject()
|
|
|| canvas.getActiveObject().type !== "activeSelection"
|
|
) {
|
|
return;
|
|
}
|
|
|
|
canvas.getActiveObject().toGroup();
|
|
canvas.requestRenderAll();
|
|
};
|
|
|
|
export const unGroupShapes = (canvas: fabric.Canvas): void => {
|
|
if (
|
|
!canvas.getActiveObject()
|
|
|| canvas.getActiveObject().type !== "group"
|
|
) {
|
|
return;
|
|
}
|
|
|
|
const group = canvas.getActiveObject();
|
|
const items = group.getObjects();
|
|
group._restoreObjectsState();
|
|
canvas.remove(group);
|
|
|
|
items.forEach((item) => {
|
|
canvas.add(item);
|
|
});
|
|
|
|
canvas.requestRenderAll();
|
|
};
|
|
|
|
export const zoomIn = (instance: PanZoom): void => {
|
|
instance.smoothZoom(0, 0, 1.25);
|
|
};
|
|
|
|
export const zoomOut = (instance: PanZoom): void => {
|
|
instance.smoothZoom(0, 0, 0.5);
|
|
};
|
|
|
|
export const zoomReset = (instance: PanZoom): void => {
|
|
instance.moveTo(0, 0);
|
|
instance.smoothZoomAbs(0, 0, get(zoomResetValue));
|
|
};
|
|
|
|
const copyItem = (canvas: fabric.Canvas): void => {
|
|
if (!canvas.getActiveObject()) {
|
|
return;
|
|
}
|
|
|
|
// clone what are you copying since you
|
|
// may want copy and paste on different moment.
|
|
// and you do not want the changes happened
|
|
// later to reflect on the copy.
|
|
canvas.getActiveObject().clone(function(cloned) {
|
|
_clipboard = cloned;
|
|
});
|
|
};
|
|
|
|
const pasteItem = (canvas: fabric.Canvas): void => {
|
|
// clone again, so you can do multiple copies.
|
|
_clipboard.clone(function(clonedObj) {
|
|
canvas.discardActiveObject();
|
|
|
|
clonedObj.set({
|
|
left: clonedObj.left + 10,
|
|
top: clonedObj.top + 10,
|
|
evented: true,
|
|
});
|
|
|
|
if (clonedObj.type === "activeSelection") {
|
|
// active selection needs a reference to the canvas.
|
|
clonedObj.canvas = canvas;
|
|
clonedObj.forEachObject(function(obj) {
|
|
canvas.add(obj);
|
|
});
|
|
|
|
// this should solve the unselectability
|
|
clonedObj.setCoords();
|
|
} else {
|
|
canvas.add(clonedObj);
|
|
}
|
|
|
|
_clipboard.top += 10;
|
|
_clipboard.left += 10;
|
|
canvas.setActiveObject(clonedObj);
|
|
canvas.requestRenderAll();
|
|
});
|
|
};
|
|
|
|
export const makeMaskTransparent = (
|
|
canvas: fabric.Canvas,
|
|
opacity = false,
|
|
): void => {
|
|
const objects = canvas.getObjects();
|
|
objects.forEach((object) => {
|
|
object.set({
|
|
opacity: opacity ? 0.4 : 1,
|
|
transparentCorners: false,
|
|
});
|
|
});
|
|
canvas.renderAll();
|
|
};
|
|
|
|
export const moveShapeToCanvasBoundaries = (canvas: fabric.Canvas): void => {
|
|
canvas.on("object:modified", function(o) {
|
|
const activeObject = o.target;
|
|
if (!activeObject) {
|
|
return;
|
|
}
|
|
if (
|
|
activeObject.type === "activeSelection"
|
|
|| activeObject.type === "rect"
|
|
) {
|
|
modifiedSelection(canvas, activeObject);
|
|
}
|
|
if (activeObject.type === "ellipse") {
|
|
modifiedEllipse(canvas, activeObject);
|
|
}
|
|
if (activeObject.type === "i-text") {
|
|
modifiedText(canvas, activeObject);
|
|
}
|
|
});
|
|
};
|
|
|
|
const modifiedSelection = (
|
|
canvas: fabric.Canvas,
|
|
object: fabric.Object,
|
|
): void => {
|
|
const newWidth = object.width * object.scaleX;
|
|
const newHeight = object.height * object.scaleY;
|
|
|
|
object.set({
|
|
width: newWidth,
|
|
height: newHeight,
|
|
scaleX: 1,
|
|
scaleY: 1,
|
|
});
|
|
setShapePosition(canvas, object);
|
|
};
|
|
|
|
const modifiedEllipse = (
|
|
canvas: fabric.Canvas,
|
|
object: fabric.Object,
|
|
): void => {
|
|
const newRx = object.rx * object.scaleX;
|
|
const newRy = object.ry * object.scaleY;
|
|
const newWidth = object.width * object.scaleX;
|
|
const newHeight = object.height * object.scaleY;
|
|
|
|
object.set({
|
|
rx: newRx,
|
|
ry: newRy,
|
|
width: newWidth,
|
|
height: newHeight,
|
|
scaleX: 1,
|
|
scaleY: 1,
|
|
});
|
|
setShapePosition(canvas, object);
|
|
};
|
|
|
|
const modifiedText = (canvas: fabric.Canvas, object: fabric.Object): void => {
|
|
setShapePosition(canvas, object);
|
|
};
|
|
|
|
const setShapePosition = (
|
|
canvas: fabric.Canvas,
|
|
object: fabric.Object,
|
|
): void => {
|
|
if (object.left < 0) {
|
|
object.set({ left: 0 });
|
|
}
|
|
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.top + object.height * object.scaleY + object.strokeWidth > canvas.height) {
|
|
object.set({ top: canvas.height - object.height * object.scaleY });
|
|
}
|
|
object.setCoords();
|
|
};
|
|
|
|
export function disableRotation(obj: fabric.Object): void {
|
|
obj.setControlsVisibility({
|
|
mtr: false,
|
|
});
|
|
}
|
|
|
|
export function enableUniformScaling(canvas: fabric.Canvas, obj: fabric.Object): void {
|
|
obj.setControlsVisibility({ mb: false, ml: false, mt: false, mr: false });
|
|
let timer: number;
|
|
obj.on("scaling", (e) => {
|
|
if (["bl", "br", "tr", "tl"].includes(e.transform.corner)) {
|
|
clearTimeout(timer);
|
|
canvas.uniformScaling = true;
|
|
timer = setTimeout(() => {
|
|
canvas.uniformScaling = false;
|
|
}, 500);
|
|
}
|
|
});
|
|
}
|
|
|
|
export function addBorder(obj: fabric.Object): void {
|
|
obj.stroke = BORDER_COLOR;
|
|
}
|