diff --git a/.inscode b/.inscode
index e1777316cc8a34b6c53f0ae12504b6c66fbdb272..53bd5672d18e835f6152f32196f1ee92bb055664 100644
--- a/.inscode
+++ b/.inscode
@@ -1,6 +1,9 @@
-run = "npm i && npm run dev"
language = "node"
+[deployment]
+build = "npm i && npm run build"
+run = "npm run preview"
+
[env]
PATH = "/root/${PROJECT_DIR}/.config/npm/node_global/bin:/root/${PROJECT_DIR}/node_modules/.bin:${PATH}"
XDG_CONFIG_HOME = "/root/.config"
diff --git a/basic.jpg b/basic.jpg
new file mode 100644
index 0000000000000000000000000000000000000000..e497340b42a6c7bebb67f51dbb883a0ae321528c
Binary files /dev/null and b/basic.jpg differ
diff --git a/index.d.ts b/index.d.ts
new file mode 100644
index 0000000000000000000000000000000000000000..534c632e59e8d8ec29bd0d6a52186956cdaca8f7
--- /dev/null
+++ b/index.d.ts
@@ -0,0 +1 @@
+export function setupCounter(element: HTMLButtonElement): void
diff --git a/index.html b/index.html
new file mode 100644
index 0000000000000000000000000000000000000000..0e64843f4a0b40995e34f7056196286d14802f67
--- /dev/null
+++ b/index.html
@@ -0,0 +1,20 @@
+
+
+
+
+
+
+ Vite App
+
+
+
+
+
+
+
diff --git a/index.js b/index.js
deleted file mode 100644
index 2d7e6834fb6366b3120c7a37cc5f637bc4a33928..0000000000000000000000000000000000000000
--- a/index.js
+++ /dev/null
@@ -1 +0,0 @@
-console.log("欢迎来到 InsCode");
\ No newline at end of file
diff --git a/package.json b/package.json
index 72caa1750a1c44c18460a496d258fbd3c51c673a..2f26c97742c268ea5ee3100afeabf6e832fc1ea8 100644
--- a/package.json
+++ b/package.json
@@ -1,18 +1,32 @@
{
- "name": "nodejs",
- "version": "1.0.0",
- "description": "",
- "main": "index.js",
- "scripts": {
- "dev": "node index.js",
- "test": "echo \"Error: no test specified\" && exit 1"
- },
- "keywords": [],
- "author": "",
- "license": "ISC",
- "dependencies": {
- "@types/node": "^18.0.6",
- "node-fetch": "^3.2.6"
- }
+ "name": "online-image-editor",
+ "private": true,
+ "version": "0.0.0",
+ "type": "module",
+ "files": [
+ "dist",
+ "index.d.ts"
+ ],
+ "main": "./dist/counter.umd.cjs",
+ "module": "./dist/counter.js",
+ "types": "./index.d.ts",
+ "exports": {
+ "types": "./index.d.ts",
+ "import": "./dist/counter.js",
+ "require": "./dist/counter.umd.cjs"
+ },
+ "scripts": {
+ "dev": "vite",
+ "build": "tsc && vite build"
+ },
+ "devDependencies": {
+ "typescript": "^5.4.5",
+ "vite": "^5.4.8",
+ "vite-plugin-copy": "^0.1.6",
+ "vite-plugin-static-copy": "^2.0.0"
+ },
+ "dependencies": {
+ "fabric": "^6.4.3",
+ "rollup-plugin-copy": "^3.5.0"
}
-
\ No newline at end of file
+}
diff --git a/src/assets/add.svg b/src/assets/add.svg
new file mode 100644
index 0000000000000000000000000000000000000000..10c074445e61cafd77785ff721724a4f4a2e264d
--- /dev/null
+++ b/src/assets/add.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/src/assets/arrow.svg b/src/assets/arrow.svg
new file mode 100644
index 0000000000000000000000000000000000000000..f29aa8e74eb20cf4cc3185594650b5de63cc2eb8
--- /dev/null
+++ b/src/assets/arrow.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/src/assets/cancel.svg b/src/assets/cancel.svg
new file mode 100644
index 0000000000000000000000000000000000000000..399fb03ba543428fed93ffe1ea9c48ae066bbbb2
--- /dev/null
+++ b/src/assets/cancel.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/src/assets/circle.svg b/src/assets/circle.svg
new file mode 100644
index 0000000000000000000000000000000000000000..3f16d19b41a96dc2d31aec5ae1ce6947a061bd7b
--- /dev/null
+++ b/src/assets/circle.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/src/assets/confirm.svg b/src/assets/confirm.svg
new file mode 100644
index 0000000000000000000000000000000000000000..8c3afe25e28f5f387cd1b6d5339fdb576bf0b489
--- /dev/null
+++ b/src/assets/confirm.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/src/assets/crop.svg b/src/assets/crop.svg
new file mode 100644
index 0000000000000000000000000000000000000000..1d88a1e8f4206943c1f065b52b2038871774b4e8
--- /dev/null
+++ b/src/assets/crop.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/src/assets/download.svg b/src/assets/download.svg
new file mode 100644
index 0000000000000000000000000000000000000000..c9c092b6f30daaef8f707d00eeaef92b4af50abe
--- /dev/null
+++ b/src/assets/download.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/src/assets/draw.svg b/src/assets/draw.svg
new file mode 100644
index 0000000000000000000000000000000000000000..f9cb1a266e2c1eaeba0ffc43e578f74600027607
--- /dev/null
+++ b/src/assets/draw.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/src/assets/extend.svg b/src/assets/extend.svg
new file mode 100644
index 0000000000000000000000000000000000000000..22a58b52551f76f096a5a689c7b97378adb3f9de
--- /dev/null
+++ b/src/assets/extend.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/src/assets/flip.svg b/src/assets/flip.svg
new file mode 100644
index 0000000000000000000000000000000000000000..7f99bfa191f74808e1d5b7ced65ba40f33478031
--- /dev/null
+++ b/src/assets/flip.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/src/assets/flipX.svg b/src/assets/flipX.svg
new file mode 100644
index 0000000000000000000000000000000000000000..2223d66b4539bc3a229f1d46482611b5f93e85eb
--- /dev/null
+++ b/src/assets/flipX.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/src/assets/flipY.svg b/src/assets/flipY.svg
new file mode 100644
index 0000000000000000000000000000000000000000..061f1085f57493911b4c1b800d97e4f5c4e47173
--- /dev/null
+++ b/src/assets/flipY.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/src/assets/hand.svg b/src/assets/hand.svg
new file mode 100644
index 0000000000000000000000000000000000000000..1e828b464c245d926f86b7a5502c413a458d8629
--- /dev/null
+++ b/src/assets/hand.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/src/assets/history.svg b/src/assets/history.svg
new file mode 100644
index 0000000000000000000000000000000000000000..8774fc6d64b3ed2c5d9ab87a7bb0afe0cdd98166
--- /dev/null
+++ b/src/assets/history.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/src/assets/mask.svg b/src/assets/mask.svg
new file mode 100644
index 0000000000000000000000000000000000000000..680ed44338ba7c80bebac37b3e8da815aa87e1b2
--- /dev/null
+++ b/src/assets/mask.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/src/assets/mosaic.svg b/src/assets/mosaic.svg
new file mode 100644
index 0000000000000000000000000000000000000000..7b930e751eb48e82a4bde09ea71a6c4f0480210c
--- /dev/null
+++ b/src/assets/mosaic.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/src/assets/rect.svg b/src/assets/rect.svg
new file mode 100644
index 0000000000000000000000000000000000000000..e87bb5df2fb51748219137fa9abbe0cedd87999a
--- /dev/null
+++ b/src/assets/rect.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/src/assets/redo.svg b/src/assets/redo.svg
new file mode 100644
index 0000000000000000000000000000000000000000..6cce26380aa0cb06697e486b765d61026556f819
--- /dev/null
+++ b/src/assets/redo.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/src/assets/reset.svg b/src/assets/reset.svg
new file mode 100644
index 0000000000000000000000000000000000000000..15d4981831fe6d03c76ed86cb58525280f171082
--- /dev/null
+++ b/src/assets/reset.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/src/assets/rotate.svg b/src/assets/rotate.svg
new file mode 100644
index 0000000000000000000000000000000000000000..497e6f1a6c1cf0ba3ed0da9460c4076b9fa6d029
--- /dev/null
+++ b/src/assets/rotate.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/src/assets/shrink.svg b/src/assets/shrink.svg
new file mode 100644
index 0000000000000000000000000000000000000000..44af73b241efef13470a52bee58840e3cede1158
--- /dev/null
+++ b/src/assets/shrink.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/src/assets/subtract.svg b/src/assets/subtract.svg
new file mode 100644
index 0000000000000000000000000000000000000000..ed9b0623913afd5823b92932ab92bb557f02f725
--- /dev/null
+++ b/src/assets/subtract.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/src/assets/text.svg b/src/assets/text.svg
new file mode 100644
index 0000000000000000000000000000000000000000..ebb382362262506f5a7fa2458ba1d4cfb3a73c58
--- /dev/null
+++ b/src/assets/text.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/src/assets/triangle.svg b/src/assets/triangle.svg
new file mode 100644
index 0000000000000000000000000000000000000000..df668de4e79fefc1f772a18497df01f15b480cfd
--- /dev/null
+++ b/src/assets/triangle.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/src/assets/undo.svg b/src/assets/undo.svg
new file mode 100644
index 0000000000000000000000000000000000000000..3b60e19ad006b2686055a4ff20243056fc1a7548
--- /dev/null
+++ b/src/assets/undo.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/src/assets/upload.svg b/src/assets/upload.svg
new file mode 100644
index 0000000000000000000000000000000000000000..92bec0c9e2676122e61332f304f71d607d1c8568
--- /dev/null
+++ b/src/assets/upload.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/src/assets/zoomIn.svg b/src/assets/zoomIn.svg
new file mode 100644
index 0000000000000000000000000000000000000000..87cbca74ec8047a9bf1ab7e298364b2b766dd198
--- /dev/null
+++ b/src/assets/zoomIn.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/src/assets/zoomOut.svg b/src/assets/zoomOut.svg
new file mode 100644
index 0000000000000000000000000000000000000000..038669d8f960aecd05ada266f829b39fc648fff0
--- /dev/null
+++ b/src/assets/zoomOut.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/src/element_manager.ts b/src/element_manager.ts
new file mode 100644
index 0000000000000000000000000000000000000000..2042594d1fdc572fe9425a337bbe84ebeb70d9ef
--- /dev/null
+++ b/src/element_manager.ts
@@ -0,0 +1,1661 @@
+import { FabricObject, Point } from "fabric";
+import { CanvasDetailedProps, FabricCanvasProps, FlipXUndoProps, FlipYUndoProps, RotateProps } from "./history";
+import ImageEditor from "./image_editor";
+import { OperatorProps, OperatorType } from "./image_editor_operator";
+import MosaicOperator from "./operator/mosaic_operator";
+import TextOperator from "./operator/text_operator";
+import { getAbsolutePosition } from "./uitls";
+
+const COLOR_MAP = {
+ RED: '#FF0000',
+ ORANGLE: '#FFA500',
+ BLUE: '#1A9BFF',
+ GREEN: '#1AAF19',
+ BLACK: '#323232',
+ GREY: '#808080',
+ WHITE: '#FFFFFF'
+}
+
+const DEFAULT_FUNCTION = () => { }
+
+export const pxielToNumber = (length: string) => {
+ if (length == null) {
+ return 0;
+ }
+ length = length.replace('px', '');
+ if (length == '') {
+ return 0;
+ }
+ return Number(length);
+}
+
+const toNumber = (str: string) => {
+ if (str == '') {
+ str = '0';
+ }
+ return Number(str);
+}
+
+export default class ElementManager {
+
+ public static HAS_CURSOR_CSS_ADDED = false;
+
+ private static COLOR_ACTIVE_FLAG = "color_in_active";
+
+ private static ACTIVE_SIZE_COLOR = '#1AAD19';
+
+ private static DEACTIVE_SIZE_COLOR = '#C8C8C8';
+
+ private imageEditor: ImageEditor | null = null;
+
+ readonly canvasWrapper: HTMLDivElement;
+ readonly canvas: HTMLCanvasElement;
+ private fabricWrapperEl: HTMLDivElement | null = null;
+ private northResizer: HTMLDivElement;
+ private northWestResizer: HTMLDivElement;
+ private westResizer: HTMLDivElement;
+ private southWestResizer: HTMLDivElement;
+ private southResizer: HTMLDivElement;
+ private southEastResizer: HTMLDivElement;
+ private eastResizer: HTMLDivElement;
+ private northEastResizer: HTMLDivElement;
+
+ private topInResize: boolean = false;
+ // fw Fabric Wrapper
+ private topChange = { y: NaN, top: NaN, height: NaN, fwTop: NaN, changeHeight: NaN };
+ private topStartFunc: (e: MouseEvent) => void = DEFAULT_FUNCTION;
+ private topMoveFunc: (e: MouseEvent) => void = DEFAULT_FUNCTION;
+ private topFinsihFunc: (e: MouseEvent) => void = DEFAULT_FUNCTION;
+ private leftInResize: boolean = false;
+ private leftChange = { x: NaN, left: NaN, width: NaN, fwLeft: NaN, changeWidth: NaN };
+ private leftStartFunc: (e: MouseEvent) => void = DEFAULT_FUNCTION;
+ private leftMoveFunc: (e: MouseEvent) => void = DEFAULT_FUNCTION;
+ private leftFinishFunc: (e: MouseEvent) => void = DEFAULT_FUNCTION;
+ private bottomInResize: boolean = false;
+ private bottomChange = { y: NaN, height: NaN, top: NaN, leftRightTop: NaN, changeHeight: NaN };
+ private bottomStartFunc: (e: MouseEvent) => void = DEFAULT_FUNCTION;
+ private bottomMoveFunc: (e: MouseEvent) => void = DEFAULT_FUNCTION;
+ private bottomFinishFunc: (e: MouseEvent) => void = DEFAULT_FUNCTION;
+ private rightInResize: boolean = false;
+ private rightChange = { x: NaN, width: NaN, left: NaN, topBottomLeft: NaN, changeWidth: NaN };
+ private rightStartFunc: (e: MouseEvent) => void = DEFAULT_FUNCTION;
+ private rightMoveFunc: (e: MouseEvent) => void = DEFAULT_FUNCTION;
+ private rightFinishFunc: (e: MouseEvent) => void = DEFAULT_FUNCTION;
+ private squareSize: number = NaN;
+
+ readonly wrapper: HTMLDivElement;
+
+ private screenshotCanvas: HTMLCanvasElement;
+ private screenshotResizer: {
+ northWest: HTMLDivElement,
+ north: HTMLDivElement,
+ northEast: HTMLDivElement,
+ east: HTMLDivElement,
+ southEast: HTMLDivElement,
+ south: HTMLDivElement,
+ southWest: HTMLDivElement,
+ west: HTMLDivElement
+ };
+
+ private screenshotToolbar: HTMLDivElement;
+ private screenshotConfirmButton: HTMLDivElement;
+ private screenshotCancelButton: HTMLDivElement;
+
+ private toolbar: HTMLDivElement;
+ private rectangleMenu: HTMLDivElement;
+ private ellipseMenu: HTMLDivElement;
+ private arrowMenu: HTMLDivElement;
+ private drawMenu: HTMLDivElement;
+ private textMenu: HTMLDivElement;
+ private mosaicMenu: HTMLDivElement;
+
+ private shrinkMenu: HTMLDivElement;
+ private extendMenu: HTMLDivElement;
+
+ private flipXMenu: HTMLDivElement;
+ private flipYMenu: HTMLDivElement;
+ private rotateClockwiseMenu: HTMLDivElement;
+ private rotateCounterClockwiseMenu: HTMLDivElement;
+ private cropMenu: HTMLDivElement;
+
+
+ private undoMenu: HTMLDivElement;
+ private redoMenu: HTMLDivElement;
+ private resetMenu: HTMLDivElement;
+ private cancelMenu: HTMLDivElement;
+ private confirmMenu: HTMLDivElement;
+ private optionBar: HTMLDivElement;
+
+ private small: HTMLSpanElement;
+ private normal: HTMLSpanElement;
+ private big: HTMLSpanElement;
+
+ private red: HTMLSpanElement;
+ private orangle: HTMLSpanElement;
+ private blue: HTMLSpanElement;
+ private green: HTMLSpanElement;
+ private black: HTMLSpanElement;
+ private white: HTMLSpanElement;
+ private grey: HTMLSpanElement;
+
+ private sizeOptions: HTMLSpanElement;
+ private colorOptions: HTMLSpanElement;
+ private optionArrow: HTMLDivElement;
+
+ private menuMap = new Map();
+ private eleColorMap = new Map();
+ private colorEleMap = new Map();
+
+ constructor(options: any) {
+
+ this.wrapper = options.wrapper;
+ this.canvas = options.canvas;
+
+ this.screenshotCanvas = options.screenshotCanvas;
+ this.screenshotResizer = options.screenshotResizer;
+ this.screenshotToolbar = options.screenshotToolbar.toolbar;
+ this.screenshotConfirmButton = options.screenshotToolbar.screenshot.confirm;
+ this.screenshotCancelButton = options.screenshotToolbar.screenshot.cancel;
+
+ this.canvasWrapper = options.canvasWrapper;
+ this.northResizer = options.northResizer;
+ this.northWestResizer = options.northWestResizer;
+ this.westResizer = options.westResizer;
+ this.southWestResizer = options.southWestResizer;
+ this.southResizer = options.southResizer;
+ this.southEastResizer = options.southEastResizer;
+ this.eastResizer = options.eastResizer;
+ this.northEastResizer = options.northEastResizer;
+
+ this.fixResizerPosition();
+
+ this.squareSize = this.southResizer.getBoundingClientRect().width;
+
+ this.toolbar = options.toolbar;
+
+ this.rectangleMenu = options.rectangleMenu;
+ this.menuMap.set(OperatorType.RECT, this.rectangleMenu);
+ this.ellipseMenu = options.ellipseMenu;
+ this.menuMap.set(OperatorType.ELLIPSE, this.ellipseMenu);
+ this.arrowMenu = options.arrowMenu;
+ this.menuMap.set(OperatorType.ARROW, this.arrowMenu);
+ this.drawMenu = options.drawMenu;
+ this.menuMap.set(OperatorType.DRAW, this.drawMenu);
+ this.textMenu = options.textMenu;
+ this.menuMap.set(OperatorType.TEXT, this.textMenu);
+ this.mosaicMenu = options.mosaicMenu;
+ this.menuMap.set(OperatorType.MOSAIC, this.mosaicMenu);
+
+ this.shrinkMenu = options.shrinkMenu;
+ this.extendMenu = options.extendMenu;
+ this.flipXMenu = options.flipXMenu;
+ this.flipYMenu = options.flipYMenu;
+ this.rotateClockwiseMenu = options.rotateClockwiseMenu;
+ this.rotateCounterClockwiseMenu = options.rotateCounterClockwiseMenu;
+ this.cropMenu = options.cropMenu;
+
+ this.undoMenu = options.undoMenu;
+ this.redoMenu = options.redoMenu;
+ this.resetMenu = options.resetMenu;
+ this.cancelMenu = options.cancelMenu;
+ this.confirmMenu = options.confirmMenu;
+
+ const ele = this.createOperatorOptionBar();
+ this.optionBar = ele.optionBar;
+ this.small = ele.small;
+ this.normal = ele.normal;
+ this.big = ele.big;
+ this.red = ele.red;
+ this.orangle = ele.orangle;
+ this.green = ele.green;
+ this.blue = ele.blue;
+ this.black = ele.black;
+ this.white = ele.white;
+ this.grey = ele.grey;
+
+ this.sizeOptions = ele.sizeOptions;
+ this.colorOptions = ele.colorOptions;
+ this.optionArrow = ele.arrow;
+
+ this.eleColorMap.set(this.red, COLOR_MAP.RED);
+ this.eleColorMap.set(this.orangle, COLOR_MAP.ORANGLE);
+ this.eleColorMap.set(this.green, COLOR_MAP.GREEN);
+ this.eleColorMap.set(this.blue, COLOR_MAP.BLUE);
+ this.eleColorMap.set(this.black, COLOR_MAP.BLACK);
+ this.eleColorMap.set(this.white, COLOR_MAP.WHITE);
+ this.eleColorMap.set(this.grey, COLOR_MAP.GREY);
+
+ this.colorEleMap.set(COLOR_MAP.RED, this.red);
+ this.colorEleMap.set(COLOR_MAP.ORANGLE, this.orangle);
+ this.colorEleMap.set(COLOR_MAP.GREEN, this.green);
+ this.colorEleMap.set(COLOR_MAP.BLUE, this.black);
+ this.colorEleMap.set(COLOR_MAP.BLACK, this.black);
+ this.colorEleMap.set(COLOR_MAP.WHITE, this.white);
+ this.colorEleMap.set(COLOR_MAP.GREY, this.grey);
+ }
+
+ init(imageEditor: ImageEditor) {
+ this.imageEditor = imageEditor;
+ this.fabricWrapperEl = imageEditor.getCanvas().wrapperEl;
+ this.initResizers();
+ this.fixToolbarPosition();
+ this.appendHoverCSS();
+ }
+
+ appendHoverCSS() {
+ if (ElementManager.HAS_CURSOR_CSS_ADDED) {
+ return;
+ }
+ const style = document.createElement('style');
+ const css = `
+ .north-cursor-resize:hover, .south-cursor-resize:hover{
+ cursor: ns-resize;
+ }
+
+ .west-cursor-resize:hover, .east-cursor-resize:hover{
+ cursor: ew-resize;
+ }
+
+ .north-east-cursor-resize:hover, .south-west-cursor-resize:hover{
+ cursor: nesw-resize;
+ }
+
+ .north-west-cursor-resize:hover, .south-east-cursor-resize:hover{
+ cursor: nwse-resize;
+ }
+ `
+ style.appendChild(document.createTextNode(css));
+ document.head.appendChild(style);
+ ElementManager.HAS_CURSOR_CSS_ADDED = true;
+
+ this.northResizer.classList.add('north-cursor-resize');
+ this.westResizer.classList.add('west-cursor-resize');
+ this.southResizer.classList.add('south-cursor-resize');
+ this.eastResizer.classList.add('east-cursor-resize');
+
+ this.screenshotResizer.north.classList.add('north-cursor-resize');
+ this.screenshotResizer.northWest.classList.add('north-west-cursor-resize');
+ this.screenshotResizer.west.classList.add('west-cursor-resize');
+ this.screenshotResizer.southWest.classList.add('south-west-cursor-resize');
+ this.screenshotResizer.south.classList.add('south-cursor-resize');
+ this.screenshotResizer.southEast.classList.add('south-east-cursor-resize');
+ this.screenshotResizer.east.classList.add('east-cursor-resize');
+ this.screenshotResizer.northEast.classList.add('north-east-cursor-resize');
+ }
+
+ createOperatorOptionBar() {
+ const wrapper = document.createElement("div");
+ // 默认隐藏
+ wrapper.style.display = 'none';
+ wrapper.style.backgroundColor = 'white';
+ wrapper.style.position = 'absolute';
+ wrapper.style.borderRadius = '4px';
+ // 解决行高预留空白问题
+ wrapper.style.fontSize = '0';
+ const sizeOptions = document.createElement("span");
+ const colorOptions = document.createElement("span");
+ sizeOptions.style.display = 'inline-block';
+ colorOptions.style.display = 'inline-block';
+ wrapper.append(sizeOptions);
+ wrapper.append(colorOptions);
+ const small = document.createElement("span");
+ small.style.width = '8px';
+ small.style.height = '8px';
+ small.style.margin = '16px 0 16px 16px';
+ small.style.backgroundColor = ElementManager.DEACTIVE_SIZE_COLOR;
+ small.style.display = 'inline-block';
+ small.style.borderRadius = '50%';
+ const normal = document.createElement("span");
+ normal.style.width = '12px';
+ normal.style.height = '12px';
+ normal.style.margin = '14px 0 14px 14px';
+ normal.style.backgroundColor = ElementManager.DEACTIVE_SIZE_COLOR;
+ normal.style.display = 'inline-block';
+ normal.style.borderRadius = '50%';
+ const big = document.createElement("span");
+ big.style.width = '16px';
+ big.style.height = '16px';
+ big.style.margin = '12px 16px 12px 14px';
+ big.style.backgroundColor = ElementManager.DEACTIVE_SIZE_COLOR;
+ big.style.display = 'inline-block';
+ big.style.borderRadius = '50%';
+ sizeOptions.append(small);
+ small.classList.add('online-image-editor-operator-option');
+ sizeOptions.append(normal);
+ normal.classList.add('online-image-editor-operator-option');
+ sizeOptions.append(big);
+ big.classList.add('online-image-editor-operator-option');
+
+ const red = document.createElement("span");
+ red.style.backgroundColor = COLOR_MAP.RED;
+ const orangle = document.createElement("span");
+ orangle.style.backgroundColor = COLOR_MAP.ORANGLE;
+ const blue = document.createElement("span");
+ blue.style.backgroundColor = COLOR_MAP.BLUE;
+ const green = document.createElement("span");
+ green.style.backgroundColor = COLOR_MAP.GREEN;
+ const black = document.createElement("span");
+ black.style.backgroundColor = COLOR_MAP.BLACK;
+ const white = document.createElement("span");
+ white.style.backgroundColor = COLOR_MAP.WHITE;
+ const grey = document.createElement("span");
+ grey.style.backgroundColor = COLOR_MAP.GREY;
+ colorOptions.append(red);
+ colorOptions.append(orangle);
+ colorOptions.append(blue);
+ colorOptions.append(green);
+ colorOptions.append(black);
+ colorOptions.append(white);
+ colorOptions.append(grey);
+ const colors = [red, orangle, blue, green, black, white, grey];
+
+ for (const color of colors) {
+ const style = color.style;
+ style.display = 'inline-block';
+ style.width = '20px';
+ style.height = '20px';
+ style.margin = '10px 0 10px 8px';
+ style.boxSizing = 'border-box';
+ color.classList.add('online-image-editor-operator-option');
+ }
+
+ red.style.margin = '10px 0 10px 0';
+ grey.style.marginRight = '8px';
+ white.style.border = 'solid 1px #E6E6E6';
+ white.style.boxSizing = 'border-box';
+
+ const arrowWrapper = document.createElement('div');
+ const arrow = document.createElement('div');
+ arrow.style.position = 'absolute';
+ arrow.style.left = '142px';
+ arrow.style.top = '-8px';
+ arrow.style.borderTopWidth = '0';
+ arrow.classList.add('online-image-editor-operator-option-arrow');
+ arrowWrapper.append(arrow);
+ wrapper.append(arrowWrapper);
+
+ const style = document.createElement('style');
+ const css = `
+ .online-image-editor-operator-option:hover{
+ cursor: pointer;
+ }
+
+ .online-image-editor-operator-option-arrow:after{
+ position: absolute;
+ width: 0;
+ height: 0;
+ border-color: transparent;
+ border-style: solid;
+ border-width: 8px;
+ content: "";
+ top:1px;
+ margin-left:-8px;
+ border-top-width:0;
+ border-bottom-color: #FFF;
+ }
+ `
+ style.appendChild(document.createTextNode(css));
+ document.head.appendChild(style);
+ document.body.append(wrapper);
+ return {
+ optionBar: wrapper,
+ small, normal, big, red, orangle, green, blue, black, white, grey,
+ sizeOptions, colorOptions, arrow
+ };
+ }
+
+ bindEvents() {
+
+ const imageEditor = this.imageEditor!;
+
+ this.rectangleMenu.onclick = () => { this.switchOperator(OperatorType.RECT) };
+ this.ellipseMenu.onclick = () => { this.switchOperator(OperatorType.ELLIPSE) };
+ this.arrowMenu.onclick = () => { this.switchOperator(OperatorType.ARROW) };
+ this.drawMenu.onclick = () => { this.switchOperator(OperatorType.DRAW) };
+ this.mosaicMenu.onclick = () => { this.switchOperator(OperatorType.MOSAIC) };
+ this.textMenu.onclick = () => { this.switchOperator(OperatorType.TEXT) };
+
+ this.shrinkMenu.onclick = () => { this.shrinkCanvasToBackgroundImage(); }
+ this.extendMenu.onclick = () => { this.extendsCanvas(); }
+
+ this.flipXMenu.onclick = () => { this.flipHorizontal(); }
+ this.flipYMenu.onclick = () => { this.flipVertical(); }
+ this.rotateClockwiseMenu.onclick = () => { this.rotateClockwise(); }
+ this.rotateCounterClockwiseMenu.onclick = () => { this.rotateCounterClockwise(); }
+ this.cropMenu.onclick = () => { this.cropImage(); }
+
+ this.undoMenu.onclick = () => { imageEditor.getHistory().undo(); }
+ this.redoMenu.onclick = () => { imageEditor.getHistory().redo(); }
+
+ this.resetMenu.onclick = () => { this.resetImageEditor(); }
+
+ this.confirmMenu.onclick = () => { this.downloadAreaImage(); }
+ }
+
+
+
+ switchOperator(type: OperatorType) {
+ const imageEditor = this.imageEditor!;
+ const previous = imageEditor.getOperatorType();
+ if (imageEditor.getOperatorType() == type) {
+ imageEditor.changeOperatorType(OperatorType.NONE);
+ } else {
+ imageEditor.changeOperatorType(type);
+ }
+ const current = imageEditor.getOperatorType();
+ if (previous != OperatorType.NONE) {
+ const preEle = this.menuMap.get(previous);
+ preEle.style.backgroundColor = 'transparent';
+ this.hideOptionBar();
+ }
+ if (current != OperatorType.NONE) {
+ const currEle = this.menuMap.get(current);
+ currEle.style.backgroundColor = '#FFF';
+ this.showOptionBar(currEle);
+ }
+ }
+
+ hideOptionBar() {
+ this.optionBar.style.display = 'none';
+ }
+
+ showOptionBarDirect() {
+ this.optionBar.style.display = 'inline-block';
+ }
+
+ showOptionBar(currEle: HTMLDivElement) {
+ const imageEditor = this.imageEditor!;
+ this.adjustOptionBarPosition(currEle)
+ const operator = imageEditor.getActiveOperator();
+ // 马赛克不显示颜色选项
+ const isMosaic = operator instanceof MosaicOperator;
+ if (!isMosaic) {
+ this.showFullOptions();
+ const that = this;
+ const color = operator.getOperatorColor();
+ const eles = this.eleColorMap.keys();
+ for (const ele of eles) {
+ const eleColor = this.eleColorMap.get(ele);
+ if (eleColor == color) {
+ this.activeColor(ele);
+ } else {
+ this.deactiveColor(ele);
+ }
+ }
+
+ this.red.onclick = () => { that.changeColor(operator, COLOR_MAP.RED, that.red) };
+ this.orangle.onclick = () => { that.changeColor(operator, COLOR_MAP.ORANGLE, that.orangle) };
+ this.green.onclick = () => { that.changeColor(operator, COLOR_MAP.GREEN, that.green) };
+ this.blue.onclick = () => { that.changeColor(operator, COLOR_MAP.BLUE, that.blue) };
+ this.black.onclick = () => { that.changeColor(operator, COLOR_MAP.BLACK, that.black) };
+ this.white.onclick = () => { that.changeColor(operator, COLOR_MAP.WHITE, that.white) };
+ this.grey.onclick = () => { that.changeColor(operator, COLOR_MAP.GREY, that.grey) };
+ } else {
+ this.showSizeOptions();
+ }
+
+ let s = 2, n = 4, b = 6;
+ if (operator instanceof MosaicOperator) {
+ s = 10, n = 20, b = 40;
+ } else if (operator instanceof TextOperator) {
+ s = 15, n = 20, b = 25;
+ }
+ const size = operator.getOperatorSize();
+ switch (size) {
+ case s: this.selectSize(this.small); break;
+ case n: this.selectSize(this.normal); break;
+ case b: this.selectSize(this.big); break;
+ }
+ const that = this;
+ this.small.onclick = () => { operator.setOperatorSize(s); that.selectSize(that.small); }
+ this.normal.onclick = () => { operator.setOperatorSize(n); that.selectSize(that.normal); }
+ this.big.onclick = () => { operator.setOperatorSize(b); that.selectSize(that.big); }
+ }
+
+ showSizeOptions() {
+ const isColorVisiable = this.imageEditor!.getOperatorType() != OperatorType.MOSAIC;
+ if (!isColorVisiable) {
+ this.colorOptions.style.display = 'none';
+ // 196 / 2,196是整个颜色区域的宽度,去除之后,大小选择框,要向右移动这么多
+ let left = Number(this.optionBar.style.left.replace('px', ''));
+ left = left + (196) / 2;
+ let width = this.optionBar.getBoundingClientRect().width;
+ this.optionBar.style.left = left + 'px';
+ const arrowLeft = width / 2;
+ this.optionArrow.style.left = arrowLeft + 'px';
+ }
+ }
+ showFullOptions() {
+ const isColorVisiable = this.colorOptions.style.display == 'inline-block';
+ if (!isColorVisiable) {
+ this.colorOptions.style.display = 'inline-block';
+ let arrowLeft = Number(this.optionArrow.style.left.replace('px', ''));
+ arrowLeft = arrowLeft + 168 / 2;
+ this.optionArrow.style.left = arrowLeft + 'px';
+ }
+ }
+
+ changeColor(operator: OperatorProps, color: string, ele: HTMLSpanElement) {
+ operator.setOperatorColor(color);
+ const colors = [this.red, this.orangle, this.green, this.blue, this.black, this.white, this.grey];
+ for (const color of colors) {
+ if (color == ele) {
+ this.activeColor(color);
+ } else {
+ this.deactiveColor(color);
+ }
+ }
+ }
+
+ activeColor(ele: HTMLSpanElement) {
+ ele.setAttribute(ElementManager.COLOR_ACTIVE_FLAG, 'true');
+ if (ele != this.white) {
+ ele.style.border = '6px solid ' + this.eleColorMap.get(ele);
+ ele.style.backgroundColor = 'white';
+ } else {
+ ele.style.border = '6px solid #E6E6E6';
+ ele.style.backgroundColor = 'white';
+ }
+ }
+
+ deactiveColor(ele: HTMLSpanElement) {
+ if (ele.getAttribute(ElementManager.COLOR_ACTIVE_FLAG)) {
+ if (ele != this.white) {
+ ele.style.border = '0';
+ ele.style.backgroundColor = this.eleColorMap.get(ele);
+ } else {
+ ele.style.border = 'solid 1px #E6E6E6';
+ ele.style.backgroundColor = 'white';
+ }
+ ele.removeAttribute(ElementManager.COLOR_ACTIVE_FLAG);
+ }
+ }
+
+ selectSize(ele: HTMLSpanElement) {
+ const sizes = [this.small, this.normal, this.big];
+ for (const size of sizes) {
+ if (ele == size) {
+ this.activeSize(size);
+ } else {
+ this.deactiveSize(size);
+ }
+ }
+ }
+
+ activeSize(ele: HTMLSpanElement) {
+ ele.style.backgroundColor = ElementManager.ACTIVE_SIZE_COLOR;
+ }
+
+ deactiveSize(ele: HTMLSpanElement) {
+ ele.style.backgroundColor = ElementManager.DEACTIVE_SIZE_COLOR;
+ }
+
+ hideToolbar() {
+ this.toolbar.style.display = 'none';
+ this.hideOptionBar();
+ }
+
+ showToolbar() {
+ this.toolbar.style.display = 'block';
+ const optType = this.imageEditor!.getOperatorType();
+ // 对于要显示的toolbar,直接显示
+ if (optType != OperatorType.NONE) {
+ this.showOptionBarDirect();
+ }
+ this.fixToolbarPosition();
+ }
+
+ initResizers() {
+
+ const that = this;
+
+ /********************** 开始处理头部的拉伸箭头 *****************/
+ this.northResizer.removeEventListener('mousedown', this.topStartFunc)
+ window.removeEventListener('mousemove', this.topMoveFunc);
+ window.removeEventListener('mouseup', this.topFinsihFunc);
+
+ this.topStartFunc = (event: MouseEvent) => {
+ that.topInResize = true;
+ let fwTop = this.fabricWrapperEl!.style.top.replace('px', '');
+ if (fwTop == '') {
+ fwTop = '0';
+ }
+ const top = this.canvasWrapper.style.top.replace('px', '');
+ const height = this.canvasWrapper.style.height.replace('px', '');
+ that.topChange.y = event.pageY;
+ that.topChange.top = Number(top);
+ that.topChange.height = Number(height);
+ that.topChange.fwTop = Number(fwTop);
+ that.topChange.changeHeight = 0;
+ const body = document.querySelector('body');
+ body!.style.cursor = 'n-resize'
+ that.hideToolbar();
+ }
+
+ this.topMoveFunc = (event: MouseEvent) => {
+ if (!that.topInResize) return;
+ that.changeHeightFromTop(event.pageY);
+ }
+ this.topFinsihFunc = (_event: MouseEvent) => {
+ if (!that.topInResize) {
+ return;
+ }
+ that.finishResize();
+ that.showToolbar();
+ }
+
+ this.northResizer.addEventListener('mousedown', this.topStartFunc)
+ window.addEventListener('mousemove', this.topMoveFunc);
+ window.addEventListener('mouseup', this.topFinsihFunc);
+
+ /********************** 开始处理左侧的拉伸箭头 *****************/
+ this.westResizer.removeEventListener('mousedown', this.leftStartFunc)
+ window.removeEventListener('mousemove', this.leftMoveFunc);
+ window.removeEventListener('mouseup', this.leftFinishFunc);
+
+ this.leftStartFunc = (event: MouseEvent) => {
+ that.leftInResize = true;
+ let fwLeft = this.fabricWrapperEl!.style.left.replace('px', '');
+ if (fwLeft == '') {
+ fwLeft = '0';
+ }
+ let left = this.canvasWrapper.style.left.replace('px', '');
+ const width = this.canvasWrapper.style.width.replace('px', '')
+ that.leftChange.x = event.pageX;
+ that.leftChange.left = Number(left);
+ that.leftChange.width = Number(width);
+ that.leftChange.fwLeft = Number(fwLeft);
+ that.leftChange.changeWidth = 0;
+ const body = document.querySelector('body');
+ body!.style.cursor = 'w-resize'
+ that.hideToolbar();
+ }
+
+ this.leftMoveFunc = (event: MouseEvent) => {
+ if (!that.leftInResize) return;
+ that.changeHeightFromLeft(event.pageX);
+ }
+ this.leftFinishFunc = (_event: MouseEvent) => {
+ if (!that.leftInResize) return;
+ that.finishResize();
+ that.showToolbar();
+ }
+
+ this.westResizer.addEventListener('mousedown', this.leftStartFunc)
+ window.addEventListener('mousemove', this.leftMoveFunc);
+ window.addEventListener('mouseup', this.leftFinishFunc);
+
+ /********************** 开始处理底部的拉伸箭头 *****************/
+
+ this.southResizer.removeEventListener('mousedown', this.bottomStartFunc)
+ window.removeEventListener('mousemove', this.bottomMoveFunc);
+ window.removeEventListener('mouseup', this.bottomFinishFunc);
+
+ this.bottomStartFunc = (event: MouseEvent) => {
+ that.bottomInResize = true;
+ const height = this.canvasWrapper.style.height.replace('px', '');
+ const top = this.southResizer.style.top.replace('px', '');
+ const leftRightTop = this.westResizer.style.top.replace('px', '');
+ that.bottomChange.height = Number(height);
+ that.bottomChange.y = event.pageY;
+ that.bottomChange.top = Number(top);
+ that.bottomChange.leftRightTop = Number(leftRightTop);
+ that.bottomChange.changeHeight = 0;
+ const body = document.querySelector('body');
+ body!.style.cursor = 's-resize'
+ that.hideToolbar();
+ }
+
+ this.bottomMoveFunc = (event: MouseEvent) => {
+ if (!that.bottomInResize) return;
+ that.changeHeightFromBottom(event.pageY);
+ }
+ this.bottomFinishFunc = (_event: MouseEvent) => {
+ if (!that.bottomInResize) return;
+ that.finishResize();
+ that.showToolbar();
+ }
+
+ this.southResizer.addEventListener('mousedown', this.bottomStartFunc)
+ window.addEventListener('mousemove', this.bottomMoveFunc);
+ window.addEventListener('mouseup', this.bottomFinishFunc);
+
+ /********************** 开始处理右侧的拉伸箭头 *****************/
+
+ this.southResizer.removeEventListener('mousedown', this.rightStartFunc)
+ window.removeEventListener('mousemove', this.rightMoveFunc);
+ window.removeEventListener('mouseup', this.rightFinishFunc);
+
+ this.rightStartFunc = (event: MouseEvent) => {
+ that.rightInResize = true;
+ const width = this.canvasWrapper.style.width.replace('px', '');
+ const left = this.eastResizer.style.left.replace('px', '');
+ const topBottomLeft = this.northResizer.style.left.replace('px', '');
+ that.rightChange.width = Number(width)
+ that.rightChange.x = event.pageX;
+ that.rightChange.left = Number(left);
+ that.rightChange.topBottomLeft = Number(topBottomLeft);
+ that.rightChange.changeWidth = 0;
+ const body = document.querySelector('body');
+ body!.style.cursor = 'e-resize'
+ that.hideToolbar();
+ }
+
+ this.rightMoveFunc = (event: MouseEvent) => {
+ if (!that.rightInResize) return;
+ that.changeWidthFromRight(event.pageX);
+ }
+ this.rightFinishFunc = (_event: MouseEvent) => {
+ if (!that.rightInResize) return;
+ that.finishResize();
+ that.showToolbar();
+ }
+
+ this.eastResizer.addEventListener('mousedown', this.rightStartFunc)
+ window.addEventListener('mousemove', this.rightMoveFunc);
+ window.addEventListener('mouseup', this.rightFinishFunc);
+
+ /********************** 拉伸箭头的部分处理结束 *****************/
+ }
+
+ changeHeightFromLeft(pageX: number) {
+ const currentX = this.leftChange.x;
+ const oldLeft = this.leftChange.left;
+ const oldWidth = this.leftChange.width;
+
+ // 用当前Y值,减去开始的Y值,得到的一段长度是top增加的值,和高度减少的值
+ let changedWidth = pageX - currentX;
+ let newWidth = oldWidth - changedWidth;
+
+ if (newWidth < 80) {
+ newWidth = 80;
+ changedWidth = oldWidth - newWidth;
+ }
+
+ const newLeft = Number(oldLeft) + changedWidth;
+ const newFwLeft = this.leftChange.fwLeft - changedWidth;
+
+ this.canvasWrapper.style.width = newWidth + 'px';
+ this.canvasWrapper.style.left = newLeft + 'px';
+ this.westResizer.style.left = (newLeft - this.squareSize) + 'px';
+ this.fabricWrapperEl!.style.left = newFwLeft + 'px';
+ this.leftChange.changeWidth = changedWidth;
+ this.fixResizerPosition();
+ }
+
+ changeHeightFromTop(pageY: number) {
+ const currentY = this.topChange.y;
+ const oldTop = this.topChange.top;
+ const oldHeight = this.topChange.height;
+
+ // 用当前Y值,减去开始的Y值,得到的一段长度是top增加的值,和高度减少的值
+ let changedHeight = pageY - currentY;
+ let newHeight = oldHeight - changedHeight;
+ if (newHeight < 80) {
+ newHeight = 80;
+ changedHeight = oldHeight - newHeight;
+ }
+
+ const newTop = Number(oldTop) + changedHeight;
+ const newFwTop = this.topChange.fwTop - changedHeight;
+
+ this.canvasWrapper.style.height = newHeight + 'px';
+ this.canvasWrapper.style.top = newTop + 'px';
+ this.northResizer.style.top = (newTop - this.squareSize) + 'px';
+ this.fabricWrapperEl!.style.top = (newFwTop) + 'px';
+ this.topChange.changeHeight = changedHeight;
+
+ this.fixResizerPosition();
+ }
+
+ changeHeightFromBottom(pageY: number) {
+ const currentY = this.bottomChange.y;
+ const oldHeight = this.bottomChange.height;
+ let changedHeight = pageY - currentY;
+ let newHeight = oldHeight + changedHeight;
+ // 给个最小值
+ if (newHeight < 80) {
+ newHeight = 80;
+ changedHeight = newHeight - oldHeight;
+ }
+ this.canvasWrapper.style.height = newHeight + 'px';
+ this.bottomChange.changeHeight = changedHeight;
+ this.fixResizerPosition();
+ }
+
+ changeWidthFromRight(pageX: number) {
+ const currentY = this.rightChange.x;
+ const oldWidth = this.rightChange.width;
+ let changedWidth = pageX - currentY;
+ let newWidth = Number(oldWidth) + changedWidth;
+ if (newWidth < 80) {
+ newWidth = 80;
+ changedWidth = newWidth - oldWidth;
+ }
+ const newLeft = this.rightChange.left + changedWidth;
+ this.canvasWrapper.style.width = newWidth + 'px';
+ this.eastResizer.style.left = newLeft + 'px';
+ // bottom和top也要跟着同步改变
+ this.northResizer.style.left = this.rightChange.topBottomLeft + (changedWidth / 2) + 'px';
+ this.southResizer.style.left = this.rightChange.topBottomLeft + (changedWidth / 2) + 'px';
+ this.rightChange.changeWidth = changedWidth;
+
+ this.fixResizerPosition();
+ }
+
+ finishResize() {
+ const body = document.querySelector('body');
+ body!.style.cursor = 'default'
+ if (this.topInResize) {
+ this.topInResize = false;
+ this.expandTopToBaseMap();
+ }
+ if (this.bottomInResize) {
+ this.bottomInResize = false;
+ this.expandBottomToBaseMap();
+ }
+ if (this.leftInResize) {
+ this.leftInResize = false;
+ this.expandLeftToBaseMap();
+ }
+ if (this.rightInResize) {
+ this.rightInResize = false;
+ this.expandRightToBaseMap();
+ }
+ }
+
+ expandTopToBaseMap() {
+ let fabricTopStr = this.fabricWrapperEl?.style.top.replace('px', '');
+ let fabricHeightStr = this.fabricWrapperEl?.style.height.replace('px', '');
+ if (fabricTopStr == '') {
+ fabricTopStr = '0';
+ }
+ const fabricTop = Number(fabricTopStr);
+ const fabricHeight = Number(fabricHeightStr);
+ // 小于0不用考虑,大于0要考虑,将大于0部分的宽度扩展出来
+ if (fabricTop > 0) {
+ this.fabricWrapperEl!.style.top = '0';
+ const newHeight = fabricHeight + fabricTop;
+ this.imageEditor!.setCanvasHeight(newHeight);
+ this.imageEditor!.transformY(fabricTop);
+ }
+ }
+
+ expandLeftToBaseMap() {
+ let fabricLeftStr = this.fabricWrapperEl?.style.left.replace('px', '');
+ let fabricWidthStr = this.fabricWrapperEl?.style.width.replace('px', '');
+ if (fabricLeftStr == '') {
+ fabricLeftStr = '0';
+ }
+ const fabricLeft = Number(fabricLeftStr);
+ const fabricWidth = Number(fabricWidthStr);
+ // 小于0不用考虑,大于0要考虑,将大于0部分的宽度扩展出来
+ if (fabricLeft > 0) {
+ this.fabricWrapperEl!.style.left = '0';
+ const newWidth = fabricWidth + fabricLeft;
+ this.imageEditor!.setCanvasWidth(newWidth);
+ this.imageEditor!.transformX(fabricLeft);
+ }
+ }
+
+ expandBottomToBaseMap() {
+ let fabricTopStr = this.fabricWrapperEl?.style.top.replace('px', '');
+ if (fabricTopStr == '') {
+ fabricTopStr = '0'
+ }
+ const fabricTop = Number(fabricTopStr);
+ const fabricHeightStr = this.fabricWrapperEl?.style.height.replace('px', '');
+ const fabircHeight = Number(fabricHeightStr);
+ const heightExcludeLeftTop = fabircHeight + fabricTop;
+ const wrapperHeightStr = this.canvasWrapper.style.height.replace('px', '');
+ const wrapperHeight = Number(wrapperHeightStr);
+ let newHeight = fabircHeight;
+ if (heightExcludeLeftTop < wrapperHeight) {
+ const diffHeight = wrapperHeight - heightExcludeLeftTop;
+ newHeight = fabircHeight + diffHeight;
+ }
+ this.imageEditor?.setCanvasHeight(newHeight);
+ }
+
+ expandRightToBaseMap() {
+ let fabricLeftStr = this.fabricWrapperEl?.style.left.replace('px', '');
+ if (fabricLeftStr == '') {
+ fabricLeftStr = '0';
+ }
+ const fabricLeft = Number(fabricLeftStr);
+ const fabricWidthStr = this.fabricWrapperEl?.style.width.replace('px', '');
+ // 可以肯定的是Top和left必然是小于0的,大于0的直接扩展了
+ const fabricWidth = Number(fabricWidthStr);
+ // 这里的可见范围不是全部的可见范围,不包括因为left、top是负数而被隐藏的那一部分
+ // 但是包括右边和下边被隐藏的部分
+ const widthExcludeLeftTop = fabricWidth + fabricLeft;
+ const wrapperWidthStr = this.canvasWrapper.style.width.replace('px', '');
+ const wrapperWidth = Number(wrapperWidthStr);
+ // 如果wrapper大于可见区域,那么要计算差值,然后在canvas上加上这个差值
+ let newWidth = fabricWidth;
+ if (widthExcludeLeftTop < wrapperWidth) {
+ const diffWidth = wrapperWidth - widthExcludeLeftTop;
+ newWidth = fabricWidth + diffWidth;
+ }
+ this.imageEditor?.setCanvasWidth(newWidth)
+ }
+
+ // 四周如果有白板,就把白板都缩了,其它的不同,相当于只动画板,不动画布
+ shrinkCanvasToBackgroundImage() {
+ const canvas = this.imageEditor!.getCanvas();
+ const image = canvas.backgroundImage!;
+ const point = image.getXY();
+
+ const { visiableHeight, visiableWidth, left, top } = this.getCanvasAreaInfo();
+ const shrinkRight = point.x + image.width - left < visiableWidth;
+ const shrinkBottom = point.y + image.height - top < visiableHeight;
+
+ const cutWrapperLeft = point.x > left ? point.x - left : 0;
+ const cutWrapperTop = point.y > top ? point.y - top : 0;
+
+ const cutWrapperRight = shrinkRight ? visiableWidth - (point.x + image.width - left) : 0;
+ const cutWrapperBottom = shrinkBottom ? visiableHeight - (point.y + image.height - top) : 0;
+
+ const wrapperWidth = pxielToNumber(this.canvasWrapper.style.width)
+ const wrapperHeight = pxielToNumber(this.canvasWrapper.style.height)
+ const wrapperTop = pxielToNumber(this.canvasWrapper.style.top)
+ const wrapperLeft = pxielToNumber(this.canvasWrapper.style.left)
+
+ this.canvasWrapper.style.width = wrapperWidth - cutWrapperRight - cutWrapperLeft + 'px';
+ this.canvasWrapper.style.height = wrapperHeight - cutWrapperBottom - cutWrapperTop + 'px';
+
+ // 视口向下移动的时候,画板不要动,也就是说画板left、top要相应的变化
+ this.canvasWrapper.style.top = wrapperTop + cutWrapperTop + 'px';
+ this.canvasWrapper.style.left = wrapperLeft + cutWrapperLeft + 'px';
+ this.fabricWrapperEl!.style.top = -top - cutWrapperTop + 'px';
+ this.fabricWrapperEl!.style.left = -left - cutWrapperLeft + 'px';
+
+ this.fixComponentsPosition();
+ }
+
+ // 如果图片没有显示全,那么先显示全图片
+ extendsCanvas() {
+
+ const canvas = this.imageEditor!.getCanvas();
+ const image = canvas.backgroundImage!;
+
+ const point = image.getXY();
+ const { visiableHeight, visiableWidth, left, top, canvasHeight, canvasWidth } = this.getCanvasAreaInfo();
+
+ const topExtend = point.y - top;
+ const leftExtend = point.x - left;
+ const bottomExtend = visiableHeight - (point.y + image.height - top);
+ const rightExtend = visiableWidth - (point.x + image.width - left);
+
+ const wrapperWidth = pxielToNumber(this.canvasWrapper.style.width)
+ const wrapperHeight = pxielToNumber(this.canvasWrapper.style.height)
+ const wrapperTop = pxielToNumber(this.canvasWrapper.style.top)
+ const wrapperLeft = pxielToNumber(this.canvasWrapper.style.left)
+
+ let maxXExtend = Math.max(leftExtend, rightExtend);
+ let maxYExtend = Math.max(topExtend, bottomExtend);
+ let minXExtend = Math.min(leftExtend, rightExtend);
+ let minYExtend = Math.min(topExtend, bottomExtend);
+
+ let xExtend = 0, yExtend = 0;
+ if (maxXExtend >= 0 && maxXExtend !== minXExtend) {
+ xExtend = maxXExtend;
+ } else if (maxXExtend >= 0 && maxXExtend === minXExtend) {
+ xExtend = maxXExtend + Math.round(image.width * 0.2);
+ }
+
+ if (maxYExtend >= 0 && maxYExtend !== minYExtend) {
+ yExtend = maxYExtend;
+ } else if (maxYExtend >= 0 && maxYExtend === minYExtend) {
+ yExtend = maxYExtend + Math.round(image.height * 0.15);
+ }
+
+
+ const extendLeft = xExtend - leftExtend;
+ const extendRight = xExtend - rightExtend;
+ const extendTop = yExtend - topExtend;
+ const extendBottom = yExtend - bottomExtend;
+
+ const newWrapperHeight = wrapperHeight + extendBottom + extendTop;
+ const newWrapperWidth = wrapperWidth + extendLeft + extendRight;
+
+ this.canvasWrapper.style.width = newWrapperWidth + 'px';
+ this.canvasWrapper.style.height = newWrapperHeight + 'px';
+
+ this.canvasWrapper.style.top = wrapperTop - extendTop + 'px';
+ this.canvasWrapper.style.left = wrapperLeft - extendLeft + 'px';
+
+ let newLeft = 0, newTop = 0;
+ // 如果只是扩展现有的
+ if (extendLeft < left) {
+ newLeft = extendLeft - left;
+ this.fabricWrapperEl!.style.left = newLeft + 'px';
+ } else if (extendLeft >= left) {
+ const newExtend = extendLeft - left;
+ this.fabricWrapperEl!.style.left = '0'
+ this.imageEditor!.transformX(newExtend);
+ }
+
+ if (extendTop < top) {
+ newTop = extendTop - top;
+ this.fabricWrapperEl!.style.top = newTop + 'px';
+ } else if (extendTop >= top) {
+ const newExtend = extendTop - top;
+ this.fabricWrapperEl!.style.top = '0'
+ this.imageEditor!.transformY(newExtend);
+ }
+
+ const extendWidth = newWrapperWidth - (canvasWidth - left);
+ const extendHeight = newWrapperHeight - (canvasHeight - top);
+
+ const newCanvasWidth = extendWidth > 0 ? canvasWidth + extendWidth : canvasWidth;
+ const newCanvasHeight = extendHeight > 0 ? canvasHeight + extendHeight : canvasHeight
+
+ this.imageEditor!.setCanvasDims(newCanvasWidth, newCanvasHeight);
+
+ this.fixComponentsPosition();
+ }
+
+ flipHorizontal() {
+ // 左右翻转时,上下是不要动的,然后左侧多余的部分移动到右侧,右侧多余的部分移动到左侧
+ const { right, canvasWidth } = this.getCanvasAreaInfo();
+
+ const previous = new FlipXUndoProps();
+ const current = new FlipXUndoProps();
+ previous.fabricWrapperEl = this.fabricWrapperEl!;
+ current.fabricWrapperEl = this.fabricWrapperEl!;
+
+ previous.left = this.fabricWrapperEl!.style.left;
+ // 内容翻转,左偏移变右偏移
+ this.fabricWrapperEl!.style.left = -right + 'px';
+ current.left = this.fabricWrapperEl!.style.left;
+
+ const canvas = this.imageEditor?.getCanvas()!;
+ // 翻转状态换一下
+ const backgroundImage = canvas.backgroundImage!;
+ current.backgroundImage = previous.backgroundImage = backgroundImage;
+
+ const flipX = backgroundImage!.flipX;
+ previous.flipX = flipX;
+ backgroundImage!.flipX = !flipX;
+ current.flipX = backgroundImage!.flipX;
+
+ // 左右偏移互换一下
+ const x = backgroundImage.getX();
+ const biWidth = backgroundImage.width;
+ const newX = canvasWidth - biWidth - x;
+ backgroundImage.setX(newX);
+ previous.x = x;
+ current.x = newX;
+
+ const objs = canvas.getObjects() ?? [];
+ for (const obj of objs) {
+ previous.objs.push(obj);
+ current.objs.push(obj);
+
+ const objFlipX = obj.flipX;
+ previous.objFlipX.push(objFlipX);
+
+ const x = obj.getX()
+ previous.objX.push(x);
+
+ const width = obj.width;
+ const nx = canvasWidth - width - x;
+ obj.flipX = !objFlipX;
+ obj.setX(nx);
+
+ current.objFlipX.push(obj.flipX);
+ current.objX.push(nx);
+ canvas.setActiveObject(obj);
+ }
+
+ canvas.renderAll();
+ this.imageEditor!.getHistory().recordFlipXAction(previous, current);
+ }
+
+ flipVertical() {
+ // 上下翻转时,左右是不要动的,然后上侧多余的部分移动到下侧,下侧多余的部分移动到上侧
+ const { bottom, canvasHeight } = this.getCanvasAreaInfo();
+
+ const previous = new FlipYUndoProps();
+ const current = new FlipYUndoProps();
+ previous.fabricWrapperEl = this.fabricWrapperEl!;
+ current.fabricWrapperEl = this.fabricWrapperEl!;
+
+ // 内容翻转,下偏移变上偏移
+ previous.top = this.fabricWrapperEl!.style.top;
+ this.fabricWrapperEl!.style.top = -bottom + 'px';
+ current.top = this.fabricWrapperEl!.style.top;
+
+ const canvas = this.imageEditor?.getCanvas()!;
+ // 翻转状态换一下
+ const backgroundImage = canvas.backgroundImage!;
+ current.backgroundImage = previous.backgroundImage = backgroundImage;
+
+ const flipY = backgroundImage!.flipY;
+ previous.flipY = flipY;
+ backgroundImage!.flipY = !flipY;
+ current.flipY = backgroundImage!.flipY;
+
+ // 上下偏移互换一下
+ const y = backgroundImage.getY();
+ const biHeight = backgroundImage.height;
+ const newY = canvasHeight - biHeight - y;
+ backgroundImage.setY(newY);
+ previous.y = y;
+ current.y = newY;
+
+ const objs = canvas.getObjects() ?? [];
+ for (const obj of objs) {
+ previous.objs.push(obj);
+ current.objs.push(obj);
+
+ const objFlipY = obj.flipY;
+ previous.objFlipY.push(objFlipY);
+
+ const y = obj.getY()
+ previous.objY.push(y);
+
+ const height = obj.height;
+ const ny = canvasHeight - height - y;
+ obj.flipY = !objFlipY;
+ obj.setY(ny);
+
+ current.objFlipY.push(obj.flipY);
+ current.objY.push(ny);
+ canvas.setActiveObject(obj);
+ }
+
+ canvas.renderAll();
+ this.imageEditor!.getHistory().recordFlipYAction(previous, current);
+ }
+
+ // 顺时针旋转90度
+ rotateClockwise() {
+
+ const previous = new RotateProps();
+ const current = new RotateProps();
+ previous.canvasWrapper = current.canvasWrapper = this.canvasWrapper;
+ previous.fabricWrapperEl = current.fabricWrapperEl = this.fabricWrapperEl!;
+ previous.imageEditor = current.imageEditor = this.imageEditor!;
+
+ const canvasArea = this.getCanvasAreaInfo();
+ const { left, bottom } = canvasArea;
+ const { canvasWidth, canvasHeight } = canvasArea;
+ const { visiableHeight, visiableWidth } = canvasArea;
+ const canvas = this.imageEditor!.getCanvas()!;
+
+ previous.canvasHeight = canvasHeight; previous.canvasWidth = canvasWidth;
+ // 顺时针旋转90度的时候,长高要做一个互换
+ this.imageEditor!.setCanvasDims(canvasHeight, canvasWidth);
+ current.canvasHeight = canvasWidth; current.canvasWidth = canvasHeight;
+
+ const fwStyle = this.fabricWrapperEl!.style;
+ previous.left = fwStyle.left;
+ previous.top = fwStyle.top;
+ fwStyle.left = (-1) * bottom + 'px';
+ fwStyle.top = (-1) * left + 'px';
+ current.left = fwStyle.left;
+ current.top = fwStyle.top;
+
+ // 可见区域也要一同进行变换
+ previous.width = this.canvasWrapper.style.width;
+ previous.height = this.canvasWrapper.style.height;
+
+ this.canvasWrapper.style.width = visiableHeight + 'px';
+ this.canvasWrapper.style.height = visiableWidth + 'px';
+
+ current.width = this.canvasWrapper.style.width;
+ current.height = this.canvasWrapper.style.height;
+
+ const image = canvas.backgroundImage!
+ const objs = canvas.getObjects() as FabricObject[] ?? [];
+ objs.unshift(image);
+
+ // 还要考虑自带旋转度数的对象
+ const oriAngle = image.angle;
+ for (const obj of objs) {
+ const { x, y } = obj.getXY();
+ // 旋转时是按照左上角的顶点进行旋转的
+ // 所以要注意到左上角的位置
+ let newX, newY;
+ // 旋转90度的时候,left变成top(y),bottom变成left(x)
+ if (oriAngle == 0) {
+ newY = x;
+ // 底部的长度是canvas的长度减去top,减去图片高度
+ const imgBottom = canvasHeight - y - obj.height;
+ // 按照左上角旋转过后,左上角的顶点去了右上角因此还要向右移动一个宽度的位置
+ newX = imgBottom + obj.height;
+ } else if (oriAngle == 90) {
+ // 旋转时,依旧是left(x)变为top(y),bottom变成left(x)
+ // 但是由于现在已经旋转了,定位的顶点在右上角,所以实际left值要减去一个图片的宽度
+ const imgLeft = x - obj.height;
+ // 实际的bottom值依旧是画板高度减去y值减去底图高度
+ const imgBottom = canvasHeight - y - obj.width;
+
+ // 但是因为旋转了180度,所以要向右平移一个宽度,向下平移一个高度
+ newX = imgBottom + obj.width;
+ newY = imgLeft + obj.height;
+
+ } else if (oriAngle == 180) {
+ // 同上
+ const imgLeft = x - obj.width;
+ const imgBottom = canvasHeight - y;
+ newX = imgBottom;
+ newY = imgLeft + obj.width;
+ } else if (oriAngle == 270) {
+ // 同上
+ const imgLeft = x;
+ const imgBottom = canvasHeight - y;
+ newX = imgBottom;
+ newY = imgLeft;
+ } else {
+ throw Error('不满足预期的度数' + oriAngle)
+ }
+
+ const angle = obj.angle;
+ const newAngle = (angle + 90) % 360;
+ obj.set('angle', newAngle);
+ obj.setXY(new Point(newX, newY));
+
+ previous.objs.push(obj);
+ previous.objAngle.push(angle);
+ previous.objPos.push({ x, y })
+
+ current.objs.push(obj);
+ current.objAngle.push(newAngle);
+ current.objPos.push({ x: newX, y: newY });
+ }
+
+ const shapes = canvas.getObjects() as FabricObject[] ?? [];
+ for (const shape of shapes) {
+ canvas.setActiveObject(shape);
+ shape.setCoords();
+ }
+ canvas.renderAll();
+
+ previous.canvasWrapperProps = this.calculateCanvasWrapper();
+
+ // 旋转后,整个图会回到界面的正中央
+ this.moveCanvasToCenter();
+
+ current.canvasWrapperProps = this.calculateCanvasWrapper();
+
+ this.imageEditor!.getHistory().recordRotateAction(previous, current);
+ }
+
+ calculateCanvasWrapper() {
+ const prop = new CanvasDetailedProps();
+ prop.canvasWrapper = this.canvasWrapper;
+ prop.canvasWrapperLeft = this.canvasWrapper.style.left;
+ prop.canvasWrapperTop = this.canvasWrapper.style.top;
+ prop.canvasWrapperHeight = this.canvasWrapper.style.height;
+ prop.canvasWrapperWidth = this.canvasWrapper.style.width;
+
+ prop.topResizer = this.northResizer;
+ prop.topResizerLeft = this.northResizer.style.left;
+ prop.topResizerTop = this.northResizer.style.top;
+
+ prop.leftResizer = this.westResizer;
+ prop.leftResizerLeft = this.westResizer.style.left;
+ prop.leftResizerTop = this.westResizer.style.top;
+
+ prop.bottomResizer = this.southResizer;
+ prop.bottomResizerLeft = this.southResizer.style.left;
+ prop.bottomResizerTop = this.southResizer.style.top;
+
+ prop.rightResizer = this.eastResizer;
+ prop.rightResizerLeft = this.eastResizer.style.left;
+ prop.rightResizerTop = this.eastResizer.style.top;
+
+ prop.toolbar = this.toolbar;
+ prop.toolbarLeft = this.toolbar.style.left;
+ prop.toolbarTop = this.toolbar.style.top;
+
+ prop.optionBar = this.optionBar;
+ prop.optionBarLeft = this.optionBar.style.left;
+ prop.optionBarTop = this.optionBar.style.top;
+ return prop;
+ }
+
+
+ // 逆时针旋转90度
+ rotateCounterClockwise() {
+
+ const previous = new RotateProps();
+ const current = new RotateProps();
+ previous.canvasWrapper = current.canvasWrapper = this.canvasWrapper;
+ previous.fabricWrapperEl = current.fabricWrapperEl = this.fabricWrapperEl!;
+ previous.imageEditor = current.imageEditor = this.imageEditor!;
+
+ const canvasArea = this.getCanvasAreaInfo();
+ const { right, top } = canvasArea;
+ const { canvasWidth, canvasHeight } = canvasArea;
+ const { visiableHeight, visiableWidth } = canvasArea;
+ const canvas = this.imageEditor!.getCanvas()!;
+
+ previous.canvasHeight = canvasHeight; previous.canvasWidth = canvasWidth;
+ // 逆时针旋转90度的时候,长高要做一个互换
+ this.imageEditor!.setCanvasDims(canvasHeight, canvasWidth);
+ current.canvasHeight = canvasWidth; current.canvasWidth = canvasHeight;
+
+ const fwStyle = this.fabricWrapperEl!.style;
+ previous.left = fwStyle.left;
+ previous.top = fwStyle.top;
+
+ fwStyle.left = (-1) * top + 'px';
+ fwStyle.top = (-1) * right + 'px';
+
+ current.left = fwStyle.left;
+ current.top = fwStyle.top;
+
+ // 可见区域也要一同进行变换
+ previous.width = this.canvasWrapper.style.width;
+ previous.height = this.canvasWrapper.style.height;
+
+ this.canvasWrapper.style.width = visiableHeight + 'px';
+ this.canvasWrapper.style.height = visiableWidth + 'px';
+
+ current.width = this.canvasWrapper.style.width;
+ current.height = this.canvasWrapper.style.height;
+
+ const image = canvas.backgroundImage!
+ const objs = canvas.getObjects() as FabricObject[] ?? [];
+ objs.unshift(image);
+
+ // 还要考虑自带旋转度数的对象
+ const oriAngle = image.angle;
+ for (const obj of objs) {
+ const { x, y } = obj.getXY();
+ // 旋转时是按照左上角的顶点进行旋转的
+ // 所以要注意到左上角的位置
+ let newX, newY;
+ // 直接参考顺时针
+ if (oriAngle == 0) {
+ newX = y;
+ const imageRight = canvasWidth - x - obj.width;
+ newY = imageRight + obj.width;
+ } else if (oriAngle == 270) {
+ const imgTop = y - obj.width;
+ const imgRight = canvasWidth - x - obj.height;
+ newX = imgTop + obj.width;
+ newY = imgRight + obj.height;
+ } else if (oriAngle == 180) {
+ const imgTop = y - obj.height;
+ const imgRight = canvasWidth - x;
+ newX = imgTop + obj.height;
+ newY = imgRight;
+ } else if (oriAngle == 90) {
+ const imgRight = canvasWidth - x;
+ const imgTop = y;
+ newX = imgTop;
+ newY = imgRight;
+ } else {
+ throw Error('不满足预期的度数' + oriAngle)
+ }
+ const angle = obj.angle;
+ const newAngle = (angle - 90 + 360) % 360;
+
+ obj.set('angle', newAngle);
+ obj.setXY(new Point(newX, newY));
+
+ previous.objs.push(obj);
+ previous.objAngle.push(angle);
+ previous.objPos.push({ x, y })
+
+ current.objs.push(obj);
+ current.objAngle.push(newAngle);
+ current.objPos.push({ x: newX, y: newY });
+
+ }
+
+ const shapes = canvas.getObjects() as FabricObject[] ?? [];
+ for (const shape of shapes) {
+ canvas.setActiveObject(shape);
+ shape.setCoords();
+ }
+ canvas.renderAll();
+
+
+ previous.canvasWrapperProps = this.calculateCanvasWrapper();
+
+ // 旋转后,整个图会回到界面的正中央
+ this.moveCanvasToCenter();
+
+ current.canvasWrapperProps = this.calculateCanvasWrapper();
+
+ this.imageEditor!.getHistory().recordRotateAction(previous, current);
+
+ }
+
+ moveCanvasToCenter() {
+
+ // 先旋转,旋转完看看会不会出滚动条
+ // 如果会出滚动条,那么就要考虑把滚动条的值算进去了
+ // 如果没有出现滚动条,那么就不用考虑滚动条了
+ const { width: parentWidth, height: parentHeight } = this.wrapper.getBoundingClientRect();
+ const { width: wrapperWidth, height: wrapperHeight, left: wrapperLeft, top: wrapperTop } = this.canvasWrapper.getBoundingClientRect();
+
+ let cwLeft, cwTop;
+ if (parentWidth <= wrapperWidth) {
+ cwLeft = 0;
+ this.canvasWrapper.style.left = String(cwLeft);
+ } else {
+ const extra = parentWidth - wrapperWidth;
+ cwLeft = extra / 2;
+ this.canvasWrapper.style.left = cwLeft + 'px';
+ }
+ if (parentHeight <= wrapperHeight) {
+ cwTop = 0;
+ this.canvasWrapper.style.top = String(cwTop);
+ } else {
+ const extra = parentHeight - wrapperHeight;
+ cwTop = extra / 2;
+ this.canvasWrapper.style.top = cwTop + 'px';
+ }
+
+ this.fixComponentsPosition();
+ }
+
+ fixComponentsPosition() {
+ this.fixToolbarPosition();
+ this.fixResizerPosition();
+ }
+
+ fixResizerPosition() {
+
+ const cwTop = pxielToNumber(this.canvasWrapper.style.top)
+ const cwLeft = pxielToNumber(this.canvasWrapper.style.left)
+ const wrapperWidth = pxielToNumber(this.canvasWrapper.style.width)
+ const wrapperHeight = pxielToNumber(this.canvasWrapper.style.height)
+
+ // 12是拉伸按钮的大小,6是拉伸大小的一半,用来做偏移用
+ const northResizerTop = cwTop - 12;
+ const northResizerLeft = cwLeft + wrapperWidth / 2 - 6;
+
+ const westResizerTop = cwTop + wrapperHeight / 2 - 6;
+ const westResizerLeft = cwLeft - 12;
+
+ const southResizerTop = cwTop + wrapperHeight;
+ const southResizerLeft = cwLeft + wrapperWidth / 2 - 6;
+ const eastResizerTop = cwTop + wrapperHeight / 2 - 6;
+ const eastResizerLeft = cwLeft + wrapperWidth;
+
+ this.northResizer.style.top = northResizerTop + 'px';
+ this.northResizer.style.left = northResizerLeft + 'px';
+
+ this.northWestResizer.style.top = northResizerTop + 'px';
+ this.northWestResizer.style.left = westResizerLeft + 'px';
+
+ this.westResizer.style.top = westResizerTop + 'px';
+ this.westResizer.style.left = westResizerLeft + 'px';
+
+ this.southWestResizer.style.top = southResizerTop + 'px';
+ this.southWestResizer.style.left = westResizerLeft + 'px';
+
+ this.southResizer.style.top = southResizerTop + 'px';
+ this.southResizer.style.left = southResizerLeft + 'px';
+
+ this.southEastResizer.style.top = southResizerTop + 'px';
+ this.southEastResizer.style.left = eastResizerLeft + 'px';
+
+ this.eastResizer.style.top = eastResizerTop + 'px';
+ this.eastResizer.style.left = eastResizerLeft + 'px';
+
+ this.northEastResizer.style.top = northResizerTop + 'px';
+ this.northEastResizer.style.left = eastResizerLeft + 'px';
+ }
+
+ fixToolbarPosition() {
+ const top = pxielToNumber(this.canvasWrapper.style.top);
+ const left = pxielToNumber(this.canvasWrapper.style.left);
+ const width = pxielToNumber(this.canvasWrapper.style.width);
+ const toolbarWidth = pxielToNumber(this.toolbar.style.width) + 2 * pxielToNumber(this.toolbar.style.padding);
+ const height = pxielToNumber(this.canvasWrapper.style.height);
+ // 20是一个间隔高度
+ this.toolbar.style.top = top + height + 20 + 'px';
+ this.toolbar.style.left = left + width / 2 - toolbarWidth / 2 + 'px';
+ const current = this.imageEditor!.getOperatorType()
+ const currEle = this.menuMap.get(current);
+ this.adjustOptionBarPosition(currEle);
+ if (current == OperatorType.MOSAIC) {
+ this.showSizeOptions();
+ }
+ }
+
+ adjustOptionBarPosition(currEle?: HTMLDivElement) {
+ if (currEle == null) {
+ return;
+ }
+ const pos = getAbsolutePosition(currEle);
+ const optionBar = this.optionBar;
+ optionBar.style.display = 'inline-block';
+ optionBar.style.left = Math.round(pos.x - 130) + 'px';
+ optionBar.style.top = Math.round(pos.y + 36) + 'px';
+ }
+
+ getCanvasAreaInfo() {
+
+ // 完整的canvas区域,包括可见与不可见的区域
+ const canvasWidth = toNumber(this.fabricWrapperEl!.style.width.replace('px', ''));
+ const canvasHeight = toNumber(this.fabricWrapperEl!.style.height.replace('px', ''));
+
+ // canvas在左侧和上侧隐藏的区域,值是left和top的绝对值
+ const left = (-1) * toNumber(this.fabricWrapperEl!.style.left.replace('px', ''));
+ const top = (-1) * toNumber(this.fabricWrapperEl!.style.top.replace('px', ''));
+
+ const visiableWidth = toNumber(this.canvasWrapper!.style.width.replace('px', ''));
+ const visiableHeight = toNumber(this.canvasWrapper!.style.height.replace('px', ''));
+
+ // canvas的宽高 减去 可见区域的长度 减去 左侧上侧的区域
+ // 就能得到右侧和下侧部分的长度
+ const right = canvasWidth - visiableWidth - left;
+ const bottom = canvasHeight - visiableHeight - top;
+
+ return {
+ canvasWidth, canvasHeight, visiableWidth, visiableHeight, top, bottom, left, right
+ }
+ }
+
+ getScreenshotCanvas() {
+ return this.screenshotCanvas;
+ }
+
+ getScreenshotResizers() {
+ return this.screenshotResizer;
+ }
+
+ getScreenshotToolbar() {
+ return this.screenshotToolbar;
+ }
+ getScreenshotConfirmButton() {
+ return this.screenshotConfirmButton;
+ }
+ getScreenshotCancelButton() {
+ return this.screenshotCancelButton;
+ }
+
+ getFabricWrapper() {
+ return this.fabricWrapperEl;
+ }
+
+ // 不能用两个canvas,两个canvas会带来闪烁的问题,直接在原来的canvas上操作
+ cropImage() {
+ this.hideToolbar();
+ const width = pxielToNumber(this.canvasWrapper!.style.width);
+ const height = pxielToNumber(this.canvasWrapper!.style.height);
+ const screenshot = this.imageEditor!.getScreenshoter();
+ const left = pxielToNumber(this.canvasWrapper!.style.left);
+ const top = pxielToNumber(this.canvasWrapper!.style.top);
+ screenshot.initMask(left, top, width, height);
+ }
+
+ resetWrapper(width: number, height: number) {
+ this.canvasWrapper.style.width = width + 'px';
+ this.canvasWrapper.style.height = height + 'px';
+ this.moveCanvasToCenter();
+ }
+
+ hideResizer() {
+ this.northResizer.style.display = 'none';
+ this.westResizer.style.display = 'none';
+ this.southResizer.style.display = 'none';
+ this.eastResizer.style.display = 'none';
+ }
+
+ showResizer() {
+ this.northResizer.style.display = 'block';
+ this.westResizer.style.display = 'block';
+ this.southResizer.style.display = 'block';
+ this.eastResizer.style.display = 'block';
+ }
+
+ calculateCanvasInfo() {
+ const info = new FabricCanvasProps();
+ info.fabricWrapperEl = this.fabricWrapperEl!;
+ info.fabricWrapperElLeft = this.fabricWrapperEl!.style.left;
+ info.fabricWrapperElTop = this.fabricWrapperEl!.style.top;
+ const canvas = this.imageEditor!.getCanvas();
+ info.canvasWidth = canvas.width;
+ info.canvasHeight = canvas.height;
+ info.canvasBackgroundColor = canvas.backgroundColor as string;
+ info.canvasBackgroundImage = canvas.backgroundImage;
+ info.objects = canvas.getObjects();
+ return info;
+ }
+
+ resetImageEditor() {
+ const canvas = this.imageEditor!.getCanvas()
+ const image = canvas.backgroundImage!;
+ const width = image.width;
+ const height = image.height;
+ this.imageEditor!.setCanvasDims(width, height);
+ image.setXY(new Point(0, 0));
+ const objects = canvas.getObjects()
+ for (const o of objects) {
+ canvas.remove(o);
+ }
+ this.fabricWrapperEl!.style.width = '0';
+ this.fabricWrapperEl!.style.height = '0';
+ this.canvasWrapper.style.width = canvas.width + 'px';
+ this.canvasWrapper.style.height = canvas.height + 'px';
+ this.canvasWrapper.style.left = this.imageEditor!.initWrapperLeft;
+ this.canvasWrapper.style.top = this.imageEditor!.initWrapperTop;
+ this.fixComponentsPosition();
+ this.imageEditor!.getHistory().clearRedoStack();
+ }
+
+ downloadAreaImage() {
+ const width = pxielToNumber(this.canvasWrapper.style.width);
+ const height = pxielToNumber(this.canvasWrapper.style.height);
+ const left = pxielToNumber(this.fabricWrapperEl!.style.left)
+ const top = pxielToNumber(this.fabricWrapperEl!.style.top)
+ const start = new Point(left, top);
+ const end = new Point(left + width, top + height);
+ const image = this.imageEditor!.getAreaImageInfo(start, end);
+ const link = document.createElement("a");
+ link.href = image;
+ link.download = 'image.png';
+ link.click();
+ }
+}
\ No newline at end of file
diff --git a/src/history.ts b/src/history.ts
new file mode 100644
index 0000000000000000000000000000000000000000..efc62588f61c6a3213cdd4a381825389f355e623
--- /dev/null
+++ b/src/history.ts
@@ -0,0 +1,731 @@
+import { Canvas, Ellipse, FabricObject, Point, TDegree } from "fabric";
+import ImageEditor from "./image_editor";
+
+interface OperationAction {
+
+ undo(): void;
+
+ redo(): void;
+}
+
+enum State {
+ CAN_UNDO, CAN_REDO
+}
+
+class CreateAction implements OperationAction {
+
+ protected canvas: Canvas;
+
+ protected object: FabricObject;
+
+ protected state: State = State.CAN_UNDO;
+
+ constructor(canvas: Canvas, object: FabricObject) {
+ this.canvas = canvas;
+ this.object = object;
+ }
+
+ undo() {
+ if (this.state == State.CAN_UNDO) {
+ this.canvas.remove(this.object);
+ this.canvas.renderAll();
+ this.state = State.CAN_REDO;
+ }
+ }
+
+ redo() {
+ if (this.state == State.CAN_REDO) {
+ this.canvas.add(this.object);
+ this.canvas.renderAll();
+ this.state = State.CAN_UNDO;
+ }
+ }
+}
+
+class RemoveAction implements OperationAction {
+
+ protected canvas: Canvas;
+
+ protected object: FabricObject;
+
+ protected state: State = State.CAN_UNDO;
+
+ constructor(canvas: Canvas, object: FabricObject) {
+ this.canvas = canvas;
+ this.object = object;
+ }
+
+ undo() {
+ if (this.state == State.CAN_UNDO) {
+ this.canvas.add(this.object);
+ this.canvas.renderAll();
+ this.state = State.CAN_REDO;
+ }
+ }
+
+ redo() {
+ if (this.state == State.CAN_REDO) {
+ this.canvas.remove(this.object);
+ this.canvas.renderAll();
+ this.state = State.CAN_UNDO;
+ }
+ }
+}
+
+class MoveAction implements OperationAction {
+
+ protected canvas: Canvas;
+
+ protected object: FabricObject;
+
+ protected previousX: number;
+
+ protected previousY: number;
+
+ protected currentX: number;
+
+ protected currentY: number;
+
+ constructor(canvas: Canvas, object: FabricObject, previousX: number, previousY: number) {
+ this.object = object;
+ this.previousX = previousX;
+ this.previousY = previousY;
+ this.currentX = object.getX();
+ this.currentY = object.getY();
+ this.canvas = canvas;
+ }
+
+ undo(): void {
+ this.object.setX(this.previousX);
+ this.object.setY(this.previousY);
+ this.canvas.renderAll();
+ }
+
+ redo(): void {
+ this.object.setX(this.currentX);
+ this.object.setY(this.currentY);
+ this.canvas.renderAll();
+ }
+
+}
+
+class ScaleAction implements OperationAction {
+
+ protected canvas: Canvas;
+
+ protected object: FabricObject;
+
+ protected previousWidth: number;
+
+ protected previousHeight: number;
+
+ protected previousX: number;
+
+ protected previousY: number;
+
+ protected currentWidth: number;
+
+ protected currentHeight: number;
+
+ protected currentX: number;
+
+ protected currentY: number;
+
+ constructor(canvas: Canvas, object: FabricObject, previousWidth: number, previousHeight: number, previousX: number, previousY: number) {
+ this.canvas = canvas;
+ this.object = object;
+ this.previousWidth = previousWidth;
+ this.previousHeight = previousHeight;
+ this.previousX = previousX;
+ this.previousY = previousY;
+ this.currentWidth = object.get('width');
+ this.currentHeight = object.get('height');
+ this.currentX = object.getX();
+ this.currentY = object.getY();
+ }
+
+ undo(): void {
+ this.object.set({ width: this.previousWidth, height: this.previousHeight });
+ this.object.setX(this.previousX);
+ this.object.setY(this.previousY);
+ this.canvas.renderAll();
+ }
+
+ redo(): void {
+ this.object.set({ width: this.currentWidth, height: this.currentHeight });
+ this.object.setX(this.currentX);
+ this.object.setY(this.currentY);
+ this.canvas.renderAll();
+ }
+}
+
+class EllipseScaleAction extends ScaleAction {
+
+ protected previousRX: number;
+
+ protected previousRY: number;
+
+ protected currentRX: number;
+
+ protected currentRY: number;
+
+ constructor(canvas: Canvas, object: Ellipse, previousWidth: number, previousHeight: number
+ , previousX: number, previousY: number, previousRX: number, previousRY: number) {
+ super(canvas, object, previousWidth, previousHeight, previousX, previousY);
+ this.previousRX = previousRX;
+ this.previousRY = previousRY;
+ console.log(this.previousRX)
+ console.log(this.previousRY)
+ this.currentRX = object.rx;
+ this.currentRY = object.ry;
+ }
+
+ undo(): void {
+ const obj = this.object as Ellipse;
+ obj.rx = this.previousRX;
+ obj.ry = this.previousRY;
+ super.undo();
+ }
+
+ redo(): void {
+ const obj = this.object as Ellipse;
+ obj.rx = this.currentRX;
+ obj.ry = this.currentRY;
+ super.redo();
+ }
+}
+
+
+class RatioScaleAction implements OperationAction {
+
+ protected canvas: Canvas;
+
+ protected object: FabricObject;
+
+ protected previousScaleX: number;
+
+ protected previousScaleY: number;
+
+ protected previousX: number;
+
+ protected previousY: number;
+
+ protected currentScaleX: number;
+
+ protected currentScaleY: number;
+
+ protected currentX: number;
+
+ protected currentY: number;
+
+ constructor(canvas: Canvas, object: FabricObject, previousScaleX: number, previousScaleY: number, previousX: number, previousY: number) {
+ this.canvas = canvas;
+ this.object = object;
+ this.previousScaleX = previousScaleX;
+ this.previousScaleY = previousScaleY;
+ this.previousX = previousX;
+ this.previousY = previousY;
+ this.currentScaleX = object.scaleX;
+ this.currentScaleY = object.scaleY;
+ this.currentX = object.getX();
+ this.currentY = object.getY();
+ }
+
+ undo(): void {
+ this.object.scaleX = this.previousScaleX;
+ this.object.scaleY = this.previousScaleY;
+ this.object.setX(this.previousX);
+ this.object.setY(this.previousY);
+ this.canvas.renderAll();
+ }
+
+ redo(): void {
+ this.object.scaleX = this.currentScaleX;
+ this.object.scaleY = this.currentScaleY;
+ this.object.setX(this.currentX);
+ this.object.setY(this.currentY);
+ this.canvas.renderAll();
+ }
+}
+
+export class FlipXUndoProps {
+ fabricWrapperEl?: HTMLDivElement;
+ left: string = '';
+ backgroundImage?: FabricObject;
+ flipX: boolean = false;
+ x: number = 0;
+ objs: FabricObject[] = [];
+ objX: number[] = [];
+ objFlipX: boolean[] = [];
+}
+
+class FlipXAction implements OperationAction {
+
+ private canvas: Canvas;
+
+ private previous: FlipXUndoProps;
+
+ private current: FlipXUndoProps;
+
+ private hasUndo = false;
+
+ constructor(canvas: Canvas, previous: FlipXUndoProps, current: FlipXUndoProps) {
+ this.canvas = canvas;
+ this.previous = previous;
+ this.current = current;
+ }
+ undo(): void {
+ if (this.hasUndo) {
+ return;
+ }
+ const previous = this.previous;
+ const bi = previous.backgroundImage;
+ const fabricWrapperEl = previous.fabricWrapperEl;
+ fabricWrapperEl!.style.left = previous.left;
+ bi!.flipX = previous.flipX;
+ bi!.setX(previous.x);
+ for (const index in previous.objs) {
+ const obj = previous.objs[index];
+ const flipX = previous.objFlipX[index];
+ const x = previous.objX[index];
+ obj.flipX = flipX;
+ obj.setX(x);
+ obj.setCoords();
+ }
+ this.canvas.renderAll();
+ this.hasUndo = true;
+ }
+ redo(): void {
+ if (!this.hasUndo) {
+ return;
+ }
+ const current = this.current;
+ const bi = current.backgroundImage;
+ const fabricWrapperEl = current.fabricWrapperEl;
+ fabricWrapperEl!.style.left = current.left;
+ bi!.flipX = current.flipX;
+ bi!.setX(current.x);
+ for (const index in current.objs) {
+ const obj = current.objs[index];
+ const flipX = current.objFlipX[index];
+ const x = current.objX[index];
+ obj.flipX = flipX;
+ obj.setX(x);
+ obj.setCoords();
+ }
+ this.canvas.renderAll();
+ this.hasUndo = false
+ }
+}
+
+export class FlipYUndoProps {
+ fabricWrapperEl?: HTMLDivElement;
+ top: string = '';
+ backgroundImage?: FabricObject;
+ flipY: boolean = false;
+ y: number = 0;
+ objs: FabricObject[] = [];
+ objY: number[] = [];
+ objFlipY: boolean[] = [];
+}
+
+class FlipYAction implements OperationAction {
+
+ private canvas: Canvas;
+
+ private previous: FlipYUndoProps;
+
+ private current: FlipYUndoProps;
+
+ private hasUndo = false;
+
+ constructor(canvas: Canvas, previous: FlipYUndoProps, current: FlipYUndoProps) {
+ this.canvas = canvas;
+ this.previous = previous;
+ this.current = current;
+ }
+ undo(): void {
+ if (this.hasUndo) {
+ return;
+ }
+ const previous = this.previous;
+ const bi = previous.backgroundImage;
+ const fabricWrapperEl = previous.fabricWrapperEl;
+ fabricWrapperEl!.style.top = previous.top;
+ bi!.flipY = previous.flipY;
+ bi!.setY(previous.y);
+ for (const index in previous.objs) {
+ const obj = previous.objs[index];
+ const flipY = previous.objFlipY[index];
+ const y = previous.objY[index];
+ obj.flipY = flipY;
+ obj.setY(y);
+ obj.setCoords();
+ }
+ this.canvas.renderAll();
+ this.hasUndo = true;
+ }
+ redo(): void {
+ if (!this.hasUndo) {
+ return;
+ }
+ const current = this.current;
+ const bi = current.backgroundImage;
+ const fabricWrapperEl = current.fabricWrapperEl;
+ fabricWrapperEl!.style.top = current.top;
+ bi!.flipY = current.flipY;
+ bi!.setY(current.y);
+ for (const index in current.objs) {
+ const obj = current.objs[index];
+ const flipY = current.objFlipY[index];
+ const y = current.objY[index];
+ obj.flipY = flipY;
+ obj.setY(y);
+ obj.setCoords();
+ }
+ this.canvas.renderAll();
+ this.hasUndo = false
+ }
+}
+
+export class CanvasWrapperProps {
+ canvasWrapper?: HTMLDivElement;
+ canvasWrapperLeft = '';
+ canvasWrapperTop = '';
+ topResizer?: HTMLDivElement;
+ topResizerLeft = '';
+ topResizerTop = '';
+ leftResizer?: HTMLDivElement;
+ leftResizerLeft = '';
+ leftResizerTop = '';
+ bottomResizer?: HTMLDivElement;
+ bottomResizerTop = '';
+ bottomResizerLeft = '';
+ rightResizer?: HTMLDivElement;
+ rightResizerTop = '';
+ rightResizerLeft = '';
+ toolbar?: HTMLDivElement;
+ toolbarLeft = '';
+ toolbarTop = '';
+ optionBar?: HTMLDivElement;
+ optionBarLeft = '';
+ optionBarTop = '';
+}
+
+export class CanvasDetailedProps extends CanvasWrapperProps {
+ canvasWrapperHeight = ''
+ canvasWrapperWidth = '';
+}
+
+export class FabricCanvasProps {
+ fabricWrapperEl?: HTMLDivElement;
+ fabricWrapperElLeft = '';
+ fabricWrapperElTop = '';
+ canvasWidth = 0;
+ canvasHeight = 0;
+ canvasBackgroundColor = '';
+ canvasBackgroundImage?: FabricObject
+ objects = [] as FabricObject[]
+}
+
+export class RotateProps {
+ fabricWrapperEl?: HTMLDivElement;
+ canvasWrapper?: HTMLDivElement;
+ imageEditor?: ImageEditor;
+ left: string = '';
+ top: string = '';
+ canvasHeight: number = 0;
+ canvasWidth: number = 0;
+ height: string = '';
+ width: string = '';
+ objs: FabricObject[] = [];
+ objPos: any[] = [];
+ objAngle: TDegree[] = [];
+ canvasWrapperProps?: CanvasWrapperProps;
+}
+
+abstract class CanvasSetAction implements OperationAction {
+ undo(): void {
+
+ }
+ redo(): void {
+ }
+ resetCanvasWrapper(prop: CanvasWrapperProps) {
+ prop.canvasWrapper!.style.top = prop.canvasWrapperTop;
+ prop.canvasWrapper!.style.left = prop.canvasWrapperLeft;
+
+ prop.topResizer!.style.top = prop.topResizerTop;
+ prop.topResizer!.style.left = prop.topResizerLeft;
+
+ prop.leftResizer!.style.top = prop.leftResizerTop;
+ prop.leftResizer!.style.left = prop.leftResizerLeft;
+
+ prop.bottomResizer!.style.top = prop.bottomResizerTop;
+ prop.bottomResizer!.style.left = prop.bottomResizerLeft;
+
+ prop.rightResizer!.style.top = prop.rightResizerTop;
+ prop.rightResizer!.style.left = prop.rightResizerLeft;
+
+ prop.toolbar!.style.top = prop.toolbarTop;
+ prop.toolbar!.style.left = prop.toolbarLeft;
+
+ prop.optionBar!.style.top = prop.optionBarTop;
+ prop.optionBar!.style.left = prop.optionBarLeft;
+ }
+}
+
+class RotationAction extends CanvasSetAction {
+
+ private previous: RotateProps;
+ private current: RotateProps;
+ private canvas: Canvas;
+ private hasUndo = false;
+
+ constructor(canvas: Canvas, previous: RotateProps, current: RotateProps) {
+ super();
+ this.canvas = canvas;
+ this.current = current;
+ this.previous = previous;
+ }
+ undo(): void {
+ if (this.hasUndo) {
+ return;
+ }
+ const previous = this.previous;
+ previous.imageEditor!.setCanvasDims(previous.canvasWidth, previous.canvasHeight);
+ const fwStyle = previous.fabricWrapperEl!.style;
+ fwStyle.left = previous.left;
+ fwStyle.top = previous.top;
+
+ const canvasWrapper = previous.canvasWrapper!;
+ canvasWrapper.style.width = previous.width;
+ canvasWrapper.style.height = previous.height;
+
+ for (const index in previous.objs) {
+ const obj = previous.objs[index];
+ const xy = previous.objPos[index];
+ const angle = previous.objAngle[index];
+ obj.set('angle', angle)
+ obj.setXY(new Point(xy.x, xy.y));
+ obj.setCoords();
+ }
+ this.canvas.renderAll();
+ this.resetCanvasWrapper(previous.canvasWrapperProps!);
+ this.hasUndo = true;
+
+ }
+
+
+ redo(): void {
+ if (!this.hasUndo) {
+ return;
+ }
+ const current = this.current;
+ current.imageEditor!.setCanvasDims(current.canvasWidth, current.canvasHeight);
+ const fwStyle = current.fabricWrapperEl!.style;
+ fwStyle.left = current.left;
+ fwStyle.top = current.top;
+
+ const canvasWrapper = current.canvasWrapper!;
+ canvasWrapper.style.width = current.width;
+ canvasWrapper.style.height = current.height;
+
+ for (const index in current.objs) {
+ const obj = current.objs[index];
+ const xy = current.objPos[index];
+ const angle = current.objAngle[index];
+ obj.set('angle', angle)
+ obj.setXY(new Point(xy.x, xy.y));
+ obj.setCoords();
+ }
+ this.canvas.renderAll();
+ this.resetCanvasWrapper(current.canvasWrapperProps!);
+ this.hasUndo = false;
+ }
+
+}
+
+class CropAction extends CanvasSetAction {
+
+ private previousWrapper: CanvasDetailedProps;
+ private previousCanvas: FabricCanvasProps;
+ private cropWrapper: CanvasDetailedProps;
+ private cropCanvas: FabricCanvasProps;
+ private canvas: Canvas;
+ private hasUndo = false;
+
+ constructor(canvas: Canvas, previousWrapper: CanvasDetailedProps, previousCanvas: FabricCanvasProps
+ , cropWrapper: CanvasDetailedProps, cropCanvas: FabricCanvasProps
+ ) {
+ super();
+ this.canvas = canvas;
+ this.previousWrapper = previousWrapper;
+ this.previousCanvas = previousCanvas;
+ this.cropWrapper = cropWrapper;
+ this.cropCanvas = cropCanvas;
+ }
+
+ undo(): void {
+ if (this.hasUndo) {
+ return;
+ }
+ this.clearCanvas();
+ super.resetCanvasWrapper(this.previousWrapper);
+ const pw = this.previousWrapper;
+ pw.canvasWrapper!.style.width = pw.canvasWrapperWidth;
+ pw.canvasWrapper!.style.height = pw.canvasWrapperHeight;
+ const pc = this.previousCanvas;
+ pc.fabricWrapperEl!.style.left = pc.fabricWrapperElLeft;
+ pc.fabricWrapperEl!.style.top = pc.fabricWrapperElTop;
+ this.canvas.setDimensions({ width: pc.canvasWidth, height: pc.canvasHeight });
+ this.canvas.backgroundColor = pc.canvasBackgroundColor;
+ this.canvas.backgroundImage = pc.canvasBackgroundImage!;
+ for (const object of pc.objects) {
+ this.canvas.add(object);
+ object.setCoords();
+ }
+
+ this.canvas.renderAll();
+ this.hasUndo = true;
+ }
+
+ redo(): void {
+ if (!this.hasUndo) {
+ return;
+ }
+ this.clearCanvas();
+ super.resetCanvasWrapper(this.cropWrapper);
+ const cw = this.cropWrapper;
+ cw.canvasWrapper!.style.width = cw.canvasWrapperWidth;
+ cw.canvasWrapper!.style.height = cw.canvasWrapperHeight;
+ const cc = this.cropCanvas;
+ cc.fabricWrapperEl!.style.left = cc.fabricWrapperElLeft;
+ cc.fabricWrapperEl!.style.top = cc.fabricWrapperElTop;
+ this.canvas.setDimensions({ width: cc.canvasWidth, height: cc.canvasHeight });
+ this.canvas.backgroundColor = cc.canvasBackgroundColor;
+ this.canvas.backgroundImage = cc.canvasBackgroundImage!;
+ for (const object of cc.objects) {
+ this.canvas.add(object);
+ object.setCoords();
+ }
+
+ this.canvas.renderAll();
+ this.hasUndo = false;
+ }
+
+ clearCanvas() {
+ // 不太清楚fabric会不会destory这些对象,所以防止万一起见,还是先删除了
+ const canvas = this.canvas;
+ canvas.backgroundImage = undefined;
+ const objects = canvas.getObjects();
+ for (const obj of objects) {
+ canvas.remove(obj);
+ }
+ this.canvas.clear();
+ }
+}
+
+
+export default class OperationHistory {
+
+ protected undoStack: OperationAction[];
+
+ protected redoStack: OperationAction[];
+
+ protected canvas: Canvas;
+
+ constructor(canvas: Canvas) {
+ this.undoStack = [];
+ this.redoStack = [];
+ this.canvas = canvas;
+ }
+
+ getCanvas() {
+ return this.canvas;
+ }
+
+ redo(): boolean {
+ if (this.redoStack.length > 0) {
+ const opr = this.redoStack.pop()!;
+ opr.redo();
+ this.undoStack.push(opr!);
+ return true;
+ }
+ return false;
+ }
+
+ undo(): boolean {
+ if (this.undoStack.length > 0) {
+ const opr = this.undoStack.pop()!;
+ opr.undo();
+ this.redoStack.push(opr);
+ return true;
+ }
+ return false;
+ }
+
+
+ recordCreateAction(object: FabricObject) {
+ this.undoStack.push(new CreateAction(this.canvas, object));
+ this.clearRedoStack();
+ }
+
+ recordRemoveAction(object: FabricObject) {
+ this.undoStack.push(new RemoveAction(this.canvas, object));
+ this.clearRedoStack();
+ }
+
+ recordMoveAction(object: FabricObject, previousX: number, previousY: number) {
+ this.undoStack.push(new MoveAction(this.canvas, object, previousX, previousY));
+ this.clearRedoStack();
+ }
+
+ // 放缩可能会改变图形的位置
+ recordScaleAction(object: FabricObject, previousWidth: number, previousHeight: number,
+ previousX: number, previousY: number) {
+ this.undoStack.push(new ScaleAction(this.canvas, object, previousWidth, previousHeight, previousX, previousY));
+ this.clearRedoStack();
+ }
+
+ recordEllipseScaleAction(object: Ellipse, previousWidth: number, previousHeight: number,
+ previousX: number, previousY: number, previousRX: number, previousRY: number) {
+ this.undoStack.push(new EllipseScaleAction(this.canvas, object, previousWidth, previousHeight, previousX, previousY, previousRX, previousRY));
+ this.clearRedoStack();
+ }
+
+ recordRatioScaleAction(object: FabricObject, previousScaleX: number, previousScaleY: number,
+ previousX: number, previousY: number) {
+ this.undoStack.push(new RatioScaleAction(this.canvas, object, previousScaleX, previousScaleY,
+ previousX, previousY));
+ this.clearRedoStack();
+ }
+
+ recordFlipXAction(previous: FlipXUndoProps, current: FlipXUndoProps) {
+ this.undoStack.push(new FlipXAction(this.canvas, previous, current));
+ this.clearRedoStack();
+ }
+
+ recordFlipYAction(previous: FlipYUndoProps, current: FlipYUndoProps) {
+ this.undoStack.push(new FlipYAction(this.canvas, previous, current));
+ this.clearRedoStack();
+ }
+
+ recordRotateAction(previous: RotateProps, current: RotateProps) {
+ this.undoStack.push(new RotationAction(this.canvas, previous, current));
+ this.clearRedoStack();
+ }
+
+ recordCropAction(wrapper: CanvasDetailedProps, canvas: FabricCanvasProps, cropWrapper: CanvasDetailedProps, cropCanvas: FabricCanvasProps) {
+ this.undoStack.push(new CropAction(this.canvas, wrapper, canvas, cropWrapper, cropCanvas));
+ this.clearRedoStack();
+ }
+
+ clearRedoStack() {
+ this.redoStack = [];
+ }
+
+ clearStack(){
+ this.redoStack = []
+ this.undoStack = []
+ }
+}
\ No newline at end of file
diff --git a/src/image_editor.ts b/src/image_editor.ts
new file mode 100644
index 0000000000000000000000000000000000000000..710bc3ba8cd50e0f0001917ba1e83ae4a46c43a5
--- /dev/null
+++ b/src/image_editor.ts
@@ -0,0 +1,251 @@
+import { Canvas, FabricImage, FabricObject, Point, StaticCanvas } from "fabric";
+import ArrowOperator from "./operator/arrow_operator";
+import DrawOperator from "./operator/draw_operator";
+import EllipseOperator from "./operator/ellipse_operator";
+import { OperatorType } from "./image_editor_operator";
+import ElementManager from "./element_manager";
+import MosaicOperator from "./operator/mosaic_operator";
+import RectangleOperator from "./operator/rect_operator";
+import TextOperator from "./operator/text_operator";
+import OperationHistory from "./history";
+import { Screenshoter } from "./screenshoter";
+import { ImageEditorShortcutManager } from "./shortcut_manager";
+
+export default class ImageEditor {
+
+ private canvas: Canvas;
+
+ private screenshoter: Screenshoter;
+
+ private history: OperationHistory;
+
+ private operatorType: OperatorType = OperatorType.NONE;
+
+ private elementManager: ElementManager;
+
+ private rectOperator: RectangleOperator;
+
+ private ellipseOperator: EllipseOperator;
+
+ private arrowOperator: ArrowOperator;
+
+ private drawOperator: DrawOperator;
+
+ private mosaicOperator: MosaicOperator;
+
+ private textOperator: TextOperator;
+
+ private operatorMap = new Map();
+
+ readonly initWrapperLeft: string;
+
+ readonly initWrapperTop: string;
+
+ private shortcutManager: ImageEditorShortcutManager;
+
+ constructor(canvas: Canvas, elementManager: ElementManager) {
+ this.elementManager = elementManager;
+ this.canvas = canvas;
+ this.history = new OperationHistory(canvas);
+ this.rectOperator = new RectangleOperator(this);
+ this.ellipseOperator = new EllipseOperator(this);
+ this.arrowOperator = new ArrowOperator(this);
+ this.drawOperator = new DrawOperator(this);
+ this.mosaicOperator = new MosaicOperator(this);
+ this.textOperator = new TextOperator(this);
+ this.bindOperators();
+ this.operatorMap.set(OperatorType.RECT, this.rectOperator);
+ this.operatorMap.set(OperatorType.ELLIPSE, this.ellipseOperator);
+ this.operatorMap.set(OperatorType.ARROW, this.arrowOperator);
+ this.operatorMap.set(OperatorType.TEXT, this.textOperator);
+ this.operatorMap.set(OperatorType.DRAW, this.drawOperator);
+ this.operatorMap.set(OperatorType.MOSAIC, this.mosaicOperator);
+ this.canvas.selection = false;
+ this.screenshoter = new Screenshoter();
+ const canvasWrapper = elementManager.canvasWrapper;
+ this.initWrapperLeft = canvasWrapper.style.left;
+ this.initWrapperTop = canvasWrapper.style.top;
+ this.shortcutManager = new ImageEditorShortcutManager(this);
+ }
+
+ init() {
+ this.elementManager.init(this);
+ this.elementManager.bindEvents();
+ this.screenshoter.init(this, this.elementManager);
+ }
+
+ bindOperators() {
+ const rectOperator = this.rectOperator;
+ this.canvas.on('mouse:down', rectOperator.handleMouseDown.bind(rectOperator));
+ this.canvas.on('mouse:move', rectOperator.handleMouseMove.bind(rectOperator));
+ this.canvas.on('mouse:up', rectOperator.handleMouseUp.bind(rectOperator));
+ const ellipseOperator = this.ellipseOperator;
+ this.canvas.on('mouse:down', ellipseOperator.handleMouseDown.bind(ellipseOperator));
+ this.canvas.on('mouse:move', ellipseOperator.handleMouseMove.bind(ellipseOperator));
+ this.canvas.on('mouse:up', ellipseOperator.handleMouseUp.bind(ellipseOperator));
+ const arrowOperator = this.arrowOperator;
+ this.canvas.on('mouse:down', arrowOperator.handleMouseDown.bind(arrowOperator));
+ this.canvas.on('mouse:move', arrowOperator.handleMouseMove.bind(arrowOperator));
+ this.canvas.on('mouse:up', arrowOperator.handleMouseUp.bind(arrowOperator));
+ const textOperator = this.textOperator;
+ this.canvas.on('mouse:down:before', textOperator.handleMouseDownBefore.bind(textOperator))
+ this.canvas.on('mouse:down', textOperator.handleMouseDown.bind(textOperator));
+ this.canvas.on('mouse:up', textOperator.handleMouseUp.bind(textOperator));
+ }
+
+ getCanvas() {
+ return this.canvas;
+ }
+
+ getActiveOperator() {
+ return this.operatorMap.get(this.operatorType);
+ }
+
+ getOperatorType() {
+ return this.operatorType;
+ }
+
+ changeOperatorType(type: OperatorType) {
+ // 如果要修改的type和当前的是一样的话,那么就不变
+ if (this.operatorType == type) {
+ return;
+ }
+ const previous = this.operatorType;
+ const current = type;
+ switch (previous) {
+ case OperatorType.MOSAIC: this.mosaicOperator.endMosaicMode(); break
+ case OperatorType.DRAW: this.drawOperator.endDrawMode(); break;
+ }
+ switch (current) {
+ case OperatorType.MOSAIC: this.mosaicOperator.startMosaicMode(); break
+ case OperatorType.DRAW: this.drawOperator.startDrawMode(); break;
+ }
+ this.operatorType = current;
+ if (current == OperatorType.NONE) {
+ this.canvas.getObjects().forEach((obj: FabricObject) => {
+ // 重新调整完后,要将对象激活一下,这或许是个坑?
+ this.canvas.setActiveObject(obj);
+ })
+ }
+ }
+
+ getHistory(): OperationHistory {
+ return this.history;
+ }
+
+ setCanvasHeight(height: number) {
+ this.canvas.setDimensions({ height })
+ }
+
+ setCanvasWidth(width: number) {
+ this.canvas.setDimensions({ width })
+ }
+
+ setCanvasDims(width: number, height: number) {
+ this.canvas.setDimensions({ height, width });
+ }
+
+ transformX(fabricLeft: number) {
+ const x = this.canvas.backgroundImage!.getX()
+ this.canvas.backgroundImage?.setX(x + fabricLeft)
+ let objs = this.canvas.getObjects();
+ if (objs == null) {
+ objs = [];
+ }
+ for (const obj of objs) {
+ obj.left += fabricLeft;
+ obj.setCoords();
+ }
+ this.canvas.renderAll();
+ }
+
+ transformY(fabricTop: number) {
+ const y = this.canvas.backgroundImage!.getY()
+ this.canvas.backgroundImage?.setY(y + fabricTop)
+ let objs = this.canvas.getObjects();
+ if (objs == null) {
+ objs = [];
+ }
+ for (const obj of objs) {
+ obj.top += fabricTop;
+ obj.setCoords();
+ }
+ this.canvas.renderAll();
+ }
+
+ getAreaImageInfo(start: Point, bottom: Point) {
+
+ const width = bottom.x - start.x;
+ const height = bottom.y - start.y;
+
+ const tempCanvas = new StaticCanvas(undefined, { width, height });
+ tempCanvas.add(new FabricImage(this.canvas.lowerCanvasEl, {
+ left: 0,
+ top: 0,
+ }))
+
+ const image = tempCanvas.toDataURL({
+ format: 'png',
+ left: start.x,
+ top: start.y,
+ width, height,
+ multiplier: 1
+ });
+
+ return image;
+ }
+
+ async renderToCanvas(imageDataUrl: string) {
+ const canvas = this.canvas;
+ const objects = canvas.getObjects();
+ for (const object of objects) {
+ canvas.remove(object);
+ }
+ canvas.backgroundImage = undefined;
+ canvas.clear();
+ const elementManger = this.elementManager;
+ let ret;
+ await FabricImage.fromURL(imageDataUrl).then(img => {
+ ret = img;
+ const width = img.width;
+ const height = img.height;
+ canvas.setDimensions({ width, height })
+ img.setX(0);
+ img.setY(0);
+ canvas.backgroundImage = img;
+ canvas.backgroundColor = '#FFF';
+ const style = elementManger.getFabricWrapper()!.style;
+ style.left = '0px';
+ style.top = '0px';
+ elementManger.resetWrapper(width, height);
+ canvas.renderAll();
+ })
+ return ret;
+ }
+
+ getScreenshoter() {
+ return this.screenshoter;
+ }
+
+ // 保存状态,后面还原直接用
+ storeCanvasState() {
+ const wrapperInfo = this.elementManager.calculateCanvasWrapper();
+ const canvasInfo = this.elementManager.calculateCanvasInfo();
+ return {
+ wrapper: wrapperInfo,
+ canvas: canvasInfo
+ }
+ }
+
+ destory() {
+ this.shortcutManager.destroy();
+ }
+
+ removeActiveObjects() {
+ const active = this.canvas!.getActiveObject();
+ if (active) {
+ this.canvas.remove(active);
+ this.history.recordRemoveAction(active);
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/image_editor_operator.ts b/src/image_editor_operator.ts
new file mode 100644
index 0000000000000000000000000000000000000000..282eb725c19c6ac7e4b86a82275a472f52e1b098
--- /dev/null
+++ b/src/image_editor_operator.ts
@@ -0,0 +1,27 @@
+
+export interface ImageEditorOperator {
+ handleMouseDownBefore?(event: any): void;
+ handleMouseDown?(event: any): void;
+ handleMouseMove?(event: any): void;
+ handleMouseUp?(event: any): void;
+}
+
+export interface OperatorProps {
+
+ setOperatorSize(width: number): void;
+
+ setOperatorColor(color: string): void;
+
+ getOperatorSize(): number;
+
+ getOperatorColor(): string;
+}
+
+export const DEFAULT_COLOR = '#FF0000';
+
+export const DEFAULT_STROKE_WIDTH = 4;
+
+export enum OperatorType {
+ RECT, ELLIPSE, ARROW, DRAW, TEXT, MOSAIC, NONE
+}
+
diff --git a/src/main.ts b/src/main.ts
new file mode 100644
index 0000000000000000000000000000000000000000..dff202a2df86cc0c1fe26e3d6021348fd192ea39
--- /dev/null
+++ b/src/main.ts
@@ -0,0 +1,316 @@
+import { Canvas, FabricImage } from 'fabric';
+import ElementManager from './element_manager';
+import ImageEditor from './image_editor';
+
+class ImageEditorHelper {
+
+ static currentImageEditor: ImageEditor | undefined;
+
+ static dpr = window.devicePixelRatio || 1;
+
+ static CANVAS_DEFAULT_WIDTH = 100;
+
+ static CANVAS_DEFAULT_HEIGHT = 100;
+
+ static createImageEditor(imageUrl: string) {
+
+ const elements = this.createElement()
+ const eleManager = new ElementManager(elements);
+
+ const resizer = (canvas: Canvas, width: number, height: number) => {
+ this.resizeCanvas(canvas, eleManager, width, height);
+ }
+ const canvas = this.initCanvas(elements.canvas, imageUrl, resizer);
+ const editor = new ImageEditor(canvas, eleManager);
+ editor.init();
+ return editor;
+ }
+
+ private static createElement(): Record {
+
+ const width = this.CANVAS_DEFAULT_WIDTH, height = this.CANVAS_DEFAULT_HEIGHT;
+
+ const wrapper = document.getElementById("app")!;
+ wrapper.style.width = '100%';
+ wrapper.style.height = '100%';
+ wrapper.style.position = 'absolute';
+ wrapper.style.visibility = 'hidden';
+ document.body.appendChild(wrapper);
+
+ const toolbar = document.createElement("div");
+ const toolbarMenu = this.initToolbar(toolbar);
+
+ // 不考虑滚动条的事,出现滚动条的话,就给点偏差
+ // canvasWrapper,包裹画板
+ const canvasWrapper = document.createElement("div");
+ canvasWrapper.style.backgroundColor = 'white';
+ canvasWrapper.style.position = 'relative';
+ canvasWrapper.style.overflow = 'hidden';
+
+ const resizers = this.createCanvasResizer(wrapper);
+
+ // 添加一张用于截图的canvas,以及8个resizer
+ const screenshotCanvas = document.createElement("canvas")
+ screenshotCanvas.style.display = 'none';
+ screenshotCanvas.style.left = '0';
+ screenshotCanvas.style.top = '0';
+ screenshotCanvas.style.position = 'absolute';
+
+ // 通过wrapper的拉伸,应该是可以的拉伸底图,底图是白色的
+ // 拉伸的过程中看不出来,等拉伸完统一结算
+ // 拉伸下右不需要考虑太多,拉伸上左要让图片进行偏移
+
+ // 给的默认值,不需要考虑太多
+ const canvas = document.createElement("canvas")
+ canvas.style.width = width + 'px';
+ canvas.style.height = height + 'px';
+ canvas.width = width;
+ canvas.height = height;
+
+ canvasWrapper.append(canvas);
+ wrapper.appendChild(canvasWrapper)
+ wrapper.appendChild(toolbar)
+ wrapper.appendChild(screenshotCanvas);
+ document.body.appendChild(wrapper);
+
+ const screenshotResizer = this.createScreenshotResizers(wrapper);
+ const screenshotToolbar = this.createScreenshotToolbar(wrapper);
+
+ const rets = {
+ ...toolbarMenu, canvas, canvasWrapper, ...resizers, toolbar, wrapper, screenshotCanvas, screenshotResizer, screenshotToolbar
+ } as any;
+ return rets;
+ }
+ static createScreenshotToolbar(parent: HTMLElement) {
+ const toolbar = document.createElement("div");
+ const style = toolbar.style;
+ style.position = 'absolute';
+ style.backgroundColor = 'rgb(229,230,231)'
+ style.borderRadius = '4px 4px 4px 4px';
+ style.height = '24px';
+ style.width = '64px';
+ style.paddingTop = '4px';
+ style.paddingBottom = '4px';
+ style.display = 'none';
+ const cancelScreenshot = this.appendMenu(toolbar, './assets/cancel.svg', 8, 0);
+ const confirmScreenshot = this.appendMenu(toolbar, './assets/confirm.svg', 0, 0);
+ parent.appendChild(toolbar);
+ return {
+ toolbar,
+ screenshot: {
+ confirm: confirmScreenshot,
+ cancel: cancelScreenshot
+ }
+ };
+ }
+ static createScreenshotResizers(parent: HTMLElement) {
+ const createResizer = () => {
+ const resizer = document.createElement("div");
+ const style = resizer.style;
+ style.left = '0';
+ style.top = '0';
+ style.position = 'absolute';
+ style.width = '8px';
+ style.height = '8px';
+ style.boxSizing = 'border-box';
+ style.border = '1px solid #19a918'
+ style.transform = 'translate(-50%,-50%)';
+ style.display = 'none';
+ resizer.addEventListener('dragstart', function (event) {
+ event.preventDefault();
+ })
+ resizer.draggable = false;
+ // style.display = 'none'
+ parent.appendChild(resizer);
+ return resizer;
+ }
+ const northWest = createResizer();
+ const north = createResizer();
+ const northEast = createResizer();
+ const east = createResizer();
+ const southEast = createResizer();
+ const south = createResizer();
+ const southWest = createResizer();
+ const west = createResizer();
+
+ return {
+ northWest, north, northEast, east, southEast, south, southWest, west
+ }
+ }
+
+ // topbar和bottombar都要做固定width,居中
+ // 不要考虑其它的,尾部也是一样的
+ private static initToolbar(toolbar: HTMLElement): Record {
+
+ toolbar.style.padding = "8px";
+ toolbar.style.backgroundColor = "#e5e6e7";
+ toolbar.style.borderRadius = "4px 4px 4px 4px";
+ toolbar.style.height = "24px";
+ toolbar.style.width = "690px";
+ toolbar.style.position = "absolute";
+
+ const ret = {} as any;
+
+ ret.rectangleMenu = this.appendMenu(toolbar, './assets/rect.svg');
+ ret.ellipseMenu = this.appendMenu(toolbar, './assets/circle.svg');
+ ret.arrowMenu = this.appendMenu(toolbar, './assets/arrow.svg');
+ ret.drawMenu = this.appendMenu(toolbar, './assets/draw.svg');
+ ret.textMenu = this.appendMenu(toolbar, './assets/text.svg');
+ ret.mosaicMenu = this.appendMenu(toolbar, './assets/mosaic.svg');
+
+ ret.shrinkMenu = this.appendMenu(toolbar, './assets/shrink.svg', 42);
+ ret.extendMenu = this.appendMenu(toolbar, './assets/extend.svg');
+ ret.flipXMenu = this.appendMenu(toolbar, './assets/flipX.svg');
+ ret.flipYMenu = this.appendMenu(toolbar, './assets/flipY.svg');
+
+
+ ret.rotateCounterClockwiseMenu = this.appendMenu(toolbar, './assets/rotate.svg');
+ ret.rotateCounterClockwiseMenu.style.transform = 'rotateY(180deg)';
+ ret.rotateClockwiseMenu = this.appendMenu(toolbar, './assets/rotate.svg');
+ ret.cropMenu = this.appendMenu(toolbar, './assets/crop.svg');
+
+ ret.undoMenu = this.appendMenu(toolbar, './assets/undo.svg', 38);
+ ret.redoMenu = this.appendMenu(toolbar, './assets/redo.svg');
+ ret.resetMenu = this.appendMenu(toolbar, './assets/reset.svg');
+ ret.cancaleMenu = this.appendMenu(toolbar, './assets/cancel.svg', 36);
+ ret.confirmMenu = this.appendMenu(toolbar, './assets/confirm.svg', 0, 0);
+ return ret;
+ }
+
+ private static appendMenu(topbar: HTMLElement, url: string, marginLeft = 0, marginRight = 8): HTMLElement {
+ const menu = document.createElement("div")
+ menu.style.display = "inline-block";
+ menu.style.width = "24px";
+ menu.style.height = "24px";
+ menu.style.marginRight = marginRight + 'px';
+ menu.style.borderRadius = "4px"
+ menu.style.lineHeight = "1";
+ if (marginLeft != 0) {
+ menu.style.marginLeft = marginLeft + "px"
+ }
+
+ const icon = document.createElement("i");
+ icon.style.display = "block";
+ icon.style.width = "24px";
+ icon.style.height = "24px";
+ icon.style.backgroundSize = "100% 100%";
+ icon.style.backgroundRepeat = "no-repeat";
+ icon.style.cursor = "pointer";
+ icon.style.opacity = "0.8";
+ icon.style.backgroundImage = `url('${url}')`
+ menu.appendChild(icon);
+ topbar.appendChild(menu);
+ return menu;
+ }
+
+ private static initCanvas(dom: HTMLCanvasElement, imageUrl: string, resizer: (canvas: Canvas, width: number, height: number) => void): fabric.Canvas {
+
+ // 随便给个默认值,后面初始化的时候改掉
+ const canvas = new Canvas(dom, {
+ width: this.CANVAS_DEFAULT_WIDTH, height: this.CANVAS_DEFAULT_HEIGHT
+ })
+
+ FabricImage.fromURL(imageUrl).then(img => {
+ // 使用setX和setY
+ img.setX(0);
+ img.setY(0);
+ canvas.backgroundImage = img;
+ canvas.backgroundColor = '#FFF';
+ // 设置完需要渲染一下
+ canvas.renderAll();
+ const width = img.width, height = img.height;
+ resizer(canvas, width, height);
+ })
+
+ return canvas;
+ }
+
+ static resizeCanvas(fbCanvas: Canvas, manager: ElementManager, width: number, height: number) {
+ const dpr = this.dpr;
+ const wrapper = manager.wrapper;
+ const canvasWrapper = manager.canvasWrapper;
+ const canvas = manager.canvas;
+
+ canvasWrapper.style.width = width + 'px';
+ canvasWrapper.style.height = height + 'px';
+
+ // top和left都要好好计算一下
+ const rect = wrapper.getBoundingClientRect();
+ const wrapperWidth = rect.width;
+ const wrapperHeight = rect.height;
+
+ let leftOffset = (wrapperWidth - width) / 2;
+ if (leftOffset <= 20) {
+ leftOffset = 20;
+ }
+ let topOffset = (wrapperHeight - height) / 2;
+ if (topOffset <= 20) {
+ topOffset = 20;
+ }
+
+ canvasWrapper.style.left = leftOffset + 'px';
+ canvasWrapper.style.top = topOffset + 'px';
+
+ canvas.style.width = width + 'px';
+ canvas.style.height = height + 'px';
+ canvas.width = Math.round(width * dpr);
+ canvas.height = Math.round(height * dpr);
+
+ fbCanvas.setDimensions({ width, height })
+
+ manager.fixComponentsPosition();
+ wrapper.style.visibility = 'visible';
+ }
+
+ private static createCanvasResizer(wrapper: HTMLElement) {
+
+ const squareSize = 12;
+
+ const northResizer = document.createElement('div');
+ const northWestResizer = document.createElement('div');
+ const westResizer = document.createElement('div');
+ const southWestResizer = document.createElement('div');
+ const southResizer = document.createElement('div');
+ const southEastResizer = document.createElement('div');
+ const eastResizer = document.createElement('div');
+ const northEastResizer = document.createElement('div');
+
+ function format(ele: HTMLDivElement) {
+ ele.style.width = squareSize + 'px';
+ ele.style.height = squareSize + 'px';
+ ele.style.backgroundColor = 'white';
+ ele.style.position = 'absolute'
+ ele.style.border = 'solid 1px #000';
+ ele.style.boxSizing = 'border-box'
+ ele.draggable = false;
+ ele.addEventListener('dragstart', function (event) {
+ event.preventDefault();
+ })
+ }
+
+ format(northResizer);
+ format(northWestResizer);
+ format(westResizer);
+ format(southWestResizer);
+ format(southResizer);;
+ format(southEastResizer);
+ format(eastResizer);
+ format(northEastResizer);
+
+ wrapper.appendChild(northResizer);
+ wrapper.appendChild(northWestResizer);
+ wrapper.appendChild(westResizer);
+ wrapper.appendChild(southWestResizer);
+ wrapper.appendChild(southResizer);
+ wrapper.appendChild(southEastResizer);
+ wrapper.appendChild(eastResizer);
+ wrapper.appendChild(northEastResizer);
+
+ return {
+ northResizer, northWestResizer, westResizer, southWestResizer,
+ southResizer, southEastResizer, eastResizer, northEastResizer
+ }
+ }
+}
+
+ImageEditorHelper.currentImageEditor = ImageEditorHelper.createImageEditor('/basic.jpg');
\ No newline at end of file
diff --git a/src/operator/arrow_operator.ts b/src/operator/arrow_operator.ts
new file mode 100644
index 0000000000000000000000000000000000000000..a102ebb5da27fad7de64669ac0246ca6326acf3c
--- /dev/null
+++ b/src/operator/arrow_operator.ts
@@ -0,0 +1,104 @@
+import { Canvas } from "fabric";
+import ImageEditor from "../image_editor";
+import { DEFAULT_COLOR, DEFAULT_STROKE_WIDTH, ImageEditorOperator, OperatorProps, OperatorType } from "../image_editor_operator";
+import Arrow from "./fabric_arrow";
+import FabricObjectChangeHelper from "./move_helper";
+
+export default class ArrowOperator implements ImageEditorOperator, OperatorProps {
+
+ private imageEditor: ImageEditor;
+
+ private canvas: Canvas;
+
+ private start: boolean;
+
+ private current: Arrow | undefined = undefined;
+
+ private startX: number;
+
+ private startY: number;
+
+ private color: string = DEFAULT_COLOR;
+
+ private strokeWidth: number = DEFAULT_STROKE_WIDTH;
+
+ constructor(imageEditor: ImageEditor) {
+ this.imageEditor = imageEditor;
+ this.canvas = imageEditor.getCanvas();
+ this.start = false;
+ this.startX = 0;
+ this.startY = 0;
+ }
+ getOperatorSize(): number {
+ return this.strokeWidth;
+ }
+ getOperatorColor(): string {
+ return this.color;
+ }
+
+ setOperatorSize(width: number): void {
+ this.strokeWidth = width;
+ }
+
+ setOperatorColor(color: string): void {
+ this.color = color;
+ }
+
+ handleMouseDown(event: any): void {
+ let refuse = this.canvas.getActiveObject() != undefined || this.start;
+ refuse = refuse || this.imageEditor.getOperatorType() != OperatorType.ARROW;
+ if (refuse) {
+ return
+ }
+ this.start = true;
+ const canvas = this.canvas;
+ canvas.requestRenderAll();
+ let point = canvas.getScenePoint(event.e);
+ const points: [number, number, number, number] = [point.x, point.y, point.x, point.y];
+ this.startX = point.x;
+ this.startY = point.y;
+ const arrow = new Arrow(points, {
+ strokeWidth: this.strokeWidth,
+ stroke: this.color,
+ lockScalingFlip: true
+ })
+ this.current = arrow;
+ canvas.add(arrow);
+ }
+
+ handleMouseMove(event: any): void {
+ if (!this.start) {
+ return;
+ }
+ const pointer = this.canvas.getScenePoint(event.e);
+ this.current?.set({
+ x2: pointer.x,
+ y2: pointer.y
+ })
+ this.canvas.renderAll();
+ }
+
+ handleMouseUp(event: any): void {
+ if (this.imageEditor.getOperatorType() != OperatorType.ARROW || !this.start) {
+ return;
+ }
+ this.start = false;
+ const canvas = this.canvas;
+ const pointer = canvas.getScenePoint(event.e);
+ const notMeetMin = Math.abs(pointer.x - this.startX) < 8 && Math.abs(pointer.y - this.startY) < 8;
+ if (notMeetMin && this.current) {
+ this.canvas.remove(this.current);
+ } else {
+ const lastXY = this.current?.getXY();
+ const lastSize = {
+ width: this.current!.width,
+ height: this.current!.height
+ }
+ this.current!.set('lastXY', lastXY);
+ this.current!.set('lastDim', lastSize);
+ FabricObjectChangeHelper.listenMove(this.current!, this.imageEditor.getHistory());
+ FabricObjectChangeHelper.listenScale(this.current!, this.imageEditor.getHistory());
+ this.imageEditor.getHistory().recordCreateAction(this.current!);
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/operator/draw_operator.ts b/src/operator/draw_operator.ts
new file mode 100644
index 0000000000000000000000000000000000000000..e8554ece5409d11362cddd00dfb0e8f2ce92b78b
--- /dev/null
+++ b/src/operator/draw_operator.ts
@@ -0,0 +1,79 @@
+import { BaseBrush, Canvas, PencilBrush, Shadow } from "fabric";
+import OperationHistory from "../history";
+import ImageEditor from "../image_editor";
+import { DEFAULT_COLOR, DEFAULT_STROKE_WIDTH, OperatorProps } from "../image_editor_operator";
+import FabricObjectChangeHelper from "./move_helper";
+
+export default class DrawOperator implements OperatorProps {
+
+ private imageEditor: ImageEditor;
+
+ private canvas: Canvas;
+
+ private history: OperationHistory;
+
+ private color: string = DEFAULT_COLOR;
+
+ private strokeWidth: number = DEFAULT_STROKE_WIDTH;
+
+ private recorder: (event: any) => void;
+
+ private brush: BaseBrush | undefined;
+
+ constructor(imageEditor: ImageEditor) {
+ this.imageEditor = imageEditor;
+ this.canvas = imageEditor.getCanvas();
+ this.history = imageEditor.getHistory();
+ this.recorder = this.recordPathCreate.bind(this);
+ }
+ getOperatorSize(): number {
+ return this.strokeWidth;
+ }
+ getOperatorColor(): string {
+ return this.color;
+ }
+
+ setOperatorSize(width: number): void {
+ this.strokeWidth = width;
+ this.brush!.width = width;
+ }
+
+ setOperatorColor(color: string): void {
+ this.color = color;
+ this.brush!.color = color;
+ }
+
+ recordPathCreate(event: any) {
+ const path = event.path;
+ path.hoverCursor = 'default';
+ path.lockScalingFlip = true;
+ this.canvas.renderAll();
+ const lastXY = path.getXY();
+ const lastScale = {
+ x: path.scaleX,
+ y: path.scaleY
+ }
+ path.set('lastXY', lastXY);
+ path.set('lastScale', lastScale);
+ FabricObjectChangeHelper.listenMove(path, this.imageEditor.getHistory());
+ FabricObjectChangeHelper.listenRatioScale(path, this.imageEditor.getHistory());
+ this.history.recordCreateAction(path);
+ }
+
+ startDrawMode(): void {
+ const canvas = this.canvas;
+ canvas.isDrawingMode = true;
+ canvas.freeDrawingBrush = new PencilBrush(canvas);
+ this.brush = canvas.freeDrawingBrush;
+ let brush = canvas.freeDrawingBrush;
+ brush.color = this.color;
+ brush.width = this.strokeWidth;
+ brush.shadow = new Shadow({ blur: 2, offsetX: 0, offsetY: 0, color: '#333' })
+ this.canvas.on('path:created', this.recorder);
+ }
+
+ endDrawMode(): void {
+ this.canvas.isDrawingMode = false;
+ this.canvas.off('path:created', this.recorder);
+ }
+}
\ No newline at end of file
diff --git a/src/operator/ellipse_operator.ts b/src/operator/ellipse_operator.ts
new file mode 100644
index 0000000000000000000000000000000000000000..40a291e8f06943982be39e0dec73c215aa768505
--- /dev/null
+++ b/src/operator/ellipse_operator.ts
@@ -0,0 +1,123 @@
+import { Canvas, Ellipse } from "fabric";
+import ImageEditor from "../image_editor";
+import { DEFAULT_COLOR, DEFAULT_STROKE_WIDTH, ImageEditorOperator, OperatorProps, OperatorType } from "../image_editor_operator";
+import FabricObjectChangeHelper from "./move_helper";
+
+export default class EllipseOperator implements ImageEditorOperator, OperatorProps {
+
+ private imageEditor: ImageEditor;
+
+ private canvas: Canvas;
+
+ private start: boolean;
+
+ private current: Ellipse | undefined = undefined;
+
+ private startX: number;
+
+ private startY: number;
+
+ private strokeWidth: number = DEFAULT_STROKE_WIDTH;
+
+ private color: string = DEFAULT_COLOR;
+
+ constructor(imageEditor: ImageEditor) {
+ this.imageEditor = imageEditor;
+ this.canvas = imageEditor.getCanvas();
+ this.start = false;
+ this.startX = 0;
+ this.startY = 0;
+ }
+ getOperatorSize(): number {
+ return this.strokeWidth;
+ }
+ getOperatorColor(): string {
+ return this.color;
+ }
+ setOperatorSize(width: number): void {
+ this.strokeWidth = width;
+ }
+ setOperatorColor(color: string): void {
+ this.color = color;
+ }
+
+ handleMouseDown(event: any): void {
+ const canvas = this.canvas;
+ if (canvas.getActiveObject() != undefined) {
+ return;
+ }
+ if (this.imageEditor.getOperatorType() != OperatorType.ELLIPSE) {
+ return;
+ }
+ if (this.start) {
+ return;
+ }
+ this.start = true;
+ let pointer = canvas.getScenePoint(event.e);
+ this.startX = pointer.x;
+ this.startY = pointer.y;
+ this.current = new Ellipse({
+ left: this.startX,
+ top: this.startY,
+ rx: 0,
+ ry: 0,
+ fill: 'transparent',
+ stroke: this.color,
+ strokeWidth: this.strokeWidth,
+ lockScalingFlip: true
+ })
+ canvas.add(this.current);
+ }
+
+ handleMouseMove(event: any): void {
+ if (!this.start) {
+ return;
+ }
+ let pointer = this.canvas.getScenePoint(event.e);
+ let rx = Math.abs(pointer.x - this.startX) / 2;
+ let ry = Math.abs(pointer.y - this.startY) / 2;
+ if (rx > this.strokeWidth / 2) {
+ rx = rx - this.strokeWidth / 2
+ }
+ if (ry > this.strokeWidth / 2) {
+ ry = ry - this.strokeWidth / 2
+ }
+ let top = pointer.y < this.startY ? pointer.y : this.startY;
+ let left = pointer.x < this.startX ? pointer.x : this.startX;
+
+ this.current?.set('rx', rx);
+ this.current?.set('ry', ry);
+ this.current?.set('top', top);
+ this.current?.set('left', left);
+
+ this.canvas.requestRenderAll();
+ }
+
+ handleMouseUp(event: any): void {
+ if (!this.start || this.imageEditor.getOperatorType() != OperatorType.ELLIPSE) {
+ return
+ }
+ this.start = false;
+ let pointer = this.canvas.getScenePoint(event.e);
+ if (pointer.x == this.startX || pointer.y == this.startY) {
+ this.canvas.remove(this.current!);
+ } else {
+ const lastXY = this.current?.getXY();
+ const lastSize = {
+ width: this.current!.width,
+ height: this.current!.height
+ }
+ const lastRXY = {
+ rx: this.current!.rx,
+ ry: this.current!.ry
+ }
+ this.current!.set('lastXY', lastXY);
+ this.current!.set('lastDim', lastSize);
+ this.current!.set('lastRXY', lastRXY);
+ FabricObjectChangeHelper.listenMove(this.current!, this.imageEditor.getHistory());
+ FabricObjectChangeHelper.listenEllipseScale(this.current!, this.imageEditor.getHistory());
+ this.imageEditor.getHistory().recordCreateAction(this.current!);
+ }
+ this.current = undefined;
+ }
+}
\ No newline at end of file
diff --git a/src/operator/fabric_arrow.ts b/src/operator/fabric_arrow.ts
new file mode 100644
index 0000000000000000000000000000000000000000..c3dac992d7e36d4328c7820925de8b011ac63d8a
--- /dev/null
+++ b/src/operator/fabric_arrow.ts
@@ -0,0 +1,64 @@
+/* eslint-disable no-param-reassign */
+import { Line } from 'fabric';
+
+// 原来的createClass不太好使了,现在改成使用extends
+// 由上到下,由左到右
+export default class Arrow extends Line {
+
+ private arrowWidth: number = 4;
+
+ constructor([x1, y1, x2, y2] = [0, 0, 0, 0], options: any = {}) {
+ super([x1, y1, x2, y2], options);
+ }
+
+ // 8种情况的分解 是在所难免的
+ _render(ctx: CanvasRenderingContext2D): void {
+ super._render(ctx);
+ ctx.save();
+
+ // 角度要重新计算一下,应该根据宽高来搞
+ const xDiff = this.x2 - this.x1;
+ const yDiff = this.y2 - this.y1;
+ let y = yDiff > 0 ? this.height : -this.height;
+ let x = xDiff > 0 ? this.width : -this.width;
+ if (xDiff == 0) {
+ x = 0;
+ }
+ if (yDiff == 0) {
+ y = 0;
+ }
+ const angle = Math.atan2(y, x);
+ // 画一个三角形,然后将其移动到合适的位置去
+ this.translateArrow(ctx);
+ ctx.rotate(angle);
+ ctx.beginPath();
+ ctx.moveTo(this.arrowWidth * 2, 0);
+ ctx.lineTo(-this.arrowWidth * 2, this.arrowWidth * 2);
+ ctx.lineTo(-this.arrowWidth * 2, -this.arrowWidth * 2);
+ ctx.closePath();
+ ctx.fillStyle = (String)(this.stroke);
+ ctx.fill();
+ ctx.restore();
+ }
+
+ translateArrow(ctx: CanvasRenderingContext2D) {
+ const diffX = this.x2 - this.x1, diffY = this.y2 - this.y1;
+ if (diffX == 0 && diffY > 0) {
+ ctx.translate(0, this.height / 2);
+ } else if (diffX == 0 && diffY < 0) {
+ ctx.translate(0, -this.height / 2);
+ } else if (diffY == 0 && diffX > 0) {
+ ctx.translate(this.width / 2, 0);
+ } else if (diffY == 0 && diffX < 0) {
+ ctx.translate(-this.width / 2, 0);
+ } else if (diffX > 0 && diffY > 0) {
+ ctx.translate(this.width / 2, this.height / 2);
+ } else if (diffX > 0 && diffY < 0) {
+ ctx.translate(this.width / 2, -this.height / 2);
+ } else if (diffX < 0 && diffY > 0) {
+ ctx.translate(-this.width / 2, this.height / 2);
+ } else if (diffX < 0 && diffY < 0) {
+ ctx.translate(-this.width / 2, -this.height / 2);
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/operator/mosaic_operator.ts b/src/operator/mosaic_operator.ts
new file mode 100644
index 0000000000000000000000000000000000000000..d70497ff79f0c2abe0e8b1434f3305522d7e9fec
--- /dev/null
+++ b/src/operator/mosaic_operator.ts
@@ -0,0 +1,137 @@
+import { Canvas, PatternBrush, Shadow } from "fabric";
+import ImageEditor from "../image_editor";
+import OperationHistory from "../history";
+import { DEFAULT_COLOR, OperatorProps } from "../image_editor_operator";
+
+const blockSize = 5;
+
+function mosaicify(imageData: any) {
+ const { data } = imageData;
+ const iLen = imageData.height;
+ const jLen = imageData.width;
+ let index;
+ let i, _i, _iLen, j, _j, _jLen, r, g, b, a;
+ for (i = 0; i < iLen; i += blockSize) {
+ for (j = 0; j < jLen; j += blockSize) {
+ index = i * 4 * jLen + j * 4;
+ r = data[index];
+ g = data[index + 1];
+ b = data[index + 2];
+ a = data[index + 3];
+
+ _iLen = Math.min(i + blockSize, iLen);
+ _jLen = Math.min(j + blockSize, jLen);
+ for (_i = i; _i < _iLen; _i++) {
+ for (_j = j; _j < _jLen; _j++) {
+ index = _i * 4 * jLen + _j * 4;
+ data[index] = r;
+ data[index + 1] = g;
+ data[index + 2] = b;
+ data[index + 3] = a;
+ }
+ }
+ }
+ }
+}
+
+export default class MosaicOperator implements OperatorProps {
+
+ private canvas: Canvas;
+
+ private history: OperationHistory;
+
+ // 10小 20中 40大
+ private width: number = 20;
+
+ private mosaicBrush: PatternBrush | undefined;
+
+ private recorder: (event: any) => void;
+
+ constructor(imageEditor: ImageEditor) {
+ this.canvas = imageEditor.getCanvas();
+ this.history = imageEditor.getHistory();
+ this.recorder = this.recordPathCreate.bind(this);
+ }
+ getOperatorSize(): number {
+ return this.width;
+ }
+ getOperatorColor(): string {
+ return DEFAULT_COLOR;
+ }
+ setOperatorSize(width: number): void {
+ this.width = width;
+ this.mosaicBrush!.width = width;
+ }
+
+ setOperatorColor(): void {
+ // ignore
+ }
+
+ recordPathCreate(event: any) {
+ const path = event.path;
+ path.selectable = false;
+ path.evented = false;
+ path.hoverCursor = 'default';
+ path.lockScalingFlip = true
+ this.canvas.renderAll();
+ this.history.recordCreateAction(path);
+ }
+
+ startMosaicMode() {
+ const canvas = this.canvas;
+ canvas.isDrawingMode = true;
+ const mosaicBrush = new PatternBrush(canvas);
+ this.mosaicBrush = mosaicBrush;
+ canvas.freeDrawingBrush = mosaicBrush;
+ mosaicBrush.width = this.width;
+ mosaicBrush.shadow = new Shadow({
+ blur: 0,
+ offsetX: 0,
+ offsetY: 0,
+ affectStroke: true,
+ });
+ mosaicBrush.getPatternSrc = function () {
+ // 创立一个暂存 canvas 来绘製要画的图案
+ const cropping = {
+ left: 0,
+ top: 0,
+ width: canvas.width,
+ height: canvas.height,
+ };
+
+ const imageCanvas = canvas.toCanvasElement(1, cropping);
+ const imageCtx = imageCanvas.getContext('2d')!;
+ const imageData = imageCtx.getImageData(
+ 0,
+ 0,
+ imageCanvas.width,
+ imageCanvas.height,
+ );
+ mosaicify(imageData);
+ imageCtx.putImageData(imageData, 0, 0);
+
+ const patternCanvas = document.createElement('canvas'); // 这里的ceateElement一定要使用fabric内置的方法
+ const patternCtx = patternCanvas.getContext('2d')!;
+ patternCanvas.width = canvas.width || 0;
+ patternCanvas.height = canvas.height || 0;
+ patternCtx.drawImage(
+ imageCanvas,
+ 0,
+ 0,
+ imageCanvas.width,
+ imageCanvas.height,
+ cropping.left,
+ cropping.top,
+ cropping.width,
+ cropping.height,
+ );
+ return patternCanvas;
+ };
+ this.canvas.on('path:created', this.recorder);
+ }
+
+ endMosaicMode() {
+ this.canvas.isDrawingMode = false;
+ this.canvas.off('path:created', this.recorder);
+ }
+}
\ No newline at end of file
diff --git a/src/operator/move_helper.ts b/src/operator/move_helper.ts
new file mode 100644
index 0000000000000000000000000000000000000000..da73a2439cda0550543604ee02a9e5134aa16d89
--- /dev/null
+++ b/src/operator/move_helper.ts
@@ -0,0 +1,157 @@
+import { Ellipse, FabricObject } from "fabric";
+import OperationHistory from "../history";
+
+export default class FabricObjectChangeHelper {
+
+ static listenMove(obj: FabricObject, history: OperationHistory) {
+ obj.on('moving', () => {
+ if (!obj.get('movingFlag')) {
+ obj.set('movingFlag', true);
+ }
+ })
+ obj.on('mouseup', () => {
+ if (!obj.get('movingFlag')) {
+ return;
+ }
+ obj.set('movingFlag', undefined);
+ const pos = obj.get('lastXY');
+ obj.set('lastXY', obj.getXY());
+ history.recordMoveAction(obj, pos.x, pos.y);
+ })
+ }
+
+ static listenScale(obj: FabricObject, history: OperationHistory) {
+ obj.on('scaling', () => {
+ if (!obj.get('scalingFlag')) {
+ obj.set('scalingFlag', true);
+ }
+
+ const { scaleX, scaleY } = obj;
+ // 这个地方还不能四舍五入,在结束的地方四舍五入比较好
+ // 四舍五入导致向左上拉的时候,位置会出现偏差
+ obj.set({
+ width: obj.width * scaleX,
+ height: obj.height * scaleY,
+ scaleX: 1, // 重置缩放
+ scaleY: 1
+ });
+
+ // 缓存会导致图像不能正确的放缩
+ obj.objectCaching = false;
+ })
+
+ obj.on('mouseup', () => {
+ // 还原缓存
+ obj.objectCaching = true;
+ const flag = obj.get('scalingFlag')
+ if (!flag) {
+ return
+ }
+ obj.set('scalingFlag', undefined);
+ const dim = obj.get('lastDim');
+ const pos = obj.get('lastXY');
+ const width = dim.width;
+ const height = dim.height;
+ const currWidth = obj.width;
+ const currHeight = obj.height;
+ const currDim = {
+ width: currWidth,
+ height: currHeight
+ }
+ obj.set('lastDim', currDim);
+ obj.set('lastXY', obj.getXY());
+ history.recordScaleAction(obj, width, height, pos.x, pos.y)
+ })
+ }
+
+ static listenEllipseScale(obj: Ellipse, history: OperationHistory) {
+ obj.on('scaling', () => {
+ if (!obj.get('scalingFlag')) {
+ obj.set('scalingFlag', true);
+ }
+
+ const { scaleX, scaleY } = obj;
+ const strokeWidth = obj.strokeWidth;
+ let rx = obj.width * scaleX / 2;
+ let ry = obj.height * scaleY / 2;
+ if (rx > strokeWidth / 2) {
+ rx = rx - strokeWidth / 2;
+ }
+ if (ry > strokeWidth / 2) {
+ ry = ry - strokeWidth / 2;
+ }
+ obj.set({
+ rx, ry,
+ width: obj.width * scaleX,
+ height: obj.height * scaleY,
+ scaleX: 1, // 重置缩放
+ scaleY: 1
+ });
+ // 缓存会导致图像不能正确的放缩
+ obj.objectCaching = false;
+ })
+
+ obj.on('mouseup', () => {
+ // 还原缓存
+ obj.objectCaching = true;
+ const flag = obj.get('scalingFlag')
+ if (!flag) {
+ return
+ }
+ obj.set('scalingFlag', undefined);
+ const dim = obj.get('lastDim');
+ const pos = obj.get('lastXY');
+ const rxy = obj.get('lastRXY');
+ console.log(rxy)
+ const width = dim.width;
+ const height = dim.height;
+ const rx = rxy.rx;
+ const ry = rxy.ry;
+ const currWidth = obj.width;
+ const currHeight = obj.height;
+ const currDim = {
+ width: currWidth,
+ height: currHeight
+ }
+ const currRXY = {
+ rx: obj.rx,
+ ry: obj.ry
+ }
+ obj.set('lastDim', currDim);
+ obj.set('lastXY', obj.getXY());
+ obj.set('lastRXY', currRXY)
+ history.recordEllipseScaleAction(obj, width, height, pos.x, pos.y, rx, ry)
+ })
+ }
+
+ // 框选,椭圆,箭头,都不是等比例缩放
+ // 文字、画笔是等比例缩放,要考虑一下等比例缩放的问题
+ static listenRatioScale(obj: FabricObject, history: OperationHistory) {
+ obj.on('scaling', () => {
+ if (!obj.get('scalingFlag')) {
+ obj.set('scalingFlag', true);
+ }
+ })
+
+ obj.on('mouseup', () => {
+ // 还原缓存
+ obj.objectCaching = true;
+ const flag = obj.get('scalingFlag')
+ if (!flag) {
+ return
+ }
+ obj.set('scalingFlag', undefined);
+ const scale = obj.get('lastScale');
+ const pos = obj.get('lastXY');
+ const scaleX = scale.x;
+ const scaleY = scale.y;
+ const currScale = {
+ x: obj.scaleX,
+ y: obj.scaleY
+ }
+ obj.set('lastScale', currScale);
+ obj.set('lastXY', obj.getXY());
+ history.recordRatioScaleAction(obj, scaleX, scaleY, pos.x, pos.y)
+ })
+ }
+}
\ No newline at end of file
diff --git a/src/operator/rect_operator.ts b/src/operator/rect_operator.ts
new file mode 100644
index 0000000000000000000000000000000000000000..fba994cec8256737ef233cc8b86ebc733c947661
--- /dev/null
+++ b/src/operator/rect_operator.ts
@@ -0,0 +1,114 @@
+import { Canvas, Rect } from "fabric";
+import ImageEditor from "../image_editor";
+import { DEFAULT_COLOR, DEFAULT_STROKE_WIDTH, ImageEditorOperator, OperatorProps, OperatorType } from "../image_editor_operator";
+import FabricObjectChangeHelper from "./move_helper";
+
+export default class RectangleOperator implements ImageEditorOperator, OperatorProps {
+
+ private imageEditor: ImageEditor;
+
+ private canvas: Canvas;
+
+ private start: boolean;
+
+ private startX: number;
+
+ private startY: number;
+
+ private strokeWidth: number = DEFAULT_STROKE_WIDTH;
+
+ private color: string = DEFAULT_COLOR;
+
+ private current: Rect | undefined;
+
+ constructor(imageEditor: ImageEditor) {
+ this.imageEditor = imageEditor;
+ this.canvas = imageEditor.getCanvas();
+ this.start = false;
+ this.startX = 0;
+ this.startY = 0;
+ }
+ getOperatorSize(): number {
+ return this.strokeWidth;
+ }
+
+ getOperatorColor(): string {
+ return this.color;
+ }
+
+ setOperatorSize(width: number): void {
+ this.strokeWidth = width;
+ }
+
+ setOperatorColor(color: string): void {
+ this.color = color;
+ }
+
+ handleMouseDown(event: any): void {
+ const canvas = this.canvas;
+ if (canvas.getActiveObject() != undefined) {
+ return;
+ }
+ if (this.imageEditor.getOperatorType() != OperatorType.RECT) {
+ return;
+ }
+ if (this.start) {
+ return;
+ }
+ this.start = true;
+ let pointer = canvas.getScenePoint(event.e);
+ this.startX = pointer.x;
+ this.startY = pointer.y;
+ this.current = new Rect({
+ left: this.startX,
+ top: this.startY,
+ width: 0,
+ height: 0,
+ fill: 'transparent',
+ stroke: this.color,
+ strokeWidth: this.strokeWidth,
+ lockScalingFlip: true
+ })
+ canvas.add(this.current);
+ }
+ handleMouseMove(event: any): void {
+ if (!this.start) {
+ return;
+ }
+ let pointer = this.canvas.getScenePoint(event.e);
+ let width = Math.abs(pointer.x - this.startX);
+ let height = Math.abs(pointer.y - this.startY);
+ const left = pointer.x < this.startX ? pointer.x : this.startX;
+ const top = pointer.y < this.startY ? pointer.y : this.startY;
+
+ this.current?.set('width', Math.round(width));
+ this.current?.set('height', Math.round(height));
+ this.current?.set('top', Math.round(top));
+ this.current?.set('left', Math.round(left));
+ this.canvas.requestRenderAll();
+ }
+
+ handleMouseUp(event: any): void {
+ if (!this.start || this.imageEditor.getOperatorType() != OperatorType.RECT) {
+ return;
+ }
+ this.start = false;
+ let pointer = this.canvas.getScenePoint(event.e);
+ let width = Math.abs(pointer.x - this.startX);
+ let height = Math.abs(pointer.y - this.startY);
+ if (width <= 0 || height <= 0) {
+ this.canvas.remove(this.current!);
+ } else {
+ const lastXY = this.current?.getXY();
+ const lastSize = {
+ width: this.current!.width,
+ height: this.current!.height
+ }
+ this.current!.set('lastXY', lastXY);
+ this.current!.set('lastDim', lastSize);
+ FabricObjectChangeHelper.listenMove(this.current!, this.imageEditor.getHistory());
+ FabricObjectChangeHelper.listenScale(this.current!, this.imageEditor.getHistory());
+ this.imageEditor.getHistory().recordCreateAction(this.current!);
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/operator/text_operator.ts b/src/operator/text_operator.ts
new file mode 100644
index 0000000000000000000000000000000000000000..c601c259062ac614518592f45640646ae8ec86b4
--- /dev/null
+++ b/src/operator/text_operator.ts
@@ -0,0 +1,118 @@
+import { Canvas, IText } from "fabric";
+import ImageEditor from "../image_editor";
+import { DEFAULT_COLOR, ImageEditorOperator, OperatorProps, OperatorType } from "../image_editor_operator";
+import FabricObjectChangeHelper from "./move_helper";
+
+export default class TextOperator implements ImageEditorOperator, OperatorProps {
+
+ private imageEditor: ImageEditor;
+
+ private canvas: Canvas;
+
+ private start: boolean = false;
+
+ private startX: number;
+
+ private startY: number;
+
+ private allowCreate: boolean = true;
+
+ private fontSize: number = 20;
+
+ private color: string = DEFAULT_COLOR;
+
+ constructor(imageEditor: ImageEditor) {
+ this.imageEditor = imageEditor;
+ this.canvas = imageEditor.getCanvas();
+ this.startX = 0;
+ this.startY = 0;
+ }
+
+ getOperatorSize(): number {
+ return this.fontSize;
+ }
+
+ getOperatorColor(): string {
+ return this.color;
+ }
+
+ setOperatorSize(fontSize: number): void {
+ this.fontSize = fontSize;
+ }
+
+ setOperatorColor(color: string): void {
+ this.color = color;
+ }
+
+ handleMouseDownBefore(): void {
+ if (this.imageEditor.getOperatorType() != OperatorType.TEXT) {
+ return
+ }
+ if (this.canvas.getActiveObject()) {
+ this.allowCreate = false;
+ } else {
+ this.allowCreate = true;
+ }
+ }
+
+ handleMouseDown(event: any): void {
+ if (!this.allowCreate) {
+ return;
+ }
+ const canvas = this.canvas;
+ if (canvas.getActiveObject() || this.imageEditor.getOperatorType() != OperatorType.TEXT) {
+ return;
+ }
+
+ const pointer = canvas.getScenePoint(event.e);
+ this.startX = pointer.x;
+ this.startY = pointer.y;
+ this.start = true;
+ }
+
+ handleMouseUp(event: any): void {
+ if (!this.allowCreate) {
+ return;
+ }
+ if (!this.start || this.imageEditor.getOperatorType() != OperatorType.TEXT) {
+ return
+ }
+ this.start = false;
+ const canvas = this.canvas;
+ const pointer = canvas.getScenePoint(event.e);
+ const width = Math.abs(pointer.x - this.startX);
+ const height = Math.abs(pointer.y - this.startY);
+
+ if (width * width + height * height > 100) {
+ return;
+ }
+
+ const text = new IText('请输入内容', {
+ left: pointer.x,
+ top: pointer.y,
+ fontSize: this.fontSize,
+ fill: this.color,
+ lockScalingFlip: true
+ } as any);
+
+ text.setControlVisible('mt', false);
+ text.setControlVisible('mb', false);
+ text.setControlVisible('ml', false);
+ text.setControlVisible('mr', false);
+
+ canvas.add(text);
+ canvas.setActiveObject(text);
+ canvas.renderAll();
+
+ const lastXY = text.getXY();
+ const lastScale = {
+ x: text.scaleX,
+ y: text.scaleY
+ }
+ text.set('lastXY', lastXY);
+ text.set('lastScale', lastScale);
+ FabricObjectChangeHelper.listenMove(text, this.imageEditor.getHistory());
+ FabricObjectChangeHelper.listenRatioScale(text, this.imageEditor.getHistory());
+ this.imageEditor.getHistory().recordCreateAction(text);
+ }
+}
\ No newline at end of file
diff --git a/src/screenshoter.ts b/src/screenshoter.ts
new file mode 100644
index 0000000000000000000000000000000000000000..9758fa4940b0020b23e4ee48b177bea6428c28b5
--- /dev/null
+++ b/src/screenshoter.ts
@@ -0,0 +1,619 @@
+import { Point } from "fabric";
+import ElementManager, { pxielToNumber } from "./element_manager";
+import ImageEditor from "./image_editor";
+
+const DEFAULT_MOUSE_DOWN_FUNC = (_e: MouseEvent) => { };
+
+export class Screenshoter {
+
+ private mouseMoving = DEFAULT_MOUSE_DOWN_FUNC;
+
+ private mouseUp = DEFAULT_MOUSE_DOWN_FUNC;
+
+ private resizerPosX = 0;
+
+ private resizerPosY = 0;
+
+ private startX = 0;
+
+ private startY = 0;
+
+ private movingX = 0;
+
+ private movingY = 0;
+
+ private width = 0;
+
+ private height = 0;
+
+ private maxLeft = 0;
+
+ private minLeft = 0;
+
+ private maxTop = 0;
+
+ private minTop = 0;
+
+ // 正在激活的
+ private activeResizer = 'none';
+
+ private mask?: HTMLCanvasElement;
+
+ private maskLeft = 0;
+
+ private maskTop = 0;
+
+ private fabricWrapperEl?: HTMLDivElement;
+
+ private imageEditor?: ImageEditor;
+
+ private elementManager?: ElementManager;
+
+ private clipArea = { startX: 0, startY: 0, width: 0, height: 0 }
+
+ private dragger = {
+ isClipAreaInDrag: false,
+ startX: 0,
+ startY: 0,
+ width: 0,
+ height: 0,
+ // 记录鼠标一开始落在的位置
+ pointerDownX: 0,
+ pointerDownY: 0,
+
+ // 做临时变量用,拖拽结束的时候需要用到
+ currentX: 0,
+ currentY: 0,
+ }
+
+ private dragRecord: Record = {
+ northWestTop: 0,
+ northWestLeft: 0,
+ northTop: 0,
+ northLeft: 0,
+ northEastTop: 0,
+ northEastLeft: 0,
+ eastTop: 0,
+ eastLeft: 0,
+ southTop: 0,
+ southLeft: 0,
+ southWestTop: 0,
+ southWestLeft: 0,
+ westTop: 0,
+ westLeft: 0
+ }
+
+ private cursorInClipArea = false;
+
+ private screenshotResizer: {
+ northWest: HTMLDivElement,
+ north: HTMLDivElement,
+ northEast: HTMLDivElement,
+ east: HTMLDivElement,
+ southEast: HTMLDivElement,
+ south: HTMLDivElement,
+ southWest: HTMLDivElement,
+ west: HTMLDivElement
+ } | undefined;
+
+ private toolbar: HTMLDivElement | undefined;
+ private confirm: HTMLDivElement | undefined;
+ private cancel: HTMLDivElement | undefined;
+
+ private confirmFunc = () => { };
+ private cancelFunc = () => { };
+
+ private mouseDownNorthWest = DEFAULT_MOUSE_DOWN_FUNC;
+ private mouseDownNorth = DEFAULT_MOUSE_DOWN_FUNC;
+ private mouseDownNorthEast = DEFAULT_MOUSE_DOWN_FUNC;
+ private mouseDownEast = DEFAULT_MOUSE_DOWN_FUNC;
+ private mouseDownSouthEast = DEFAULT_MOUSE_DOWN_FUNC;
+ private mosueDownSouth = DEFAULT_MOUSE_DOWN_FUNC;
+ private mouseDownSouthWest = DEFAULT_MOUSE_DOWN_FUNC;
+ private mouseDownWest = DEFAULT_MOUSE_DOWN_FUNC;
+
+ private canvasMouseDownFunc = DEFAULT_MOUSE_DOWN_FUNC;
+ private canvasMouseMoveFunc = DEFAULT_MOUSE_DOWN_FUNC;
+ private canvasMouseUpFunc = DEFAULT_MOUSE_DOWN_FUNC;
+
+ init(imageEditor: ImageEditor, manager: ElementManager) {
+ this.elementManager = manager;
+ this.imageEditor = imageEditor;
+ this.fabricWrapperEl = manager.getFabricWrapper()!;
+ this.mask = manager.getScreenshotCanvas();
+ this.screenshotResizer = manager.getScreenshotResizers();
+ this.toolbar = manager.getScreenshotToolbar();
+ this.confirm = manager.getScreenshotConfirmButton();
+ this.cancel = manager.getScreenshotCancelButton();
+
+ const that = this;
+
+ const recordResizer = (name: string, style: CSSStyleDeclaration, e: MouseEvent) => {
+ that.activeResizer = name;
+ that.startX = e.pageX;
+ that.startY = e.pageY;
+ that.resizerPosX = pxielToNumber(style.left);
+ that.resizerPosY = pxielToNumber(style.top);
+ }
+
+ const resizer = that.screenshotResizer!;
+
+ that.mouseDownNorthWest = (e: MouseEvent) => { recordResizer('northwest', resizer.northWest.style, e) }
+ this.screenshotResizer.northWest.addEventListener('pointerdown', this.mouseDownNorthWest);
+
+ that.mouseDownNorth = (e: MouseEvent) => { recordResizer('north', resizer.north.style, e) }
+ this.screenshotResizer.north.addEventListener('pointerdown', this.mouseDownNorth);
+
+ that.mouseDownNorthEast = (e: MouseEvent) => { recordResizer('northeast', resizer.northEast.style, e) }
+ this.screenshotResizer.northEast.addEventListener('pointerdown', this.mouseDownNorthEast);
+
+ that.mouseDownEast = (e: MouseEvent) => { recordResizer('east', resizer.east.style, e) }
+ this.screenshotResizer.east.addEventListener('pointerdown', this.mouseDownEast);
+
+ that.mouseDownSouthEast = (e: MouseEvent) => { recordResizer('southeast', resizer.southEast.style, e) }
+ this.screenshotResizer.southEast.addEventListener('pointerdown', this.mouseDownSouthEast);
+
+ that.mosueDownSouth = (e: MouseEvent) => { recordResizer('south', resizer.south.style, e) }
+ this.screenshotResizer.south.addEventListener('pointerdown', this.mosueDownSouth);
+
+ that.mouseDownSouthWest = (e: MouseEvent) => { recordResizer('southwest', resizer.southWest.style, e) }
+ this.screenshotResizer.southWest.addEventListener('pointerdown', this.mouseDownSouthWest);
+
+ that.mouseDownWest = (e: MouseEvent) => { recordResizer('west', resizer.west.style, e) }
+ this.screenshotResizer.west.addEventListener('pointerdown', this.mouseDownWest);
+
+ this.cancel.removeEventListener('click', this.cancelFunc);
+ this.confirm.removeEventListener('click', this.confirmFunc);
+
+ this.cancelFunc = this.cancelScreenshot.bind(this);
+ this.confirmFunc = this.confirmScreenshot.bind(this);
+
+ this.cancel.addEventListener('click', this.cancelFunc);
+ this.confirm.addEventListener('click', this.confirmFunc);
+ }
+
+ handleDragArea() {
+
+ const canvas = this.mask!;
+
+ canvas.removeEventListener('mousemove', this.canvasMouseMoveFunc);
+ canvas.removeEventListener('mousedown', this.canvasMouseDownFunc);
+ document.removeEventListener('mouseup', this.canvasMouseUpFunc);
+
+ this.canvasMouseMoveFunc = (event: MouseEvent) => {
+
+ const clipArea = this.clipArea;
+
+ const xRange = [clipArea.startX, clipArea.startX + clipArea.width];
+ const yRange = [clipArea.startY, clipArea.startY + clipArea.height];
+
+ const currentX = Math.round(event.pageX - this.maskLeft);
+ const currentY = Math.round(event.pageY - this.maskTop);
+
+ const xInRange = currentX >= xRange[0] && currentX <= xRange[1];
+ const yInRange = currentY >= yRange[0] && currentY <= yRange[1];
+
+ const currentCursor = canvas.style.cursor;
+
+ this.cursorInClipArea = xInRange && yInRange;
+
+ if (xInRange && yInRange && currentCursor != 'move') {
+ canvas.style.cursor = 'move';
+ } else if ((!xInRange || !yInRange) && currentCursor != 'default' && !this.dragger.isClipAreaInDrag) {
+ canvas.style.cursor = 'default';
+ }
+
+ if (this.dragger.isClipAreaInDrag) {
+ const x = event.pageX;
+ const y = event.pageY;
+
+ let changeX = x - this.dragger.pointerDownX;
+ let changeY = y - this.dragger.pointerDownY;
+
+ if (changeX + clipArea.startX < 0) {
+ changeX = -clipArea.startX;
+ } else if (clipArea.startX + changeX + clipArea.width > this.width) {
+ changeX = this.width - clipArea.width - clipArea.startX;
+ }
+
+ if (changeY + clipArea.startY < 0) {
+ changeY = -clipArea.startY;
+ } else if (clipArea.startY + changeY + clipArea.height > this.height) {
+ changeY = this.height - clipArea.height - clipArea.startY;
+ }
+
+ this.transferClipArea(changeX, changeY);
+
+ this.adjustToolbarPosition();
+ }
+ }
+
+ this.canvasMouseDownFunc = (event: MouseEvent) => {
+ if (this.cursorInClipArea === false) {
+ return;
+ }
+
+ this.dragger.isClipAreaInDrag = true;
+ this.dragger.pointerDownX = event.pageX;
+ this.dragger.pointerDownY = event.pageY;
+
+ const resizers = this.screenshotResizer!
+ Object.entries(resizers).forEach((value) => {
+ const eleName = value[0];
+ const ele = value[1];
+
+ this.dragRecord[eleName + 'Left'] = pxielToNumber(ele.style.left);
+ this.dragRecord[eleName + 'Top'] = pxielToNumber(ele.style.top);
+ })
+
+ this.dragger.height = this.clipArea.height;
+ this.dragger.width = this.clipArea.width;
+ this.dragger.startX = this.clipArea.startX;
+ this.dragger.startY = this.clipArea.startY;
+ }
+
+ this.canvasMouseUpFunc = (_event: MouseEvent) => {
+ console.log(this.dragger.isClipAreaInDrag);
+ if (!this.dragger.isClipAreaInDrag) {
+ return;
+ }
+ this.dragger.isClipAreaInDrag = false;
+ this.clipArea.startX = this.dragger.currentX;
+ this.clipArea.startY = this.dragger.currentY;
+ }
+
+ document.addEventListener('mousemove', this.canvasMouseMoveFunc)
+ canvas.addEventListener('mousedown', this.canvasMouseDownFunc)
+ document.addEventListener('mouseup', this.canvasMouseUpFunc);
+ }
+
+ transferClipArea(changeX: number, changeY: number) {
+ const resizers = this.screenshotResizer!
+ Object.entries(resizers).forEach((value) => {
+ const eleName = value[0];
+ const left = this.dragRecord[eleName + 'Left'];
+ const top = this.dragRecord[eleName + 'Top'];
+ value[1].style.left = left + changeX + 'px';
+ value[1].style.top = top + changeY + 'px';
+ })
+
+ const startX = this.dragger.startX + changeX;
+ const startY = this.dragger.startY + changeY;
+ const width = this.dragger.width;
+ const height = this.dragger.height;
+
+ const context = this.mask!.getContext('2d')!;
+ context.clearRect(0, 0, this.width, this.height);
+ context.fillStyle = 'rgba(0,0,0,0.4)';
+ context.fillRect(0, 0, this.width, this.height);
+
+ context.clearRect(startX, startY, width, height);
+
+ this.dragger.currentX = startX;
+ this.dragger.currentY = startY;
+ }
+
+ updateClipArea() {
+
+ }
+
+ getClipAreaRect() {
+ const resizer = this.screenshotResizer!;
+ const neTop = pxielToNumber(resizer.northEast.style.top);
+ const nwTop = pxielToNumber(resizer.northWest.style.top);
+ const swTop = pxielToNumber(resizer.southWest.style.top);
+ const seTop = pxielToNumber(resizer.southEast.style.top);
+
+ const nwLeft = pxielToNumber(resizer.northWest.style.left)
+ const neLeft = pxielToNumber(resizer.northEast.style.left);
+ const swLeft = pxielToNumber(resizer.southWest.style.left);
+ const seLeft = pxielToNumber(resizer.southEast.style.left);
+
+ const maxTop = Math.max(neTop, nwTop, swTop, seTop);
+ const minTop = Math.min(neTop, nwTop, swTop, seTop);
+ const maxLeft = Math.max(nwLeft, neLeft, swLeft, seLeft);
+ const minLeft = Math.min(nwLeft, neLeft, swLeft, seLeft);
+
+ // 超出的部分,实际上是不显式的
+ const canvasLeft = Math.abs(pxielToNumber(this.fabricWrapperEl!.style.left));
+ const canvasTop = Math.abs(pxielToNumber(this.fabricWrapperEl!.style.top));
+
+ const maskLeft = Math.abs(pxielToNumber(this.mask!.style.left))
+ const maskTop = Math.abs(pxielToNumber(this.mask!.style.top))
+
+ const top = minTop - maskTop + canvasTop;
+ const left = minLeft - maskLeft + canvasLeft;
+ const width = maxLeft - minLeft;
+ const height = maxTop - minTop;
+ return { top, left, width, height }
+ }
+
+ // TODO 结束的时候要把所有的事件全部都干掉
+ async confirmScreenshot() {
+
+ const storeState = this.imageEditor!.storeCanvasState();
+
+ const { top, left, width, height } = this.getClipAreaRect();
+
+ const start = new Point(left, top);
+ const end = new Point(left + width, height + top);
+ const image = this.imageEditor!.getAreaImageInfo(start, end);
+
+ this.handleScreenshotFinished();
+
+ await this.imageEditor!.renderToCanvas(image);
+ const cropState = this.imageEditor!.storeCanvasState();
+ this.imageEditor!.getHistory().recordCropAction(storeState.wrapper, storeState.canvas, cropState.wrapper, cropState.canvas);
+ }
+
+ cancelScreenshot() {
+ this.handleScreenshotFinished();
+ }
+
+ handleScreenshotFinished() {
+ this.toolbar!.style.display = 'none';
+ const resizer = this.screenshotResizer!;
+ Object.entries(resizer).forEach(([_k, v]) => {
+ v.style.display = 'none';
+ })
+ this.activeResizer = 'none';
+ this.mask!.style.display = 'none';
+ this.elementManager!.showResizer();
+ this.elementManager?.showToolbar();
+ document.removeEventListener('pointermove', this.mouseMoving);
+ document.removeEventListener('pointerup', this.mouseUp);
+ document.removeEventListener('mouseup', this.canvasMouseUpFunc);
+ }
+
+ adjustToolbarPosition() {
+ const toolbar = this.toolbar!;
+ const resizer = this.screenshotResizer!;
+
+ if (toolbar.style.display == 'none') {
+ toolbar.style.display = 'block';
+ }
+ const neTop = pxielToNumber(resizer.northEast.style.top);
+ const nwTop = pxielToNumber(resizer.northWest.style.top);
+ const swTop = pxielToNumber(resizer.southWest.style.top);
+ const seTop = pxielToNumber(resizer.southEast.style.top);
+
+ const nwLeft = pxielToNumber(resizer.northWest.style.left)
+ const neLeft = pxielToNumber(resizer.northEast.style.left);
+ const swLeft = pxielToNumber(resizer.southWest.style.left);
+ const seLeft = pxielToNumber(resizer.southEast.style.left);
+
+ const maxTop = Math.max(neTop, nwTop, swTop, seTop);
+ const maxLeft = Math.max(nwLeft, neLeft, swLeft, seLeft);
+
+ // 64为toolbar的宽度
+ toolbar.style.left = (maxLeft - 64) + 'px';
+ // 加10为了防止工具条太高
+ toolbar.style.top = maxTop + 10 + 'px';
+ }
+
+ resizeArea() {
+ const resizer = this.screenshotResizer!;
+ const changeX = this.movingX - this.startX;
+ const changeY = this.movingY - this.startY;
+
+ let newLeft = this.resizerPosX + changeX;
+ let newTop = this.resizerPosY + changeY;
+
+ if (newLeft < this.minLeft) {
+ newLeft = this.minLeft;
+ } else if (newLeft > this.maxLeft) {
+ newLeft = this.maxLeft;
+ }
+
+ if (newTop < this.minTop) {
+ newTop = this.minTop;
+ } else if (newTop > this.maxTop) {
+ newTop = this.maxTop;
+ }
+
+
+ if (this.activeResizer == 'northwest') {
+ resizer.northWest.style.left = newLeft + 'px';
+ resizer.northWest.style.top = newTop + 'px';
+
+ resizer.west.style.left = newLeft + 'px';
+ resizer.north.style.top = newTop + 'px';
+
+ resizer.southWest.style.left = newLeft + 'px';
+ resizer.northEast.style.top = newTop + 'px';
+ } else if (this.activeResizer == 'north') {
+
+ resizer.north.style.top = newTop + 'px';
+ resizer.northEast.style.top = newTop + 'px';
+ resizer.northWest.style.top = newTop + 'px';
+
+ } else if (this.activeResizer == 'northeast') {
+ resizer.northEast.style.left = newLeft + 'px';
+ resizer.northEast.style.top = newTop + 'px';
+
+ resizer.north.style.top = newTop + 'px';
+ resizer.northWest.style.top = newTop + 'px';
+
+ resizer.east.style.left = newLeft + 'px';
+ resizer.southEast.style.left = newLeft + 'px';
+
+ } else if (this.activeResizer == 'east') {
+
+ resizer.east.style.left = newLeft + 'px';
+ resizer.northEast.style.left = newLeft + 'px';
+ resizer.southEast.style.left = newLeft + 'px';
+
+ } else if (this.activeResizer == 'southeast') {
+ resizer.southEast.style.left = newLeft + 'px';
+ resizer.southEast.style.top = newTop + 'px';
+
+ resizer.east.style.left = newLeft + 'px';
+ resizer.south.style.top = newTop + 'px';
+
+ resizer.northEast.style.left = newLeft + 'px';
+ resizer.southWest.style.top = newTop + 'px';
+ } else if (this.activeResizer == 'south') {
+
+ resizer.south.style.top = newTop + 'px';
+ resizer.southEast.style.top = newTop + 'px';
+ resizer.southWest.style.top = newTop + 'px';
+
+ } else if (this.activeResizer == 'southwest') {
+ resizer.southWest.style.left = newLeft + 'px';
+ resizer.southWest.style.top = newTop + 'px';
+
+ resizer.west.style.left = newLeft + 'px';
+ resizer.south.style.top = newTop + 'px';
+
+ resizer.northWest.style.left = newLeft + 'px';
+ resizer.southEast.style.top = newTop + 'px';
+
+ } else if (this.activeResizer == 'west') {
+ resizer.west.style.left = newLeft + 'px';
+ resizer.southWest.style.left = newLeft + 'px';
+ resizer.northWest.style.left = newLeft + 'px';
+ }
+
+ // 调整时,中点位置要重新计算
+ this.formatCenterResizer();
+
+ const canvasLeft = pxielToNumber(this.mask!.style.left);
+ const canvasTop = pxielToNumber(this.mask!.style.top);
+
+ const northWestLeft = pxielToNumber(resizer.northWest.style.left);
+ const northWestTop = pxielToNumber(resizer.northWest.style.top);
+
+ const southEastLeft = pxielToNumber(resizer.southEast.style.left);
+ const southEastTop = pxielToNumber(resizer.southEast.style.top);
+
+ const width = Math.round(southEastLeft - northWestLeft);
+ const height = Math.round(southEastTop - northWestTop);
+
+ const context = this.mask!.getContext('2d')!;
+ context.clearRect(0, 0, this.width, this.height);
+ context.fillStyle = 'rgba(0,0,0,0.4)';
+ context.fillRect(0, 0, this.width, this.height);
+
+ // 然后将中间的设置为空白的,完全学习微信
+ const startX = northWestLeft - canvasLeft;
+ const startY = northWestTop - canvasTop;
+
+ context.clearRect(startX, startY, width, height);
+ this.clipArea.startX = startX;
+ this.clipArea.startY = startY;
+ this.clipArea.width = width;
+ this.clipArea.height = height;
+ this.adjustToolbarPosition();
+ }
+
+
+ formatCenterResizer() {
+ const resizer = this.screenshotResizer!;
+ const nwLeft = pxielToNumber(resizer.northWest.style.left)
+ const nwTop = pxielToNumber(resizer.northWest.style.top);
+ const neLeft = pxielToNumber(resizer.northEast.style.left);
+ const swTop = pxielToNumber(resizer.southWest.style.top);
+
+ const verticalLeft = (nwLeft + neLeft) / 2;
+ const horizontalTop = (nwTop + swTop) / 2;
+
+ resizer.north.style.left = verticalLeft + 'px';
+ resizer.south.style.left = verticalLeft + 'px';
+
+ resizer.west.style.top = horizontalTop + 'px';
+ resizer.east.style.top = horizontalTop + 'px';
+ }
+
+ initMask(left: number, top: number, width: number, height: number) {
+ this.width = width;
+ this.height = height;
+ const mask = this.mask!;
+ mask.style.left = left + 'px';
+ mask.style.top = top + 'px';
+ mask.style.width = this.width + 'px';
+ mask.style.height = this.height + 'px';
+ mask.style.display = 'block';
+ mask.width = this.width;
+ mask.height = this.height;
+
+ this.maskLeft = left;
+ this.maskTop = top;
+
+ this.minLeft = left;
+ this.maxLeft = this.minLeft + width;
+ this.minTop = top;
+ this.maxTop = this.minTop + height;
+
+ const context = mask.getContext('2d')!;
+ context.fillStyle = 'rgba(0,0,0,0.4)';
+ context.fillRect(0, 0, this.width, this.height);
+
+ const cropLeft = Math.round(this.width * 0.2);
+ const cropTop = Math.round(this.height * 0.2);
+
+ // 然后将中间的设置为空白的,完全学习微信
+ const startX = cropLeft;
+ const startY = cropTop;
+ const cropWidth = Math.round(this.width * 0.6);
+ const cropHeight = Math.round(this.height * 0.6);
+
+ context.clearRect(startX, startY, cropWidth, cropHeight);
+
+ this.clipArea = {
+ startX, startY,
+ width: cropWidth,
+ height: cropHeight
+ }
+
+ const resizer = this.screenshotResizer!;
+ resizer.northWest.style.left = left + cropLeft + 'px';
+ resizer.northWest.style.top = top + + cropTop + 'px';
+
+ resizer.north.style.left = left + cropLeft + Math.round(cropWidth / 2) + 'px';
+ resizer.north.style.top = top + + cropTop + 'px';
+
+ resizer.northEast.style.left = left + cropLeft + cropWidth + 'px';
+ resizer.northEast.style.top = top + + cropTop + 'px';
+
+ resizer.east.style.left = left + cropLeft + cropWidth + 'px';
+ resizer.east.style.top = top + + cropTop + + Math.round(cropHeight / 2) + 'px';
+
+ resizer.southEast.style.left = left + cropLeft + cropWidth + 'px';
+ resizer.southEast.style.top = top + + cropTop + cropHeight + 'px';
+
+ resizer.south.style.left = left + cropLeft + Math.round(cropWidth / 2) + 'px';
+ resizer.south.style.top = top + + cropTop + + cropHeight + 'px';
+
+ resizer.southWest.style.left = left + cropLeft + 'px';
+ resizer.southWest.style.top = top + + cropTop + cropHeight + 'px';
+
+ resizer.west.style.left = left + cropLeft + 'px';
+ resizer.west.style.top = top + + cropTop + Math.round(cropHeight / 2) + 'px';
+ Object.entries(resizer).forEach(([_k, v]) => {
+ v.style.display = 'block';
+ })
+
+ document.removeEventListener('pointermove', this.mouseMoving);
+ document.removeEventListener('pointerup', this.mouseUp);
+
+ this.mouseMoving = (e: MouseEvent) => {
+ if (this.activeResizer != 'none') {
+ this.movingX = e.pageX;
+ this.movingY = e.pageY;
+ this.resizeArea();
+ }
+ };
+ document.addEventListener('pointermove', this.mouseMoving);
+
+ this.mouseUp = () => {
+ this.activeResizer = 'none';
+ }
+
+ document.addEventListener('pointerup', this.mouseUp);
+ this.handleDragArea();
+
+ this.elementManager!.hideResizer();
+
+ this.adjustToolbarPosition();
+ }
+}
\ No newline at end of file
diff --git a/src/shortcut_manager.ts b/src/shortcut_manager.ts
new file mode 100644
index 0000000000000000000000000000000000000000..85218ea57386966c04468196c72db59c31f2b48b
--- /dev/null
+++ b/src/shortcut_manager.ts
@@ -0,0 +1,39 @@
+import ImageEditor from "./image_editor";
+
+export class ImageEditorShortcutManager {
+
+ protected imageEditor: ImageEditor;
+
+ protected keyboardEventHandler: (event: KeyboardEvent) => void;
+
+ constructor(imageEditor: ImageEditor) {
+ this.imageEditor = imageEditor;
+ this.keyboardEventHandler = this.handleKeyboardEvent.bind(this);
+ document.addEventListener('keydown', this.keyboardEventHandler)
+ }
+
+ handleKeyboardEvent(event: KeyboardEvent) {
+ const noControlKey = !event.ctrlKey && !event.shiftKey && !event.altKey
+ const ctrlOnly = event.ctrlKey && !event.shiftKey && !event.altKey;
+ if (event.key === 'Delete' && noControlKey) {
+ this.imageEditor.removeActiveObjects();
+ } else if (ctrlOnly) {
+ console.log(this.imageEditor)
+ switch (event.key) {
+ case 'z':
+ this.imageEditor!.getHistory().undo();
+ break;
+ case 'y':
+ this.imageEditor!.getHistory().redo();
+ break;
+ }
+ }
+ }
+
+ destroy() {
+ document.removeEventListener('keydown', this.keyboardEventHandler);
+ }
+
+
+
+}
\ No newline at end of file
diff --git a/src/uitls.ts b/src/uitls.ts
new file mode 100644
index 0000000000000000000000000000000000000000..09335fcdec4df690a5c0b2f6232a3a068c7d310a
--- /dev/null
+++ b/src/uitls.ts
@@ -0,0 +1,7 @@
+export function getAbsolutePosition(element: any) {
+ const rect = element.getBoundingClientRect();
+ // 结合页面的滚动距离来计算相对于整个文档的绝对位置
+ const x = rect.left + window.scrollX;
+ const y = rect.top + window.scrollY;
+ return { x, y };
+}
\ No newline at end of file
diff --git a/tsconfig.json b/tsconfig.json
new file mode 100644
index 0000000000000000000000000000000000000000..f35d2225d55f28a76536c5c1c23b33fc49471b89
--- /dev/null
+++ b/tsconfig.json
@@ -0,0 +1,23 @@
+{
+ "compilerOptions": {
+ "target": "ES2020",
+ "useDefineForClassFields": true,
+ "module": "ESNext",
+ "lib": ["ES2020", "DOM", "DOM.Iterable"],
+ "skipLibCheck": true,
+
+ /* Bundler mode */
+ "moduleResolution": "Bundler",
+ "allowImportingTsExtensions": true,
+ "resolveJsonModule": true,
+ "isolatedModules": true,
+ "noEmit": true,
+
+ /* Linting */
+ "strict": true,
+ "noUnusedLocals": false,
+ "noUnusedParameters": true,
+ "noFallthroughCasesInSwitch": true
+ },
+ "include": ["src"]
+}
diff --git a/vite.config.ts b/vite.config.ts
new file mode 100644
index 0000000000000000000000000000000000000000..7cce0d6d88af501102e213d5b0cc364d8d07c2b0
--- /dev/null
+++ b/vite.config.ts
@@ -0,0 +1,16 @@
+import { defineConfig } from 'vite'
+import { viteStaticCopy } from 'vite-plugin-static-copy'
+
+
+export default defineConfig({
+ plugins: [
+ viteStaticCopy({
+ targets: [
+ { src: 'src/assets/*', dest: 'assets/' }, // 将 src/assets 下的文件复制到 dist/assets
+ { src: 'basic.jpg', dest: '.' }, // 将 src/assets 下的文件复制到 dist/assets
+ ]
+ })
+ ],
+ build: {
+ }
+})
diff --git a/yarn.lock b/yarn.lock
new file mode 100644
index 0000000000000000000000000000000000000000..326adb1343b5b0d5edf63ed33a4f82f5ad861215
--- /dev/null
+++ b/yarn.lock
@@ -0,0 +1,1190 @@
+# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.
+# yarn lockfile v1
+
+
+"@esbuild/linux-x64@0.21.5":
+ version "0.21.5"
+ resolved "https://registry.npmmirror.com/@esbuild/linux-x64/-/linux-x64-0.21.5.tgz"
+ integrity sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==
+
+"@mapbox/node-pre-gyp@^1.0.0":
+ version "1.0.11"
+ resolved "https://registry.npmmirror.com/@mapbox/node-pre-gyp/-/node-pre-gyp-1.0.11.tgz"
+ integrity sha512-Yhlar6v9WQgUp/He7BdgzOz8lqMQ8sU+jkCq7Wx8Myc5YFJLbEe7lgui/V7G1qB1DJykHSGwreceSaD60Y0PUQ==
+ dependencies:
+ detect-libc "^2.0.0"
+ https-proxy-agent "^5.0.0"
+ make-dir "^3.1.0"
+ node-fetch "^2.6.7"
+ nopt "^5.0.0"
+ npmlog "^5.0.1"
+ rimraf "^3.0.2"
+ semver "^7.3.5"
+ tar "^6.1.11"
+
+"@nodelib/fs.scandir@2.1.5":
+ version "2.1.5"
+ resolved "https://registry.npmmirror.com/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz"
+ integrity sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==
+ dependencies:
+ "@nodelib/fs.stat" "2.0.5"
+ run-parallel "^1.1.9"
+
+"@nodelib/fs.stat@^2.0.2", "@nodelib/fs.stat@2.0.5":
+ version "2.0.5"
+ resolved "https://registry.npmmirror.com/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz"
+ integrity sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==
+
+"@nodelib/fs.walk@^1.2.3":
+ version "1.2.8"
+ resolved "https://registry.npmmirror.com/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz"
+ integrity sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==
+ dependencies:
+ "@nodelib/fs.scandir" "2.1.5"
+ fastq "^1.6.0"
+
+"@rollup/rollup-linux-x64-gnu@4.24.0":
+ version "4.24.0"
+ resolved "https://registry.npmmirror.com/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.24.0.tgz"
+ integrity sha512-ZXFk7M72R0YYFN5q13niV0B7G8/5dcQ9JDp8keJSfr3GoZeXEoMHP/HlvqROA3OMbMdfr19IjCeNAnPUG93b6A==
+
+"@rollup/rollup-linux-x64-musl@4.24.0":
+ version "4.24.0"
+ resolved "https://registry.npmmirror.com/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.24.0.tgz"
+ integrity sha512-w1i+L7kAXZNdYl+vFvzSZy8Y1arS7vMgIy8wusXJzRrPyof5LAb02KGr1PD2EkRcl73kHulIID0M501lN+vobQ==
+
+"@tootallnate/once@2":
+ version "2.0.0"
+ resolved "https://registry.npmmirror.com/@tootallnate/once/-/once-2.0.0.tgz"
+ integrity sha512-XCuKFP5PS55gnMVu3dty8KPatLqUoy/ZYzDzAGCQ8JNFCkLXzmI7vNHCR+XpbZaMWQK/vQubr7PkYq8g470J/A==
+
+"@types/estree@1.0.6":
+ version "1.0.6"
+ resolved "https://registry.npmmirror.com/@types/estree/-/estree-1.0.6.tgz"
+ integrity sha512-AYnb1nQyY49te+VRAVgmzfcgjYS91mY5P0TKUDCLEM+gNnA+3T6rWITXRLYCpahpqSQbN5cE+gHpnPyXjHWxcw==
+
+"@types/fs-extra@^8.0.1":
+ version "8.1.5"
+ resolved "https://registry.npmmirror.com/@types/fs-extra/-/fs-extra-8.1.5.tgz"
+ integrity sha512-0dzKcwO+S8s2kuF5Z9oUWatQJj5Uq/iqphEtE3GQJVRRYm/tD1LglU2UnXi2A8jLq5umkGouOXOR9y0n613ZwQ==
+ dependencies:
+ "@types/node" "*"
+
+"@types/glob@^7.1.1":
+ version "7.2.0"
+ resolved "https://registry.npmmirror.com/@types/glob/-/glob-7.2.0.tgz"
+ integrity sha512-ZUxbzKl0IfJILTS6t7ip5fQQM/J3TJYubDm3nMbgubNNYS62eXeUpoLUC8/7fJNiFYHTrGPQn7hspDUzIHX3UA==
+ dependencies:
+ "@types/minimatch" "*"
+ "@types/node" "*"
+
+"@types/minimatch@*":
+ version "5.1.2"
+ resolved "https://registry.npmmirror.com/@types/minimatch/-/minimatch-5.1.2.tgz"
+ integrity sha512-K0VQKziLUWkVKiRVrx4a40iPaxTUefQmjtkQofBkYRcoaaL/8rhwDWww9qWbrgicNOgnpIsMxyNIUM4+n6dUIA==
+
+"@types/node@*":
+ version "22.7.8"
+ resolved "https://registry.npmmirror.com/@types/node/-/node-22.7.8.tgz"
+ integrity sha512-a922jJy31vqR5sk+kAdIENJjHblqcZ4RmERviFsER4WJcEONqxKcjNOlk0q7OUfrF5sddT+vng070cdfMlrPLg==
+ dependencies:
+ undici-types "~6.19.2"
+
+abab@^2.0.6:
+ version "2.0.6"
+ resolved "https://registry.npmmirror.com/abab/-/abab-2.0.6.tgz"
+ integrity sha512-j2afSsaIENvHZN2B8GOpF566vZ5WVk5opAiMTvWgaQT8DkbOqsTfvNAvHoRGU2zzP8cPoqys+xHTRDWW8L+/BA==
+
+abbrev@1:
+ version "1.1.1"
+ resolved "https://registry.npmmirror.com/abbrev/-/abbrev-1.1.1.tgz"
+ integrity sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==
+
+acorn-globals@^7.0.0:
+ version "7.0.1"
+ resolved "https://registry.npmmirror.com/acorn-globals/-/acorn-globals-7.0.1.tgz"
+ integrity sha512-umOSDSDrfHbTNPuNpC2NSnnA3LUrqpevPb4T9jRx4MagXNS0rs+gwiTcAvqCRmsD6utzsrzNt+ebm00SNWiC3Q==
+ dependencies:
+ acorn "^8.1.0"
+ acorn-walk "^8.0.2"
+
+acorn-walk@^8.0.2:
+ version "8.3.4"
+ resolved "https://registry.npmmirror.com/acorn-walk/-/acorn-walk-8.3.4.tgz"
+ integrity sha512-ueEepnujpqee2o5aIYnvHU6C0A42MNdsIDeqy5BydrkuC5R1ZuUFnm27EeFJGoEHJQgn3uleRvmTXaJgfXbt4g==
+ dependencies:
+ acorn "^8.11.0"
+
+acorn@^8.1.0, acorn@^8.11.0, acorn@^8.8.1:
+ version "8.12.1"
+ resolved "https://registry.npmmirror.com/acorn/-/acorn-8.12.1.tgz"
+ integrity sha512-tcpGyI9zbizT9JbV6oYE477V6mTlXvvi0T0G3SNIYE2apm/G5huBa1+K89VGeovbg+jycCrfhl3ADxErOuO6Jg==
+
+agent-base@6:
+ version "6.0.2"
+ resolved "https://registry.npmmirror.com/agent-base/-/agent-base-6.0.2.tgz"
+ integrity sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==
+ dependencies:
+ debug "4"
+
+ansi-regex@^5.0.1:
+ version "5.0.1"
+ resolved "https://registry.npmmirror.com/ansi-regex/-/ansi-regex-5.0.1.tgz"
+ integrity sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==
+
+anymatch@~3.1.2:
+ version "3.1.3"
+ resolved "https://registry.npmmirror.com/anymatch/-/anymatch-3.1.3.tgz"
+ integrity sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==
+ dependencies:
+ normalize-path "^3.0.0"
+ picomatch "^2.0.4"
+
+"aproba@^1.0.3 || ^2.0.0":
+ version "2.0.0"
+ resolved "https://registry.npmmirror.com/aproba/-/aproba-2.0.0.tgz"
+ integrity sha512-lYe4Gx7QT+MKGbDsA+Z+he/Wtef0BiwDOlK/XkBrdfsh9J/jPPXbX0tE9x9cl27Tmu5gg3QUbUrQYa/y+KOHPQ==
+
+are-we-there-yet@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.npmmirror.com/are-we-there-yet/-/are-we-there-yet-2.0.0.tgz"
+ integrity sha512-Ci/qENmwHnsYo9xKIcUJN5LeDKdJ6R1Z1j9V/J5wyq8nh/mYPEpIKJbBZXtZjG04HiK7zV/p6Vs9952MrMeUIw==
+ dependencies:
+ delegates "^1.0.0"
+ readable-stream "^3.6.0"
+
+array-union@^2.1.0:
+ version "2.1.0"
+ resolved "https://registry.npmmirror.com/array-union/-/array-union-2.1.0.tgz"
+ integrity sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==
+
+asynckit@^0.4.0:
+ version "0.4.0"
+ resolved "https://registry.npmmirror.com/asynckit/-/asynckit-0.4.0.tgz"
+ integrity sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==
+
+balanced-match@^1.0.0:
+ version "1.0.2"
+ resolved "https://registry.npmmirror.com/balanced-match/-/balanced-match-1.0.2.tgz"
+ integrity sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==
+
+binary-extensions@^2.0.0:
+ version "2.3.0"
+ resolved "https://registry.npmmirror.com/binary-extensions/-/binary-extensions-2.3.0.tgz"
+ integrity sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==
+
+brace-expansion@^1.1.7:
+ version "1.1.11"
+ resolved "https://registry.npmmirror.com/brace-expansion/-/brace-expansion-1.1.11.tgz"
+ integrity sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==
+ dependencies:
+ balanced-match "^1.0.0"
+ concat-map "0.0.1"
+
+braces@^3.0.3, braces@~3.0.2:
+ version "3.0.3"
+ resolved "https://registry.npmmirror.com/braces/-/braces-3.0.3.tgz"
+ integrity sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==
+ dependencies:
+ fill-range "^7.1.1"
+
+canvas@^2.11.2:
+ version "2.11.2"
+ resolved "https://registry.npmmirror.com/canvas/-/canvas-2.11.2.tgz"
+ integrity sha512-ItanGBMrmRV7Py2Z+Xhs7cT+FNt5K0vPL4p9EZ/UX/Mu7hFbkxSjKF2KVtPwX7UYWp7dRKnrTvReflgrItJbdw==
+ dependencies:
+ "@mapbox/node-pre-gyp" "^1.0.0"
+ nan "^2.17.0"
+ simple-get "^3.0.3"
+
+chokidar@^3.5.3:
+ version "3.6.0"
+ resolved "https://registry.npmmirror.com/chokidar/-/chokidar-3.6.0.tgz"
+ integrity sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==
+ dependencies:
+ anymatch "~3.1.2"
+ braces "~3.0.2"
+ glob-parent "~5.1.2"
+ is-binary-path "~2.1.0"
+ is-glob "~4.0.1"
+ normalize-path "~3.0.0"
+ readdirp "~3.6.0"
+ optionalDependencies:
+ fsevents "~2.3.2"
+
+chownr@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.npmmirror.com/chownr/-/chownr-2.0.0.tgz"
+ integrity sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ==
+
+color-support@^1.1.2:
+ version "1.1.3"
+ resolved "https://registry.npmmirror.com/color-support/-/color-support-1.1.3.tgz"
+ integrity sha512-qiBjkpbMLO/HL68y+lh4q0/O1MZFj2RX6X/KmMa3+gJD3z+WwI1ZzDHysvqHGS3mP6mznPckpXmw1nI9cJjyRg==
+
+colorette@^1.1.0:
+ version "1.4.0"
+ resolved "https://registry.npmmirror.com/colorette/-/colorette-1.4.0.tgz"
+ integrity sha512-Y2oEozpomLn7Q3HFP7dpww7AtMJplbM9lGZP6RDfHqmbeRjiwRg4n6VM6j4KLmRke85uWEI7JqF17f3pqdRA0g==
+
+combined-stream@^1.0.8:
+ version "1.0.8"
+ resolved "https://registry.npmmirror.com/combined-stream/-/combined-stream-1.0.8.tgz"
+ integrity sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==
+ dependencies:
+ delayed-stream "~1.0.0"
+
+concat-map@0.0.1:
+ version "0.0.1"
+ resolved "https://registry.npmmirror.com/concat-map/-/concat-map-0.0.1.tgz"
+ integrity sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==
+
+console-control-strings@^1.0.0, console-control-strings@^1.1.0:
+ version "1.1.0"
+ resolved "https://registry.npmmirror.com/console-control-strings/-/console-control-strings-1.1.0.tgz"
+ integrity sha512-ty/fTekppD2fIwRvnZAVdeOiGd1c7YXEixbgJTNzqcxJWKQnjJ/V1bNEEE6hygpM3WjwHFUVK6HTjWSzV4a8sQ==
+
+cssom@^0.5.0:
+ version "0.5.0"
+ resolved "https://registry.npmmirror.com/cssom/-/cssom-0.5.0.tgz"
+ integrity sha512-iKuQcq+NdHqlAcwUY0o/HL69XQrUaQdMjmStJ8JFmUaiiQErlhrmuigkg/CU4E2J0IyUKUrMAgl36TvN67MqTw==
+
+cssom@~0.3.6:
+ version "0.3.8"
+ resolved "https://registry.npmmirror.com/cssom/-/cssom-0.3.8.tgz"
+ integrity sha512-b0tGHbfegbhPJpxpiBPU2sCkigAqtM9O121le6bbOlgyV+NyGyCmVfJ6QW9eRjz8CpNfWEOYBIMIGRYkLwsIYg==
+
+cssstyle@^2.3.0:
+ version "2.3.0"
+ resolved "https://registry.npmmirror.com/cssstyle/-/cssstyle-2.3.0.tgz"
+ integrity sha512-AZL67abkUzIuvcHqk7c09cezpGNcxUxU4Ioi/05xHk4DQeTkWmGYftIE6ctU6AEt+Gn4n1lDStOtj7FKycP71A==
+ dependencies:
+ cssom "~0.3.6"
+
+data-urls@^3.0.2:
+ version "3.0.2"
+ resolved "https://registry.npmmirror.com/data-urls/-/data-urls-3.0.2.tgz"
+ integrity sha512-Jy/tj3ldjZJo63sVAvg6LHt2mHvl4V6AgRAmNDtLdm7faqtsx+aJG42rsyCo9JCoRVKwPFzKlIPx3DIibwSIaQ==
+ dependencies:
+ abab "^2.0.6"
+ whatwg-mimetype "^3.0.0"
+ whatwg-url "^11.0.0"
+
+debug@4:
+ version "4.3.7"
+ resolved "https://registry.npmmirror.com/debug/-/debug-4.3.7.tgz"
+ integrity sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==
+ dependencies:
+ ms "^2.1.3"
+
+decimal.js@^10.4.2:
+ version "10.4.3"
+ resolved "https://registry.npmmirror.com/decimal.js/-/decimal.js-10.4.3.tgz"
+ integrity sha512-VBBaLc1MgL5XpzgIP7ny5Z6Nx3UrRkIViUkPUdtl9aya5amy3De1gsUUSB1g3+3sExYNjCAsAznmukyxCb1GRA==
+
+decompress-response@^4.2.0:
+ version "4.2.1"
+ resolved "https://registry.npmmirror.com/decompress-response/-/decompress-response-4.2.1.tgz"
+ integrity sha512-jOSne2qbyE+/r8G1VU+G/82LBs2Fs4LAsTiLSHOCOMZQl2OKZ6i8i4IyHemTe+/yIXOtTcRQMzPcgyhoFlqPkw==
+ dependencies:
+ mimic-response "^2.0.0"
+
+delayed-stream@~1.0.0:
+ version "1.0.0"
+ resolved "https://registry.npmmirror.com/delayed-stream/-/delayed-stream-1.0.0.tgz"
+ integrity sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==
+
+delegates@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.npmmirror.com/delegates/-/delegates-1.0.0.tgz"
+ integrity sha512-bd2L678uiWATM6m5Z1VzNCErI3jiGzt6HGY8OVICs40JQq/HALfbyNJmp0UDakEY4pMMaN0Ly5om/B1VI/+xfQ==
+
+detect-libc@^2.0.0:
+ version "2.0.3"
+ resolved "https://registry.npmmirror.com/detect-libc/-/detect-libc-2.0.3.tgz"
+ integrity sha512-bwy0MGW55bG41VqxxypOsdSdGqLwXPI/focwgTYCFMbdUiBAxLg9CFzG08sz2aqzknwiX7Hkl0bQENjg8iLByw==
+
+dir-glob@^3.0.1:
+ version "3.0.1"
+ resolved "https://registry.npmmirror.com/dir-glob/-/dir-glob-3.0.1.tgz"
+ integrity sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==
+ dependencies:
+ path-type "^4.0.0"
+
+domexception@^4.0.0:
+ version "4.0.0"
+ resolved "https://registry.npmmirror.com/domexception/-/domexception-4.0.0.tgz"
+ integrity sha512-A2is4PLG+eeSfoTMA95/s4pvAoSo2mKtiM5jlHkAVewmiO8ISFTFKZjH7UAM1Atli/OT/7JHOrJRJiMKUZKYBw==
+ dependencies:
+ webidl-conversions "^7.0.0"
+
+emoji-regex@^8.0.0:
+ version "8.0.0"
+ resolved "https://registry.npmmirror.com/emoji-regex/-/emoji-regex-8.0.0.tgz"
+ integrity sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==
+
+entities@^4.4.0:
+ version "4.5.0"
+ resolved "https://registry.npmmirror.com/entities/-/entities-4.5.0.tgz"
+ integrity sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==
+
+esbuild@^0.21.3:
+ version "0.21.5"
+ resolved "https://registry.npmmirror.com/esbuild/-/esbuild-0.21.5.tgz"
+ integrity sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==
+ optionalDependencies:
+ "@esbuild/aix-ppc64" "0.21.5"
+ "@esbuild/android-arm" "0.21.5"
+ "@esbuild/android-arm64" "0.21.5"
+ "@esbuild/android-x64" "0.21.5"
+ "@esbuild/darwin-arm64" "0.21.5"
+ "@esbuild/darwin-x64" "0.21.5"
+ "@esbuild/freebsd-arm64" "0.21.5"
+ "@esbuild/freebsd-x64" "0.21.5"
+ "@esbuild/linux-arm" "0.21.5"
+ "@esbuild/linux-arm64" "0.21.5"
+ "@esbuild/linux-ia32" "0.21.5"
+ "@esbuild/linux-loong64" "0.21.5"
+ "@esbuild/linux-mips64el" "0.21.5"
+ "@esbuild/linux-ppc64" "0.21.5"
+ "@esbuild/linux-riscv64" "0.21.5"
+ "@esbuild/linux-s390x" "0.21.5"
+ "@esbuild/linux-x64" "0.21.5"
+ "@esbuild/netbsd-x64" "0.21.5"
+ "@esbuild/openbsd-x64" "0.21.5"
+ "@esbuild/sunos-x64" "0.21.5"
+ "@esbuild/win32-arm64" "0.21.5"
+ "@esbuild/win32-ia32" "0.21.5"
+ "@esbuild/win32-x64" "0.21.5"
+
+escodegen@^2.0.0:
+ version "2.1.0"
+ resolved "https://registry.npmmirror.com/escodegen/-/escodegen-2.1.0.tgz"
+ integrity sha512-2NlIDTwUWJN0mRPQOdtQBzbUHvdGY2P1VXSyU83Q3xKxM7WHX2Ql8dKq782Q9TgQUNOLEzEYu9bzLNj1q88I5w==
+ dependencies:
+ esprima "^4.0.1"
+ estraverse "^5.2.0"
+ esutils "^2.0.2"
+ optionalDependencies:
+ source-map "~0.6.1"
+
+esprima@^4.0.1:
+ version "4.0.1"
+ resolved "https://registry.npmmirror.com/esprima/-/esprima-4.0.1.tgz"
+ integrity sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==
+
+estraverse@^5.2.0:
+ version "5.3.0"
+ resolved "https://registry.npmmirror.com/estraverse/-/estraverse-5.3.0.tgz"
+ integrity sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==
+
+esutils@^2.0.2:
+ version "2.0.3"
+ resolved "https://registry.npmmirror.com/esutils/-/esutils-2.0.3.tgz"
+ integrity sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==
+
+fabric@^6.4.3:
+ version "6.4.3"
+ resolved "https://registry.npmmirror.com/fabric/-/fabric-6.4.3.tgz"
+ integrity sha512-z/bJna3kWOBv+wmvVK4XxUQgCXLGb//VaSr5xPFIP708obH7472uuVsWbXam+xq+y21bLBtr4OHO1HuJyYi4FQ==
+ optionalDependencies:
+ canvas "^2.11.2"
+ jsdom "^20.0.1"
+
+fast-glob@^3.0.3, fast-glob@^3.2.11, fast-glob@^3.2.7:
+ version "3.3.2"
+ resolved "https://registry.npmmirror.com/fast-glob/-/fast-glob-3.3.2.tgz"
+ integrity sha512-oX2ruAFQwf/Orj8m737Y5adxDQO0LAB7/S5MnxCdTNDd4p6BsyIVsv9JQsATbTSq8KHRpLwIHbVlUNatxd+1Ow==
+ dependencies:
+ "@nodelib/fs.stat" "^2.0.2"
+ "@nodelib/fs.walk" "^1.2.3"
+ glob-parent "^5.1.2"
+ merge2 "^1.3.0"
+ micromatch "^4.0.4"
+
+fastq@^1.6.0:
+ version "1.17.1"
+ resolved "https://registry.npmmirror.com/fastq/-/fastq-1.17.1.tgz"
+ integrity sha512-sRVD3lWVIXWg6By68ZN7vho9a1pQcN/WBFaAAsDDFzlJjvoGx0P8z7V1t72grFJfJhu3YPZBuu25f7Kaw2jN1w==
+ dependencies:
+ reusify "^1.0.4"
+
+fill-range@^7.1.1:
+ version "7.1.1"
+ resolved "https://registry.npmmirror.com/fill-range/-/fill-range-7.1.1.tgz"
+ integrity sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==
+ dependencies:
+ to-regex-range "^5.0.1"
+
+form-data@^4.0.0:
+ version "4.0.1"
+ resolved "https://registry.npmmirror.com/form-data/-/form-data-4.0.1.tgz"
+ integrity sha512-tzN8e4TX8+kkxGPK8D5u0FNmjPUjw3lwC9lSLxxoB/+GtsJG91CO8bSWy73APlgAZzZbXEYZJuxjkHH2w+Ezhw==
+ dependencies:
+ asynckit "^0.4.0"
+ combined-stream "^1.0.8"
+ mime-types "^2.1.12"
+
+fs-extra@^11.1.0:
+ version "11.2.0"
+ resolved "https://registry.npmmirror.com/fs-extra/-/fs-extra-11.2.0.tgz"
+ integrity sha512-PmDi3uwK5nFuXh7XDTlVnS17xJS7vW36is2+w3xcv8SVxiB4NyATf4ctkVY5bkSjX0Y4nbvZCq1/EjtEyr9ktw==
+ dependencies:
+ graceful-fs "^4.2.0"
+ jsonfile "^6.0.1"
+ universalify "^2.0.0"
+
+fs-extra@^8.1.0:
+ version "8.1.0"
+ resolved "https://registry.npmmirror.com/fs-extra/-/fs-extra-8.1.0.tgz"
+ integrity sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g==
+ dependencies:
+ graceful-fs "^4.2.0"
+ jsonfile "^4.0.0"
+ universalify "^0.1.0"
+
+fs-minipass@^2.0.0:
+ version "2.1.0"
+ resolved "https://registry.npmmirror.com/fs-minipass/-/fs-minipass-2.1.0.tgz"
+ integrity sha512-V/JgOLFCS+R6Vcq0slCuaeWEdNC3ouDlJMNIsacH2VtALiu9mV4LPrHc5cDl8k5aw6J8jwgWWpiTo5RYhmIzvg==
+ dependencies:
+ minipass "^3.0.0"
+
+fs.realpath@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.npmmirror.com/fs.realpath/-/fs.realpath-1.0.0.tgz"
+ integrity sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==
+
+gauge@^3.0.0:
+ version "3.0.2"
+ resolved "https://registry.npmmirror.com/gauge/-/gauge-3.0.2.tgz"
+ integrity sha512-+5J6MS/5XksCuXq++uFRsnUd7Ovu1XenbeuIuNRJxYWjgQbPuFhT14lAvsWfqfAmnwluf1OwMjz39HjfLPci0Q==
+ dependencies:
+ aproba "^1.0.3 || ^2.0.0"
+ color-support "^1.1.2"
+ console-control-strings "^1.0.0"
+ has-unicode "^2.0.1"
+ object-assign "^4.1.1"
+ signal-exit "^3.0.0"
+ string-width "^4.2.3"
+ strip-ansi "^6.0.1"
+ wide-align "^1.1.2"
+
+glob-parent@^5.1.2, glob-parent@~5.1.2:
+ version "5.1.2"
+ resolved "https://registry.npmmirror.com/glob-parent/-/glob-parent-5.1.2.tgz"
+ integrity sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==
+ dependencies:
+ is-glob "^4.0.1"
+
+glob@^7.1.3:
+ version "7.2.3"
+ resolved "https://registry.npmmirror.com/glob/-/glob-7.2.3.tgz"
+ integrity sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==
+ dependencies:
+ fs.realpath "^1.0.0"
+ inflight "^1.0.4"
+ inherits "2"
+ minimatch "^3.1.1"
+ once "^1.3.0"
+ path-is-absolute "^1.0.0"
+
+globby@10.0.1:
+ version "10.0.1"
+ resolved "https://registry.npmmirror.com/globby/-/globby-10.0.1.tgz"
+ integrity sha512-sSs4inE1FB2YQiymcmTv6NWENryABjUNPeWhOvmn4SjtKybglsyPZxFB3U1/+L1bYi0rNZDqCLlHyLYDl1Pq5A==
+ dependencies:
+ "@types/glob" "^7.1.1"
+ array-union "^2.1.0"
+ dir-glob "^3.0.1"
+ fast-glob "^3.0.3"
+ glob "^7.1.3"
+ ignore "^5.1.1"
+ merge2 "^1.2.3"
+ slash "^3.0.0"
+
+graceful-fs@^4.1.6, graceful-fs@^4.2.0:
+ version "4.2.11"
+ resolved "https://registry.npmmirror.com/graceful-fs/-/graceful-fs-4.2.11.tgz"
+ integrity sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==
+
+has-unicode@^2.0.1:
+ version "2.0.1"
+ resolved "https://registry.npmmirror.com/has-unicode/-/has-unicode-2.0.1.tgz"
+ integrity sha512-8Rf9Y83NBReMnx0gFzA8JImQACstCYWUplepDa9xprwwtmgEZUF0h/i5xSA625zB/I37EtrswSST6OXxwaaIJQ==
+
+html-encoding-sniffer@^3.0.0:
+ version "3.0.0"
+ resolved "https://registry.npmmirror.com/html-encoding-sniffer/-/html-encoding-sniffer-3.0.0.tgz"
+ integrity sha512-oWv4T4yJ52iKrufjnyZPkrN0CH3QnrUqdB6In1g5Fe1mia8GmF36gnfNySxoZtxD5+NmYw1EElVXiBk93UeskA==
+ dependencies:
+ whatwg-encoding "^2.0.0"
+
+http-proxy-agent@^5.0.0:
+ version "5.0.0"
+ resolved "https://registry.npmmirror.com/http-proxy-agent/-/http-proxy-agent-5.0.0.tgz"
+ integrity sha512-n2hY8YdoRE1i7r6M0w9DIw5GgZN0G25P8zLCRQ8rjXtTU3vsNFBI/vWK/UIeE6g5MUUz6avwAPXmL6Fy9D/90w==
+ dependencies:
+ "@tootallnate/once" "2"
+ agent-base "6"
+ debug "4"
+
+https-proxy-agent@^5.0.0, https-proxy-agent@^5.0.1:
+ version "5.0.1"
+ resolved "https://registry.npmmirror.com/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz"
+ integrity sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==
+ dependencies:
+ agent-base "6"
+ debug "4"
+
+iconv-lite@0.6.3:
+ version "0.6.3"
+ resolved "https://registry.npmmirror.com/iconv-lite/-/iconv-lite-0.6.3.tgz"
+ integrity sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==
+ dependencies:
+ safer-buffer ">= 2.1.2 < 3.0.0"
+
+ignore@^5.1.1:
+ version "5.3.2"
+ resolved "https://registry.npmmirror.com/ignore/-/ignore-5.3.2.tgz"
+ integrity sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==
+
+inflight@^1.0.4:
+ version "1.0.6"
+ resolved "https://registry.npmmirror.com/inflight/-/inflight-1.0.6.tgz"
+ integrity sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==
+ dependencies:
+ once "^1.3.0"
+ wrappy "1"
+
+inherits@^2.0.3, inherits@2:
+ version "2.0.4"
+ resolved "https://registry.npmmirror.com/inherits/-/inherits-2.0.4.tgz"
+ integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==
+
+is-binary-path@~2.1.0:
+ version "2.1.0"
+ resolved "https://registry.npmmirror.com/is-binary-path/-/is-binary-path-2.1.0.tgz"
+ integrity sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==
+ dependencies:
+ binary-extensions "^2.0.0"
+
+is-extglob@^2.1.1:
+ version "2.1.1"
+ resolved "https://registry.npmmirror.com/is-extglob/-/is-extglob-2.1.1.tgz"
+ integrity sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==
+
+is-fullwidth-code-point@^3.0.0:
+ version "3.0.0"
+ resolved "https://registry.npmmirror.com/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz"
+ integrity sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==
+
+is-glob@^4.0.1, is-glob@~4.0.1:
+ version "4.0.3"
+ resolved "https://registry.npmmirror.com/is-glob/-/is-glob-4.0.3.tgz"
+ integrity sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==
+ dependencies:
+ is-extglob "^2.1.1"
+
+is-number@^7.0.0:
+ version "7.0.0"
+ resolved "https://registry.npmmirror.com/is-number/-/is-number-7.0.0.tgz"
+ integrity sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==
+
+is-plain-object@^3.0.0:
+ version "3.0.1"
+ resolved "https://registry.npmmirror.com/is-plain-object/-/is-plain-object-3.0.1.tgz"
+ integrity sha512-Xnpx182SBMrr/aBik8y+GuR4U1L9FqMSojwDQwPMmxyC6bvEqly9UBCxhauBF5vNh2gwWJNX6oDV7O+OM4z34g==
+
+is-potential-custom-element-name@^1.0.1:
+ version "1.0.1"
+ resolved "https://registry.npmmirror.com/is-potential-custom-element-name/-/is-potential-custom-element-name-1.0.1.tgz"
+ integrity sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==
+
+jsdom@^20.0.1:
+ version "20.0.3"
+ resolved "https://registry.npmmirror.com/jsdom/-/jsdom-20.0.3.tgz"
+ integrity sha512-SYhBvTh89tTfCD/CRdSOm13mOBa42iTaTyfyEWBdKcGdPxPtLFBXuHR8XHb33YNYaP+lLbmSvBTsnoesCNJEsQ==
+ dependencies:
+ abab "^2.0.6"
+ acorn "^8.8.1"
+ acorn-globals "^7.0.0"
+ cssom "^0.5.0"
+ cssstyle "^2.3.0"
+ data-urls "^3.0.2"
+ decimal.js "^10.4.2"
+ domexception "^4.0.0"
+ escodegen "^2.0.0"
+ form-data "^4.0.0"
+ html-encoding-sniffer "^3.0.0"
+ http-proxy-agent "^5.0.0"
+ https-proxy-agent "^5.0.1"
+ is-potential-custom-element-name "^1.0.1"
+ nwsapi "^2.2.2"
+ parse5 "^7.1.1"
+ saxes "^6.0.0"
+ symbol-tree "^3.2.4"
+ tough-cookie "^4.1.2"
+ w3c-xmlserializer "^4.0.0"
+ webidl-conversions "^7.0.0"
+ whatwg-encoding "^2.0.0"
+ whatwg-mimetype "^3.0.0"
+ whatwg-url "^11.0.0"
+ ws "^8.11.0"
+ xml-name-validator "^4.0.0"
+
+jsonfile@^4.0.0:
+ version "4.0.0"
+ resolved "https://registry.npmmirror.com/jsonfile/-/jsonfile-4.0.0.tgz"
+ integrity sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg==
+ optionalDependencies:
+ graceful-fs "^4.1.6"
+
+jsonfile@^6.0.1:
+ version "6.1.0"
+ resolved "https://registry.npmmirror.com/jsonfile/-/jsonfile-6.1.0.tgz"
+ integrity sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==
+ dependencies:
+ universalify "^2.0.0"
+ optionalDependencies:
+ graceful-fs "^4.1.6"
+
+make-dir@^3.1.0:
+ version "3.1.0"
+ resolved "https://registry.npmmirror.com/make-dir/-/make-dir-3.1.0.tgz"
+ integrity sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==
+ dependencies:
+ semver "^6.0.0"
+
+merge2@^1.2.3, merge2@^1.3.0:
+ version "1.4.1"
+ resolved "https://registry.npmmirror.com/merge2/-/merge2-1.4.1.tgz"
+ integrity sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==
+
+micromatch@^4.0.4:
+ version "4.0.8"
+ resolved "https://registry.npmmirror.com/micromatch/-/micromatch-4.0.8.tgz"
+ integrity sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==
+ dependencies:
+ braces "^3.0.3"
+ picomatch "^2.3.1"
+
+mime-db@1.52.0:
+ version "1.52.0"
+ resolved "https://registry.npmmirror.com/mime-db/-/mime-db-1.52.0.tgz"
+ integrity sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==
+
+mime-types@^2.1.12:
+ version "2.1.35"
+ resolved "https://registry.npmmirror.com/mime-types/-/mime-types-2.1.35.tgz"
+ integrity sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==
+ dependencies:
+ mime-db "1.52.0"
+
+mimic-response@^2.0.0:
+ version "2.1.0"
+ resolved "https://registry.npmmirror.com/mimic-response/-/mimic-response-2.1.0.tgz"
+ integrity sha512-wXqjST+SLt7R009ySCglWBCFpjUygmCIfD790/kVbiGmUgfYGuB14PiTd5DwVxSV4NcYHjzMkoj5LjQZwTQLEA==
+
+minimatch@^3.1.1:
+ version "3.1.2"
+ resolved "https://registry.npmmirror.com/minimatch/-/minimatch-3.1.2.tgz"
+ integrity sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==
+ dependencies:
+ brace-expansion "^1.1.7"
+
+minipass@^3.0.0:
+ version "3.3.6"
+ resolved "https://registry.npmmirror.com/minipass/-/minipass-3.3.6.tgz"
+ integrity sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==
+ dependencies:
+ yallist "^4.0.0"
+
+minipass@^5.0.0:
+ version "5.0.0"
+ resolved "https://registry.npmmirror.com/minipass/-/minipass-5.0.0.tgz"
+ integrity sha512-3FnjYuehv9k6ovOEbyOswadCDPX1piCfhV8ncmYtHOjuPwylVWsghTLo7rabjC3Rx5xD4HDx8Wm1xnMF7S5qFQ==
+
+minizlib@^2.1.1:
+ version "2.1.2"
+ resolved "https://registry.npmmirror.com/minizlib/-/minizlib-2.1.2.tgz"
+ integrity sha512-bAxsR8BVfj60DWXHE3u30oHzfl4G7khkSuPW+qvpd7jFRHm7dLxOjUk1EHACJ/hxLY8phGJ0YhYHZo7jil7Qdg==
+ dependencies:
+ minipass "^3.0.0"
+ yallist "^4.0.0"
+
+mkdirp@^1.0.3:
+ version "1.0.4"
+ resolved "https://registry.npmmirror.com/mkdirp/-/mkdirp-1.0.4.tgz"
+ integrity sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==
+
+ms@^2.1.3:
+ version "2.1.3"
+ resolved "https://registry.npmmirror.com/ms/-/ms-2.1.3.tgz"
+ integrity sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==
+
+nan@^2.17.0:
+ version "2.22.0"
+ resolved "https://registry.npmmirror.com/nan/-/nan-2.22.0.tgz"
+ integrity sha512-nbajikzWTMwsW+eSsNm3QwlOs7het9gGJU5dDZzRTQGk03vyBOauxgI4VakDzE0PtsGTmXPsXTbbjVhRwR5mpw==
+
+nanoid@^3.3.7:
+ version "3.3.7"
+ resolved "https://registry.npmmirror.com/nanoid/-/nanoid-3.3.7.tgz"
+ integrity sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g==
+
+node-fetch@^2.6.7:
+ version "2.7.0"
+ resolved "https://registry.npmmirror.com/node-fetch/-/node-fetch-2.7.0.tgz"
+ integrity sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==
+ dependencies:
+ whatwg-url "^5.0.0"
+
+nopt@^5.0.0:
+ version "5.0.0"
+ resolved "https://registry.npmmirror.com/nopt/-/nopt-5.0.0.tgz"
+ integrity sha512-Tbj67rffqceeLpcRXrT7vKAN8CwfPeIBgM7E6iBkmKLV7bEMwpGgYLGv0jACUsECaa/vuxP0IjEont6umdMgtQ==
+ dependencies:
+ abbrev "1"
+
+normalize-path@^3.0.0, normalize-path@~3.0.0:
+ version "3.0.0"
+ resolved "https://registry.npmmirror.com/normalize-path/-/normalize-path-3.0.0.tgz"
+ integrity sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==
+
+npmlog@^5.0.1:
+ version "5.0.1"
+ resolved "https://registry.npmmirror.com/npmlog/-/npmlog-5.0.1.tgz"
+ integrity sha512-AqZtDUWOMKs1G/8lwylVjrdYgqA4d9nu8hc+0gzRxlDb1I10+FHBGMXs6aiQHFdCUUlqH99MUMuLfzWDNDtfxw==
+ dependencies:
+ are-we-there-yet "^2.0.0"
+ console-control-strings "^1.1.0"
+ gauge "^3.0.0"
+ set-blocking "^2.0.0"
+
+nwsapi@^2.2.2:
+ version "2.2.13"
+ resolved "https://registry.npmmirror.com/nwsapi/-/nwsapi-2.2.13.tgz"
+ integrity sha512-cTGB9ptp9dY9A5VbMSe7fQBcl/tt22Vcqdq8+eN93rblOuE0aCFu4aZ2vMwct/2t+lFnosm8RkQW1I0Omb1UtQ==
+
+object-assign@^4.1.1:
+ version "4.1.1"
+ resolved "https://registry.npmmirror.com/object-assign/-/object-assign-4.1.1.tgz"
+ integrity sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==
+
+once@^1.3.0, once@^1.3.1:
+ version "1.4.0"
+ resolved "https://registry.npmmirror.com/once/-/once-1.4.0.tgz"
+ integrity sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==
+ dependencies:
+ wrappy "1"
+
+parse5@^7.1.1:
+ version "7.1.2"
+ resolved "https://registry.npmmirror.com/parse5/-/parse5-7.1.2.tgz"
+ integrity sha512-Czj1WaSVpaoj0wbhMzLmWD69anp2WH7FXMB9n1Sy8/ZFF9jolSQVMu1Ij5WIyGmcBmhk7EOndpO4mIpihVqAXw==
+ dependencies:
+ entities "^4.4.0"
+
+path-is-absolute@^1.0.0:
+ version "1.0.1"
+ resolved "https://registry.npmmirror.com/path-is-absolute/-/path-is-absolute-1.0.1.tgz"
+ integrity sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==
+
+path-type@^4.0.0:
+ version "4.0.0"
+ resolved "https://registry.npmmirror.com/path-type/-/path-type-4.0.0.tgz"
+ integrity sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==
+
+picocolors@^1.0.0, picocolors@^1.1.0:
+ version "1.1.0"
+ resolved "https://registry.npmmirror.com/picocolors/-/picocolors-1.1.0.tgz"
+ integrity sha512-TQ92mBOW0l3LeMeyLV6mzy/kWr8lkd/hp3mTg7wYK7zJhuBStmGMBG0BdeDZS/dZx1IukaX6Bk11zcln25o1Aw==
+
+picomatch@^2.0.4, picomatch@^2.2.1, picomatch@^2.3.1:
+ version "2.3.1"
+ resolved "https://registry.npmmirror.com/picomatch/-/picomatch-2.3.1.tgz"
+ integrity sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==
+
+postcss@^8.4.43:
+ version "8.4.47"
+ resolved "https://registry.npmmirror.com/postcss/-/postcss-8.4.47.tgz"
+ integrity sha512-56rxCq7G/XfB4EkXq9Egn5GCqugWvDFjafDOThIdMBsI15iqPqR5r15TfSr1YPYeEI19YeaXMCbY6u88Y76GLQ==
+ dependencies:
+ nanoid "^3.3.7"
+ picocolors "^1.1.0"
+ source-map-js "^1.2.1"
+
+psl@^1.1.33:
+ version "1.9.0"
+ resolved "https://registry.npmmirror.com/psl/-/psl-1.9.0.tgz"
+ integrity sha512-E/ZsdU4HLs/68gYzgGTkMicWTLPdAftJLfJFlLUAAKZGkStNU72sZjT66SnMDVOfOWY/YAoiD7Jxa9iHvngcag==
+
+punycode@^2.1.1:
+ version "2.3.1"
+ resolved "https://registry.npmmirror.com/punycode/-/punycode-2.3.1.tgz"
+ integrity sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==
+
+querystringify@^2.1.1:
+ version "2.2.0"
+ resolved "https://registry.npmmirror.com/querystringify/-/querystringify-2.2.0.tgz"
+ integrity sha512-FIqgj2EUvTa7R50u0rGsyTftzjYmv/a3hO345bZNrqabNqjtgiDMgmo4mkUjd+nzU5oF3dClKqFIPUKybUyqoQ==
+
+queue-microtask@^1.2.2:
+ version "1.2.3"
+ resolved "https://registry.npmmirror.com/queue-microtask/-/queue-microtask-1.2.3.tgz"
+ integrity sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==
+
+readable-stream@^3.6.0:
+ version "3.6.2"
+ resolved "https://registry.npmmirror.com/readable-stream/-/readable-stream-3.6.2.tgz"
+ integrity sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==
+ dependencies:
+ inherits "^2.0.3"
+ string_decoder "^1.1.1"
+ util-deprecate "^1.0.1"
+
+readdirp@~3.6.0:
+ version "3.6.0"
+ resolved "https://registry.npmmirror.com/readdirp/-/readdirp-3.6.0.tgz"
+ integrity sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==
+ dependencies:
+ picomatch "^2.2.1"
+
+requires-port@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.npmmirror.com/requires-port/-/requires-port-1.0.0.tgz"
+ integrity sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==
+
+reusify@^1.0.4:
+ version "1.0.4"
+ resolved "https://registry.npmmirror.com/reusify/-/reusify-1.0.4.tgz"
+ integrity sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==
+
+rimraf@^3.0.2:
+ version "3.0.2"
+ resolved "https://registry.npmmirror.com/rimraf/-/rimraf-3.0.2.tgz"
+ integrity sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==
+ dependencies:
+ glob "^7.1.3"
+
+rollup-plugin-copy@^3.5.0:
+ version "3.5.0"
+ resolved "https://registry.npmmirror.com/rollup-plugin-copy/-/rollup-plugin-copy-3.5.0.tgz"
+ integrity sha512-wI8D5dvYovRMx/YYKtUNt3Yxaw4ORC9xo6Gt9t22kveWz1enG9QrhVlagzwrxSC455xD1dHMKhIJkbsQ7d48BA==
+ dependencies:
+ "@types/fs-extra" "^8.0.1"
+ colorette "^1.1.0"
+ fs-extra "^8.1.0"
+ globby "10.0.1"
+ is-plain-object "^3.0.0"
+
+rollup@^4.20.0:
+ version "4.24.0"
+ resolved "https://registry.npmmirror.com/rollup/-/rollup-4.24.0.tgz"
+ integrity sha512-DOmrlGSXNk1DM0ljiQA+i+o0rSLhtii1je5wgk60j49d1jHT5YYttBv1iWOnYSTG+fZZESUOSNiAl89SIet+Cg==
+ dependencies:
+ "@types/estree" "1.0.6"
+ optionalDependencies:
+ "@rollup/rollup-android-arm-eabi" "4.24.0"
+ "@rollup/rollup-android-arm64" "4.24.0"
+ "@rollup/rollup-darwin-arm64" "4.24.0"
+ "@rollup/rollup-darwin-x64" "4.24.0"
+ "@rollup/rollup-linux-arm-gnueabihf" "4.24.0"
+ "@rollup/rollup-linux-arm-musleabihf" "4.24.0"
+ "@rollup/rollup-linux-arm64-gnu" "4.24.0"
+ "@rollup/rollup-linux-arm64-musl" "4.24.0"
+ "@rollup/rollup-linux-powerpc64le-gnu" "4.24.0"
+ "@rollup/rollup-linux-riscv64-gnu" "4.24.0"
+ "@rollup/rollup-linux-s390x-gnu" "4.24.0"
+ "@rollup/rollup-linux-x64-gnu" "4.24.0"
+ "@rollup/rollup-linux-x64-musl" "4.24.0"
+ "@rollup/rollup-win32-arm64-msvc" "4.24.0"
+ "@rollup/rollup-win32-ia32-msvc" "4.24.0"
+ "@rollup/rollup-win32-x64-msvc" "4.24.0"
+ fsevents "~2.3.2"
+
+run-parallel@^1.1.9:
+ version "1.2.0"
+ resolved "https://registry.npmmirror.com/run-parallel/-/run-parallel-1.2.0.tgz"
+ integrity sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==
+ dependencies:
+ queue-microtask "^1.2.2"
+
+safe-buffer@~5.2.0:
+ version "5.2.1"
+ resolved "https://registry.npmmirror.com/safe-buffer/-/safe-buffer-5.2.1.tgz"
+ integrity sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==
+
+"safer-buffer@>= 2.1.2 < 3.0.0":
+ version "2.1.2"
+ resolved "https://registry.npmmirror.com/safer-buffer/-/safer-buffer-2.1.2.tgz"
+ integrity sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==
+
+saxes@^6.0.0:
+ version "6.0.0"
+ resolved "https://registry.npmmirror.com/saxes/-/saxes-6.0.0.tgz"
+ integrity sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA==
+ dependencies:
+ xmlchars "^2.2.0"
+
+semver@^6.0.0:
+ version "6.3.1"
+ resolved "https://registry.npmmirror.com/semver/-/semver-6.3.1.tgz"
+ integrity sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==
+
+semver@^7.3.5:
+ version "7.6.3"
+ resolved "https://registry.npmmirror.com/semver/-/semver-7.6.3.tgz"
+ integrity sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==
+
+set-blocking@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.npmmirror.com/set-blocking/-/set-blocking-2.0.0.tgz"
+ integrity sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==
+
+signal-exit@^3.0.0:
+ version "3.0.7"
+ resolved "https://registry.npmmirror.com/signal-exit/-/signal-exit-3.0.7.tgz"
+ integrity sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==
+
+simple-concat@^1.0.0:
+ version "1.0.1"
+ resolved "https://registry.npmmirror.com/simple-concat/-/simple-concat-1.0.1.tgz"
+ integrity sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q==
+
+simple-get@^3.0.3:
+ version "3.1.1"
+ resolved "https://registry.npmmirror.com/simple-get/-/simple-get-3.1.1.tgz"
+ integrity sha512-CQ5LTKGfCpvE1K0n2us+kuMPbk/q0EKl82s4aheV9oXjFEz6W/Y7oQFVJuU6QG77hRT4Ghb5RURteF5vnWjupA==
+ dependencies:
+ decompress-response "^4.2.0"
+ once "^1.3.1"
+ simple-concat "^1.0.0"
+
+slash@^3.0.0:
+ version "3.0.0"
+ resolved "https://registry.npmmirror.com/slash/-/slash-3.0.0.tgz"
+ integrity sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==
+
+source-map-js@^1.2.1:
+ version "1.2.1"
+ resolved "https://registry.npmmirror.com/source-map-js/-/source-map-js-1.2.1.tgz"
+ integrity sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==
+
+source-map@~0.6.1:
+ version "0.6.1"
+ resolved "https://registry.npmmirror.com/source-map/-/source-map-0.6.1.tgz"
+ integrity sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==
+
+string_decoder@^1.1.1:
+ version "1.3.0"
+ resolved "https://registry.npmmirror.com/string_decoder/-/string_decoder-1.3.0.tgz"
+ integrity sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==
+ dependencies:
+ safe-buffer "~5.2.0"
+
+"string-width@^1.0.2 || 2 || 3 || 4", string-width@^4.2.3:
+ version "4.2.3"
+ resolved "https://registry.npmmirror.com/string-width/-/string-width-4.2.3.tgz"
+ integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==
+ dependencies:
+ emoji-regex "^8.0.0"
+ is-fullwidth-code-point "^3.0.0"
+ strip-ansi "^6.0.1"
+
+strip-ansi@^6.0.1:
+ version "6.0.1"
+ resolved "https://registry.npmmirror.com/strip-ansi/-/strip-ansi-6.0.1.tgz"
+ integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==
+ dependencies:
+ ansi-regex "^5.0.1"
+
+symbol-tree@^3.2.4:
+ version "3.2.4"
+ resolved "https://registry.npmmirror.com/symbol-tree/-/symbol-tree-3.2.4.tgz"
+ integrity sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==
+
+tar@^6.1.11:
+ version "6.2.1"
+ resolved "https://registry.npmmirror.com/tar/-/tar-6.2.1.tgz"
+ integrity sha512-DZ4yORTwrbTj/7MZYq2w+/ZFdI6OZ/f9SFHR+71gIVUZhOQPHzVCLpvRnPgyaMpfWxxk/4ONva3GQSyNIKRv6A==
+ dependencies:
+ chownr "^2.0.0"
+ fs-minipass "^2.0.0"
+ minipass "^5.0.0"
+ minizlib "^2.1.1"
+ mkdirp "^1.0.3"
+ yallist "^4.0.0"
+
+to-regex-range@^5.0.1:
+ version "5.0.1"
+ resolved "https://registry.npmmirror.com/to-regex-range/-/to-regex-range-5.0.1.tgz"
+ integrity sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==
+ dependencies:
+ is-number "^7.0.0"
+
+tough-cookie@^4.1.2:
+ version "4.1.4"
+ resolved "https://registry.npmmirror.com/tough-cookie/-/tough-cookie-4.1.4.tgz"
+ integrity sha512-Loo5UUvLD9ScZ6jh8beX1T6sO1w2/MpCRpEP7V280GKMVUQ0Jzar2U3UJPsrdbziLEMMhu3Ujnq//rhiFuIeag==
+ dependencies:
+ psl "^1.1.33"
+ punycode "^2.1.1"
+ universalify "^0.2.0"
+ url-parse "^1.5.3"
+
+tr46@^3.0.0:
+ version "3.0.0"
+ resolved "https://registry.npmmirror.com/tr46/-/tr46-3.0.0.tgz"
+ integrity sha512-l7FvfAHlcmulp8kr+flpQZmVwtu7nfRV7NZujtN0OqES8EL4O4e0qqzL0DC5gAvx/ZC/9lk6rhcUwYvkBnBnYA==
+ dependencies:
+ punycode "^2.1.1"
+
+tr46@~0.0.3:
+ version "0.0.3"
+ resolved "https://registry.npmmirror.com/tr46/-/tr46-0.0.3.tgz"
+ integrity sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==
+
+typescript@^5.4.5:
+ version "5.6.3"
+ resolved "https://registry.npmmirror.com/typescript/-/typescript-5.6.3.tgz"
+ integrity sha512-hjcS1mhfuyi4WW8IWtjP7brDrG2cuDZukyrYrSauoXGNgx0S7zceP07adYkJycEr56BOUTNPzbInooiN3fn1qw==
+
+undici-types@~6.19.2:
+ version "6.19.8"
+ resolved "https://registry.npmmirror.com/undici-types/-/undici-types-6.19.8.tgz"
+ integrity sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw==
+
+universalify@^0.1.0:
+ version "0.1.2"
+ resolved "https://registry.npmmirror.com/universalify/-/universalify-0.1.2.tgz"
+ integrity sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==
+
+universalify@^0.2.0:
+ version "0.2.0"
+ resolved "https://registry.npmmirror.com/universalify/-/universalify-0.2.0.tgz"
+ integrity sha512-CJ1QgKmNg3CwvAv/kOFmtnEN05f0D/cn9QntgNOQlQF9dgvVTHj3t+8JPdjqawCHk7V/KA+fbUqzZ9XWhcqPUg==
+
+universalify@^2.0.0:
+ version "2.0.1"
+ resolved "https://registry.npmmirror.com/universalify/-/universalify-2.0.1.tgz"
+ integrity sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==
+
+url-parse@^1.5.3:
+ version "1.5.10"
+ resolved "https://registry.npmmirror.com/url-parse/-/url-parse-1.5.10.tgz"
+ integrity sha512-WypcfiRhfeUP9vvF0j6rw0J3hrWrw6iZv3+22h6iRMJ/8z1Tj6XfLP4DsUix5MhMPnXpiHDoKyoZ/bdCkwBCiQ==
+ dependencies:
+ querystringify "^2.1.1"
+ requires-port "^1.0.0"
+
+util-deprecate@^1.0.1:
+ version "1.0.2"
+ resolved "https://registry.npmmirror.com/util-deprecate/-/util-deprecate-1.0.2.tgz"
+ integrity sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==
+
+vite-plugin-copy@^0.1.6:
+ version "0.1.6"
+ resolved "https://registry.npmmirror.com/vite-plugin-copy/-/vite-plugin-copy-0.1.6.tgz"
+ integrity sha512-bqIaefZOE2Jx8P5wJuHKL5GzCERa/pcwdUQWaocyTNXgalN2xkxXH7LmqRJ34V2OlKF2F9E/zj0zITS7U6PpUg==
+ dependencies:
+ fast-glob "^3.2.7"
+
+vite-plugin-static-copy@^2.0.0:
+ version "2.1.0"
+ resolved "https://registry.npmmirror.com/vite-plugin-static-copy/-/vite-plugin-static-copy-2.1.0.tgz"
+ integrity sha512-n8lEOIVM00Y/zronm0RG8RdPyFd0SAAFR0sii3NWmgG3PSCyYMsvUNRQTlb3onp1XeMrKIDwCrPGxthKvqX9OQ==
+ dependencies:
+ chokidar "^3.5.3"
+ fast-glob "^3.2.11"
+ fs-extra "^11.1.0"
+ picocolors "^1.0.0"
+
+vite@^5.4.8:
+ version "5.4.8"
+ resolved "https://registry.npmmirror.com/vite/-/vite-5.4.8.tgz"
+ integrity sha512-FqrItQ4DT1NC4zCUqMB4c4AZORMKIa0m8/URVCZ77OZ/QSNeJ54bU1vrFADbDsuwfIPcgknRkmqakQcgnL4GiQ==
+ dependencies:
+ esbuild "^0.21.3"
+ postcss "^8.4.43"
+ rollup "^4.20.0"
+ optionalDependencies:
+ fsevents "~2.3.3"
+
+w3c-xmlserializer@^4.0.0:
+ version "4.0.0"
+ resolved "https://registry.npmmirror.com/w3c-xmlserializer/-/w3c-xmlserializer-4.0.0.tgz"
+ integrity sha512-d+BFHzbiCx6zGfz0HyQ6Rg69w9k19nviJspaj4yNscGjrHu94sVP+aRm75yEbCh+r2/yR+7q6hux9LVtbuTGBw==
+ dependencies:
+ xml-name-validator "^4.0.0"
+
+webidl-conversions@^3.0.0:
+ version "3.0.1"
+ resolved "https://registry.npmmirror.com/webidl-conversions/-/webidl-conversions-3.0.1.tgz"
+ integrity sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==
+
+webidl-conversions@^7.0.0:
+ version "7.0.0"
+ resolved "https://registry.npmmirror.com/webidl-conversions/-/webidl-conversions-7.0.0.tgz"
+ integrity sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==
+
+whatwg-encoding@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.npmmirror.com/whatwg-encoding/-/whatwg-encoding-2.0.0.tgz"
+ integrity sha512-p41ogyeMUrw3jWclHWTQg1k05DSVXPLcVxRTYsXUk+ZooOCZLcoYgPZ/HL/D/N+uQPOtcp1me1WhBEaX02mhWg==
+ dependencies:
+ iconv-lite "0.6.3"
+
+whatwg-mimetype@^3.0.0:
+ version "3.0.0"
+ resolved "https://registry.npmmirror.com/whatwg-mimetype/-/whatwg-mimetype-3.0.0.tgz"
+ integrity sha512-nt+N2dzIutVRxARx1nghPKGv1xHikU7HKdfafKkLNLindmPU/ch3U31NOCGGA/dmPcmb1VlofO0vnKAcsm0o/Q==
+
+whatwg-url@^11.0.0:
+ version "11.0.0"
+ resolved "https://registry.npmmirror.com/whatwg-url/-/whatwg-url-11.0.0.tgz"
+ integrity sha512-RKT8HExMpoYx4igMiVMY83lN6UeITKJlBQ+vR/8ZJ8OCdSiN3RwCq+9gH0+Xzj0+5IrM6i4j/6LuvzbZIQgEcQ==
+ dependencies:
+ tr46 "^3.0.0"
+ webidl-conversions "^7.0.0"
+
+whatwg-url@^5.0.0:
+ version "5.0.0"
+ resolved "https://registry.npmmirror.com/whatwg-url/-/whatwg-url-5.0.0.tgz"
+ integrity sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==
+ dependencies:
+ tr46 "~0.0.3"
+ webidl-conversions "^3.0.0"
+
+wide-align@^1.1.2:
+ version "1.1.5"
+ resolved "https://registry.npmmirror.com/wide-align/-/wide-align-1.1.5.tgz"
+ integrity sha512-eDMORYaPNZ4sQIuuYPDHdQvf4gyCF9rEEV/yPxGfwPkRodwEgiMUUXTx/dex+Me0wxx53S+NgUHaP7y3MGlDmg==
+ dependencies:
+ string-width "^1.0.2 || 2 || 3 || 4"
+
+wrappy@1:
+ version "1.0.2"
+ resolved "https://registry.npmmirror.com/wrappy/-/wrappy-1.0.2.tgz"
+ integrity sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==
+
+ws@^8.11.0:
+ version "8.18.0"
+ resolved "https://registry.npmmirror.com/ws/-/ws-8.18.0.tgz"
+ integrity sha512-8VbfWfHLbbwu3+N6OKsOMpBdT4kXPDDB9cJk2bJ6mh9ucxdlnNvH1e+roYkKmN9Nxw2yjz7VzeO9oOz2zJ04Pw==
+
+xml-name-validator@^4.0.0:
+ version "4.0.0"
+ resolved "https://registry.npmmirror.com/xml-name-validator/-/xml-name-validator-4.0.0.tgz"
+ integrity sha512-ICP2e+jsHvAj2E2lIHxa5tjXRlKDJo4IdvPvCXbXQGdzSfmSpNVyIKMvoZHjDY9DP0zV17iI85o90vRFXNccRw==
+
+xmlchars@^2.2.0:
+ version "2.2.0"
+ resolved "https://registry.npmmirror.com/xmlchars/-/xmlchars-2.2.0.tgz"
+ integrity sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==
+
+yallist@^4.0.0:
+ version "4.0.0"
+ resolved "https://registry.npmmirror.com/yallist/-/yallist-4.0.0.tgz"
+ integrity sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==