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==