From ba64ff931b38fbd859e226f854ead79fac622771 Mon Sep 17 00:00:00 2001 From: zdg Date: Wed, 31 Jul 2024 10:04:12 +0800 Subject: [PATCH 1/6] =?UTF-8?q?=E6=8F=92=E4=BB=B6=E9=87=8D=E6=96=B0?= =?UTF-8?q?=E6=95=B4=E7=90=86?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- package.json | 6 +- .../src/plugins/fabric/event/clickEvent.js | 236 ---- .../src/plugins/fabric/event/index.js | 35 - src/renderer/src/plugins/fabric/history.js | 120 -- .../src/{ => plugins/fabric}/i18n/en.json | 0 .../src/{ => plugins/fabric}/i18n/index.js | 0 .../src/{ => plugins/fabric}/i18n/zh.json | 0 src/renderer/src/plugins/fabric/index.js | 1015 +++++++++++++---- .../src/plugins/fabric/nodes/draw/index.js | 27 - .../src/plugins/fabric/nodes/draw/material.js | 125 -- .../plugins/fabric/nodes/draw/multiColor.js | 98 -- src/renderer/src/plugins/fabric/test.js | 223 ---- src/renderer/src/plugins/fabric/utils/bg.js | 77 -- .../src/plugins/fabric/utils/color.js | 82 -- src/renderer/src/plugins/fabric/utils/draw.js | 33 - .../src/plugins/fabric/utils/index.js | 165 --- .../plugins/fabric/utils/shape/arrowLine.js | 164 --- .../src/plugins/fabric/utils/shape/line.js | 109 -- src/renderer/src/store/modules/draw.js | 295 ----- src/renderer/src/views/tool/test.vue | 9 +- 20 files changed, 826 insertions(+), 1993 deletions(-) delete mode 100644 src/renderer/src/plugins/fabric/event/clickEvent.js delete mode 100644 src/renderer/src/plugins/fabric/event/index.js delete mode 100644 src/renderer/src/plugins/fabric/history.js rename src/renderer/src/{ => plugins/fabric}/i18n/en.json (100%) rename src/renderer/src/{ => plugins/fabric}/i18n/index.js (100%) rename src/renderer/src/{ => plugins/fabric}/i18n/zh.json (100%) delete mode 100644 src/renderer/src/plugins/fabric/nodes/draw/index.js delete mode 100644 src/renderer/src/plugins/fabric/nodes/draw/material.js delete mode 100644 src/renderer/src/plugins/fabric/nodes/draw/multiColor.js delete mode 100644 src/renderer/src/plugins/fabric/test.js delete mode 100644 src/renderer/src/plugins/fabric/utils/bg.js delete mode 100644 src/renderer/src/plugins/fabric/utils/color.js delete mode 100644 src/renderer/src/plugins/fabric/utils/draw.js delete mode 100644 src/renderer/src/plugins/fabric/utils/index.js delete mode 100644 src/renderer/src/plugins/fabric/utils/shape/arrowLine.js delete mode 100644 src/renderer/src/plugins/fabric/utils/shape/line.js delete mode 100644 src/renderer/src/store/modules/draw.js diff --git a/package.json b/package.json index 7893b0b..5d0c6f5 100644 --- a/package.json +++ b/package.json @@ -21,8 +21,8 @@ "dependencies": { "@electron-toolkit/preload": "^3.0.1", "@electron-toolkit/utils": "^3.0.0", - "@element-plus/icons-vue": "^2.3.1", "@electron/remote": "^2.1.2", + "@element-plus/icons-vue": "^2.3.1", "@vitejs/plugin-vue-jsx": "^4.0.0", "@vueuse/core": "^10.11.0", "crypto-js": "^4.2.0", @@ -30,12 +30,12 @@ "electron-log": "^5.1.7", "electron-updater": "^6.1.7", "element-plus": "^2.7.6", - "fabric": "5.3.0", + "fabric-with-erasing": "^1.0.1", "js-cookie": "^3.0.5", "jsencrypt": "^3.3.2", "jsondiffpatch": "0.6.0", - "pdfjs-dist": "4.4.168", "lodash": "^4.17.21", + "pdfjs-dist": "4.4.168", "pinia": "^2.1.7", "pinia-plugin-persistedstate": "^3.2.1", "spark-md5": "^3.0.2", diff --git a/src/renderer/src/plugins/fabric/event/clickEvent.js b/src/renderer/src/plugins/fabric/event/clickEvent.js deleted file mode 100644 index 5a70293..0000000 --- a/src/renderer/src/plugins/fabric/event/clickEvent.js +++ /dev/null @@ -1,236 +0,0 @@ -/** - * @description: 点击事件 - */ -import { FabricVue } from '@/plugins/fabric' -import * as TYPES from '@/constants/draw' -import * as fabricStore from '@/store/modules/draw' - -export let updateInkHook = null // ((ink: IInk[]) => void) | null - -export class CanvasClickEvent { - isMouseDown = false - isSpaceKeyDown = false - startPoint // fabric.Point | undefined - currentElement = null // The current mouse move draws the element|当前鼠标移动会绘制元素 - - mouseDownTime = 0 - autoDrawInk = [[], [], []] // google auto draw ink Array> - isDrawBasic = false // 是否默认绘制 - - constructor() { - this.initClickEvent() - } - - initClickEvent() { - const canvas = FabricVue.canvas - const useDrawStore = fabricStore.useDrawStore() - const useBoardStore = fabricStore.useBoardStore() - - // 事件:按下鼠标 - canvas?.on('mouse:down', (e) => { - console.log(222222222222222222222222222) - - this.isMouseDown = true - if (this.isSpaceKeyDown) { - return - } - this.startPoint = e.absolutePointer - let currentElement = null - - if (useBoardStore.mode === TYPES.ActionMode.DRAW) { - if (useBoardStore.drawType === TYPES.DrawType.Shape) { - // switch (useShapeStore.shapeStyle) { - // case ShapeStyle.Rect: - // currentElement = new RectShape(e.absolutePointer) - // break - // case ShapeStyle.Circle: - // currentElement = new CircleShape(e.absolutePointer) - // break - // case ShapeStyle.Line: - // currentElement = new LineShape(e.absolutePointer) - // break - // case ShapeStyle.Ellipse: - // currentElement = new EllipseShape(e.absolutePointer) - // break - // case ShapeStyle.Triangle: - // currentElement = new TriangleShape(e.absolutePointer) - // break - // case ShapeStyle.ArrowLine: - // currentElement = new ArrowLineShape(e.absolutePointer) - // break - // case ShapeStyle.ArrowOutline: - // currentElement = new ArrowOutlineShape(e.absolutePointer) - // break - // case ShapeStyle.Cloud: - // currentElement = new CloudShape(e.absolutePointer) - // break - // case ShapeStyle.Tooltips: - // currentElement = new TooltipsShape(e.absolutePointer) - // break - // case ShapeStyle.Lightning: - // currentElement = new LightningShape(e.absolutePointer) - // break - // case ShapeStyle.Close: - // currentElement = new CloseShape(e.absolutePointer) - // break - // case ShapeStyle.Check: - // currentElement = new CheckShap(e.absolutePointer) - // break - // case ShapeStyle.Info: - // currentElement = new InfoShape(e.absolutePointer) - // break - // case ShapeStyle.Backspace: - // currentElement = new BackspaceShape(e.absolutePointer) - // break - // case ShapeStyle.Block: - // currentElement = new BlockShap(e.absolutePointer) - // break - // case ShapeStyle.Speaker: - // currentElement = new SpeakerShape(e.absolutePointer) - // break - // case ShapeStyle.Search: - // currentElement = new SearchShape(e.absolutePointer) - // break - // case ShapeStyle.InfoOutline: - // currentElement = new InfoOutlineShape(e.absolutePointer) - // break - // case ShapeStyle.Heart: - // currentElement = new HeartShape(e.absolutePointer) - // break - // case ShapeStyle.Alert: - // currentElement = new AlertShape(e.absolutePointer) - // break - // default: - // break - // } - } else if (useBoardStore.drawType === TYPES.DrawType.FreeStyle) { - switch (useDrawStore.drawStyle) { - // case TYPES.DrawStyle.Shape: - // currentElement = new ShapeElement() - // break - // case TYPES.DrawStyle.Pixels: - // currentElement = new PixelsElement() - // break - // case TYPES.DrawStyle.Text: - // currentElement = new DrawTextElement() - // break - // case TYPES.DrawStyle.MultiLine: - // currentElement = new MultiLineElement() - // break - // case TYPES.DrawStyle.Reticulate: - // currentElement = new ReticulateElement() - // break - // case TYPES.DrawStyle.Rainbow: - // currentElement = new RainbowElement() - // break - // case TYPES.DrawStyle.Thorn: - // currentElement = new ThornElement() - // break - // case TYPES.DrawStyle.MultiPoint: - // currentElement = new MultiPointElement() - // break - // case TYPES.DrawStyle.Wiggle: - // currentElement = new WiggleElement() - // break - case TYPES.DrawStyle.Basic: - // if (useDrawStore.openAutoDraw) { - // autoDrawData.resetLoadedSVG() - // this.mouseDownTime = new Date().getTime() - // } - this.isDrawBasic = true - break - default: - break - } - } - } - this.currentElement = currentElement - }) - - // 事件:移动鼠标 - canvas?.on('mouse:move', (e) => { - console.log(222222222222222222222222222) - - if (this.isMouseDown) { - // Press space, drag the canvas, stop drawing. - if (this.isSpaceKeyDown) { - canvas.relativePan(new fabric.Point(e.e.movementX, e.e.movementY)) - return - } - - // two touch disabled drawing on mobile - if (FabricVue.evnet?.touchEvent?.isTwoTouch) { - return - } - - if ( - this.mouseDownTime && - e.absolutePointer?.x && - e.absolutePointer?.y - ) { - this.autoDrawInk[0].push(e.absolutePointer?.x) - this.autoDrawInk[1].push(e.absolutePointer?.y) - this.autoDrawInk[2].push(new Date().getTime() - this.mouseDownTime) - } - - if ( - useBoardStore.mode === TYPES.ActionMode.DRAW && this.currentElement - ) { - this.currentElement.addPosition(e.absolutePointer) - } - } - }) - - // 事件:松开鼠标 - canvas?.on('mouse:up', (e) => { - console.log(222222222222222222222222222) - - this.isMouseDown = false - if (this.autoDrawInk?.[0]?.length > 3) { - autoDrawData.addInk([...this.autoDrawInk]) - updateInkHook?.([...autoDrawData.inks]) - this.autoDrawInk = [[], [], []] - this.mouseDownTime = 0 - } - - if (this.currentElement) { - let isDestroy = false - if (this.startPoint && e.absolutePointer) { - const { x: startX, y: startY } = this.startPoint - const { x: endX, y: endY } = e.absolutePointer - if (startX === endX && startY === endY) { - this.currentElement.destroy() - isDestroy = true - } - } - if (!isDestroy) { - if ( - this.currentElement instanceof LineShape || - this.currentElement instanceof ArrowLineShape - ) { - this.currentElement?.mouseUp() - } - FabricVue.history?.saveState() - } - this.currentElement = null - } - // zdg: 基础画笔 保存数据 - if (this.isDrawBasic) FabricVue.history?.saveState() - }) - - canvas?.on('mouse:dblclick', (e) => { - if (e?.absolutePointer) { - const { x, y } = e.absolutePointer - FabricVue.textElement?.loadText(x, y) - } - }) - } - - setSpaceKeyDownState(isSpaceKeyDown) { - this.isSpaceKeyDown = isSpaceKeyDown - } - - changeInkHookFn(fn) { - updateInkHook = fn - } -} \ No newline at end of file diff --git a/src/renderer/src/plugins/fabric/event/index.js b/src/renderer/src/plugins/fabric/event/index.js deleted file mode 100644 index 263d152..0000000 --- a/src/renderer/src/plugins/fabric/event/index.js +++ /dev/null @@ -1,35 +0,0 @@ -import { CanvasClickEvent } from './clickEvent' -// import { ObjectEvent } from './objectEvent' -// import { CanvasTouchEvent } from './touchEvent' -// import { CanvasZoomEvent } from './zoomEvent' -// import { WindowEvent } from './windowEvent' - -export class CanvasEvent { - clickEvent // CanvasClickEvent - zoomEvent // CanvasZoomEvent - objectEvent // ObjectEvent - windowEvent // WindowEvent - touchEvent // CanvasTouchEvent - - constructor() { - const clickEvent = new CanvasClickEvent() - this.clickEvent = clickEvent - - // const zoomEvent = new CanvasZoomEvent() - // this.zoomEvent = zoomEvent - - // const objectEvent = new ObjectEvent() - // this.objectEvent = objectEvent - - // const windowEvent = new WindowEvent() - // this.windowEvent = windowEvent - - // const touchEvent = new CanvasTouchEvent() - // this.touchEvent = touchEvent - } - - removeEvent() { - this.windowEvent.removeWindowEvent() - this.touchEvent.removeTouchEvent() - } -} diff --git a/src/renderer/src/plugins/fabric/history.js b/src/renderer/src/plugins/fabric/history.js deleted file mode 100644 index ab8cf15..0000000 --- a/src/renderer/src/plugins/fabric/history.js +++ /dev/null @@ -1,120 +0,0 @@ -/** - * 历史记录 - */ -import { FabricVue } from '@/plugins/fabric' -import * as commUtils from '@/plugins/fabric/utils' -import * as bgUtils from '@/plugins/fabric/utils/bg' -import { diff, unpatch, patch } from 'jsondiffpatch' -import * as fabricStore from '@/store/modules/draw' - -const initState = {} - - -/** - * Operation History - */ -export class History { - diffs = [] - canvasData = {} - index = 0 - - constructor() { - const canvas = FabricVue.canvas - if (canvas) { - const canvasJson = commUtils.getCanvasJSON() - this.canvasData = cloneDeep(canvasJson ?? {}) - } - } - - // 缓存相关数据json - saveState() { - const canvas = FabricVue?.canvas - if (canvas) { - const useFileStore = fabricStore.useFileStore() - this.diffs = this.diffs.slice(0, this.index) - const canvasJson = commUtils.getCanvasJSON() - const delta = diff(canvasJson, this.canvasData) - this.diffs.push(delta) - - // More than 50 operations, remove initial state - if (this.diffs.length > 50) { - this.diffs.shift() - } else { - this.index++ - } - this.canvasData = cloneDeep(canvasJson ?? {}) - useFileStore.updateBoardData(canvasJson) - } - } - // 撤销 - undo() { - const canvas = FabricVue?.canvas - if (canvas && this.index > 0) { - const useFileStore = fabricStore.useFileStore() - const delta = this.diffs[this.index - 1] - this.index-- - const canvasJson = patch(this.canvasData, delta) - canvas.loadFromJSON(canvasJson, () => { - commUtils.handleCanvasJSONLoaded(canvas) - - canvas.requestRenderAll() - useFileStore.updateBoardData(canvasJson) - this.canvasData = cloneDeep(canvasJson ?? {}) - FabricVue.triggerHook() - - if ((delta)?.backgroundImage) { - handleBackgroundImageWhenCanvasSizeChange() - } - }) - } - } - // 退回|重做 - redo() { - const canvas = FabricVue?.canvas - if (this.index < this.diffs.length && canvas) { - const useFileStore = fabricStore.useFileStore() - const delta = this.diffs[this.index] - this.index++ - const canvasJson = unpatch(this.canvasData, delta) - canvas.loadFromJSON(canvasJson, () => { - handleCanvasJSONLoaded(canvas) - canvas.requestRenderAll() - - useFileStore.updateBoardData(canvasJson) - this.canvasData = cloneDeep(canvasJson ?? {}) - FabricVue.triggerHook() - - if ((delta)?.backgroundImage) { - bgUtils.handleBackgroundImageWhenCanvasSizeChange() - } - }) - } - } - - clean() { - const useFileStore = fabricStore.useFileStore() - const useBoardStore = fabricStore.useBoardStore() - FabricVue?.canvas?.clear() - this.index = 0 - this.diffs = [] - this.canvasData = {} - useFileStore.updateBoardData(initState) - useBoardStore.updateBackgroundColor('#ffffff') - useBoardStore.cleanBackgroundImage() - } - - initHistory() { - const canvas = FabricVue.canvas - if (canvas) { - const canvasJson = commUtils.getCanvasJSON() - this.canvasData = canvasJson - this.index = 0 - this.diffs = [] - } - } -} - -// 对象克隆 -function cloneDeep(obj = {}) { - return JSON.parse(JSON.stringify(obj)) -} \ No newline at end of file diff --git a/src/renderer/src/i18n/en.json b/src/renderer/src/plugins/fabric/i18n/en.json similarity index 100% rename from src/renderer/src/i18n/en.json rename to src/renderer/src/plugins/fabric/i18n/en.json diff --git a/src/renderer/src/i18n/index.js b/src/renderer/src/plugins/fabric/i18n/index.js similarity index 100% rename from src/renderer/src/i18n/index.js rename to src/renderer/src/plugins/fabric/i18n/index.js diff --git a/src/renderer/src/i18n/zh.json b/src/renderer/src/plugins/fabric/i18n/zh.json similarity index 100% rename from src/renderer/src/i18n/zh.json rename to src/renderer/src/plugins/fabric/i18n/zh.json diff --git a/src/renderer/src/plugins/fabric/index.js b/src/renderer/src/plugins/fabric/index.js index 277112f..cb16fb9 100644 --- a/src/renderer/src/plugins/fabric/index.js +++ b/src/renderer/src/plugins/fabric/index.js @@ -1,27 +1,777 @@ /** - * 二次封装fabric + * @description 封装fabric js */ -import { fabric } from 'fabric' -import * as TYPES from '@/constants/draw' -import * as fabricUtils from '@/plugins/fabric/utils/draw' -import * as commUtils from '@/plugins/fabric/utils' -import * as bgUtils from '@/plugins/fabric/utils/bg' -import * as fabricStore from '@/store/modules/draw' -import { History } from '@/plugins/fabric/history' -import { CanvasEvent } from '@/plugins/fabric/event' +// import { fabric } from 'fabric' +import { fabric } from 'fabric-with-erasing' +import { diff, unpatch, patch } from 'jsondiffpatch' -// 节点node -import { renderPencilBrush } from '@/plugins/fabric/nodes/draw' +// 当前使用到的常量|类型(枚举) ============================ +export class TYPES { + static ActionMode = { + DRAW: 'draw', // 画笔模式 + ERASE: 'erase', // 橡皮擦模式 + SELECT: 'select', // 选择模式 + Board: 'board' // 画板模式 + } + // 画笔类型 + static DrawType = { + FreeStyle: 'freeStyle', + Shape: 'shape' + } + // 画笔样式 + static DrawStyle = { + Basic: 'basic', + Rainbow: 'rainbow', + Shape: 'shape', + Material: 'material', + Pixels: 'pixels', + MultiColor: 'multiColor', + Text: 'text', + MultiLine: 'multiLine', + Reticulate: 'reticulate', + MultiPoint: 'multiPoint', + Wiggle: 'wiggle', + Thorn: 'thorn' + } + // 各种形状 + static DrawShape = { + Bubble: 'bubble', + Star: 'star', + Love: 'love', + Butterfly: 'butterfly', + Snow: 'snow', + Music: 'music', + Sun: 'sun', + Moon: 'moon', + Leaf: 'leaf', + Flower: 'flower' + } + // 材质类型 + static MATERIAL_TYPE = { + CRAYON: 'crayon', + CARBON: 'carbon', + CLOTH: 'cloth', + OIL: 'oil', + CRAYON_DARK: 'crayonDark' + } + // 多颜色类型 + static MultiColorType = { + COL: 'col', + ROW: 'row', + CIRCLE: 'circle' + } + // 画笔元素 + static FREESTYLE_ELEMENT_CUSTOM_TYPE = { + IMAGE: 'image', + I_TEXT: 'itext', + RAINBOW: 'rainbow', + SHAPE: 'shape', + PIXELS: 'pixels', + DRAW_TEXT: 'drawText', + MULTI_LINE: 'multiLine', + RETICULATE: 'reticulate', + MULTI_POINT: 'multiPoint', + WIGGLE: 'wiggle', + THORN: 'thorn' + } + // 形状元素 + static SHAPE_ELEMENT_CUSTOM_TYPE = { + SHAPE_LINE: 'shapeLine', + SHAPE_RECT: 'shapeRect', + SHAPE_CIRCLE: 'shapeCircle', + SHAPE_ELLIPSE: 'shapeEllipse', + SHAPE_TRIANGLE: 'shapeTriangle', + SHAPE_ARROW_LINE: 'shapeArrowLine', + SHAPE_ARROW_OUTLINE: 'shapeArrowOutline', + SHAPE_CLOUD: 'shapeCloud', + SHAPE_TOOLTIPS: 'shapeTooltips', + SHAPE_LIGHTNING: 'shapeLightning', + SHAPE_CLOSE: 'shapeClose', + SHAPE_CHECK: 'shapeCheck', + SHAPE_INFO: 'shapeInfo', + SHAPE_BACKSPACE: 'shapeBackspace', + SHAPE_BLOCK: 'shapeBlock', + SHAPE_SPEAKER: 'shapeSpeaker', + SHAPE_SEARCH: 'shapeSearch', + SHAPE_INFO_OUTLINE: 'shapeInfoOutline', + SHAPE_HEART: 'shapeHeart', + SHAPE_ALERT: 'shapeAlert' + } + // 元素类型 + static ELEMENT_CUSTOM_TYPE = { + ...this.FREESTYLE_ELEMENT_CUSTOM_TYPE, + ...this.SHAPE_ELEMENT_CUSTOM_TYPE + } +} +// 公共方法-工具类 +export class Utils { + static lineUtils = { + polygonPositionHandler: (fabricControl, fabricObject) => { + const points = fabricObject.points + const x = points[fabricControl.pointIndex].x - fabricObject.pathOffset.x + const y = points[fabricControl.pointIndex].y - fabricObject.pathOffset.y + return fabric.util.transformPoint( + { x, y }, + fabric.util.multiplyTransformMatrices( + fabricObject.canvas?.viewportTransform, + fabricObject.calcTransformMatrix() + ) + ) + }, + anchorWrapper: (anchorIndex, fn) => { + return function (eventData, transform, x, y ) { + const fabricObject = transform.target + const points = fabricObject.points + const absolutePoint = fabric.util.transformPoint( + { + x: points[anchorIndex].x - fabricObject.pathOffset.x, + y: points[anchorIndex].y - fabricObject.pathOffset.y + }, + fabricObject.calcTransformMatrix() + ) + const actionPerformed = fn(eventData, transform, x, y) + fabricObject._setPositionDimensions({}) + const polygonBaseSize = getObjectSizeWithStroke(fabricObject) + + const newX = + (points[anchorIndex].x - fabricObject.pathOffset.x) / polygonBaseSize.x + const newY = + (points[anchorIndex].y - fabricObject.pathOffset.y) / polygonBaseSize.y + fabricObject.setPositionByOrigin( + absolutePoint, + (newX + 0.5), + (newY + 0.5) + ) + return actionPerformed + } + }, + actionHandler: (transform,x, y) => { + const polygon = transform.target, + currentControl = polygon.controls[polygon.__corner], + mouseLocalPosition = polygon.toLocalPoint( + new fabric.Point(x, y), + 'center', + 'center' + ), + polygonBaseSize = getObjectSizeWithStroke(polygon), + size = polygon._getTransformedDimensions(0, 0), + finalPointPosition = { + x: + (mouseLocalPosition.x * polygonBaseSize.x) / size.x + + polygon.pathOffset.x, + y: + (mouseLocalPosition.y * polygonBaseSize.y) / size.y + + polygon.pathOffset.y + } + + const points = polygon.points + points[currentControl.pointIndex] = finalPointPosition + + return true + } + } + static arrowLineUtils = { + pathPositionHandler: (fabricControl, fabricObject) => { + const paths = fabricObject.path + const x = paths[fabricControl.pointIndex][1] - fabricObject.pathOffset.x + const y = paths[fabricControl.pointIndex][2] - fabricObject.pathOffset.y + return fabric.util.transformPoint( + { x, y }, + fabric.util.multiplyTransformMatrices( + fabricObject.canvas?.viewportTransform, + fabricObject.calcTransformMatrix() + ) + ) + }, + anchorWrapper: (anchorIndex, fn) => { + return function (eventData, transform, x, y) { + const fabricObject = transform.target + const paths = fabricObject.path + const absolutePoint = fabric.util.transformPoint( + { + x: paths[anchorIndex][1] - fabricObject.pathOffset.x, + y: paths[anchorIndex][2] - fabricObject.pathOffset.y + }, + fabricObject.calcTransformMatrix() + ) + const actionPerformed = fn(eventData, transform, x, y) + fabricObject._setPath(fabricObject.path) + const pathBaseSize = getObjectSizeWithStroke(fabricObject) + + const newX = + (paths[anchorIndex][1] - fabricObject.pathOffset.x) / pathBaseSize.x + const newY = + (paths[anchorIndex][2] - fabricObject.pathOffset.y) / pathBaseSize.y + fabricObject.setPositionByOrigin( + absolutePoint, + (newX + 0.5), + (newY + 0.5) + ) + return actionPerformed + } + }, + actionHandler: (transform, x, y) => { + const pathObj = transform.target, + currentControl = pathObj.controls[pathObj.__corner], + mouseLocalPosition = pathObj.toLocalPoint( + new fabric.Point(x, y), + 'center', + 'center' + ), + pathBaseSize = getObjectSizeWithStroke(pathObj), + size = pathObj._getTransformedDimensions(0, 0), + finalPointPosition = { + x: + (mouseLocalPosition.x * pathBaseSize.x) / size.x + pathObj.pathOffset.x, + y: (mouseLocalPosition.y * pathBaseSize.y) / size.y + pathObj.pathOffset.y + } + + const path = pathObj.path + path[currentControl.pointIndex][1] = finalPointPosition.x + path[currentControl.pointIndex][2] = finalPointPosition.y + + if ( + currentControl.pointIndex === path?.length - 5 || + currentControl.pointIndex === path?.length - 6 + ) { + const point1 = path[path.length - 6] + const point2 = path[path.length - 5] + const newPath = calculateArrowSlidePath( + path, + point1[1], + point1[2], + point2[1], + point2[2] + ) + pathObj._setPath(newPath) + } + + return true + } + } + // 获取uuid string|number|all + static uuidv4(str = 'xxxxxxxx') { + if(str=='all') str = 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx' + else if (typeof str == 'number') str = ''.padEnd(str,'x') + return str.replace(/[xy]/g,c=> { + const r=Math.random() * 16 | 0, v = c == 'x' ? r : (r & 0x3 | 0x8); + return v.toString(16); + }) + } + // 设置对象属性 + static setObjectAttr(obj, type) { + const id = Date.now() + obj.set({id, _customType: type}) + } + // 获取fabric-canvas json + static getCanvasJSON(canvas) { + if (canvas) { + return ( + // 返回额外属性 + canvas.toDatalessJSON([ + 'id', + '_customType', + 'perPixelTargetFind', + 'objectCaching' + ]) ?? {} + ) + } + return {} + } + // 克隆对象 + static cloneDeep(obj = {}) { + return JSON.parse(JSON.stringify(obj)) + } + /** + * Handling canvas json loaded data + * Used to initialize undo redo + * @param canvas fabric.Canvas + */ + static handleCanvasJSONLoaded(canvas){ + const this_ = this + canvas.getObjects().forEach((obj) => { + if (obj._customType === TYPES.ELEMENT_CUSTOM_TYPE.SHAPE_LINE) { + const points = obj.points + const lastControl = points.length - 1 + obj.controls = points.reduce(function (acc, point, index) { + acc['p' + index] = new fabric.Control({ + positionHandler: this_.lineUtils.polygonPositionHandler, + actionHandler: this_.lineUtils.anchorWrapper( + index > 0 ? index - 1 : lastControl, + this_.lineUtils.actionHandler + ), + actionName: 'polylineEndPoint', + pointIndex: index + }) + return acc + }, {}) + } + + if (obj._customType === TYPES.ELEMENT_CUSTOM_TYPE.SHAPE_ARROW_LINE) { + const paths = (obj).path + obj.controls = paths + .slice(0, paths.length - 4) + .reduce(function (acc, point, index) { + acc['p' + index] = new fabric.Control({ + positionHandler: this_.arrowLineUtils.pathPositionHandler, + actionHandler: this_.arrowLineUtils.anchorWrapper( + index > 0 ? index - 1 : paths.length - 5, + this_.arrowLineUtils.actionHandler + ), + actionName: 'pathEndPoint', + pointIndex: index + }) + return acc + }, {}) + } + }) + } + // 获取对象大小-宽度 + static getWidth(width, fabricVue) { + return width / fabricVue.canvas?.getZoom() ?? 1 + } +} +// ====== 自由绘画 start ======== +export class RainbowElement{ // 彩虹 + points = [] // fabric.Point[] + group // fabric.Group + constructor() { + this.group = new fabric.Group([], {perPixelTargetFind: true}) + // paintBoard.canvas?.add(group) + // 设置对象属性 {id, type} + Utils.setObjectAttr(this.group, TYPES.ELEMENT_CUSTOM_TYPE.RAINBOW) + } +} +// ====== 自由绘画 end ======== +// 自由绘画 FreeStyle ============================ +// "basic": "基础", +// "rainbow": "彩虹 ", +// "shape": "多形状", +// "material": "素材", +// "pixels": "像素", +// "multiColor": "多色", +// "text": "文字", +// "multiLine": "多线连接", +// "reticulate": "网状", +// "multiPoint": "多点连接", +// "wiggle": "波浪曲线", +// "thorn": "荆棘" +export const FreeStyle = { + // 普通画笔-basic + renderPencilBrush: (fabricVue) => { + const canvas = fabricVue?.canvas + const drawConfig = fabricVue?.drawConfig + const pencilBrush = new fabric.PencilBrush(canvas) + canvas.isDrawingMode = true + canvas.freeDrawingBrush = pencilBrush + canvas.freeDrawingBrush.width = Utils.getWidth(drawConfig.drawWidth, fabricVue) + canvas.freeDrawingBrush.color = drawConfig.drawColors[0] + canvas.freeDrawingBrush.shadow = new fabric.Shadow({ + blur: Utils.getWidth(drawConfig.shadowWidth, fabricVue), + offsetX: 0, + offsetY: 0, + color: drawConfig.shadowColor + }) + }, + // 橡皮擦-设置为-自由绘图画笔 + renderEraserBrush: (fabricVue) => { + const canvas = fabricVue?.canvas + const drawConfig = fabricVue?.drawConfig + const eraserBrush = new fabric.EraserBrush(canvas) + canvas.isDrawingMode = true + canvas.freeDrawingBrush = eraserBrush + canvas.freeDrawingBrush.width = Utils.getWidth(drawConfig.eraserWidth, fabricVue) + canvas.freeDrawingBrush.color = '#FFF' + } +} +// 事件类 +// -- 点击事件 +export class CanvasClickEvent { + updateInkHook + FabricVue // fabric-canvas 对象 + isMouseDown = false // 是否按下鼠标 + isSpaceKeyDown = false // 是否按下空格键 + startPoint // 开始点坐标fabric.Point | undefined + currentElement = null // 当前鼠标移动会绘制元素 The current mouse move draws the element + mouseDownTime = 0 // 鼠标按下时间 + autoDrawInk = [[], [], []] // 自动绘制 google auto draw ink Array> + isDrawBasic = false // 是否默认绘制 + + constructor(fabricVue) { + this.FabricVue = fabricVue + this.initCanvasEvent() + } + // 初始化 + initCanvasEvent() { + const canvas = this.FabricVue?.canvas + if (!canvas) throw new Error('canvas is null') + // canvas?.on('mouse:down', e => this.mouseDown(e)) // 事件:按下鼠标 + // canvas?.on('mouse:move', e => this.mouseMove(e)) // 事件:移动鼠标 + // canvas?.on('mouse:up', e => this.mouseUp(e)) // 事件:抬起鼠标 + // canvas?.on('mouse:dblclick', e => this.mouseDblclick(e)) // 事件:双击鼠标 + canvas?.on('mouse:down', this.mouseDown.bind(this) ) // 事件:按下鼠标 + canvas?.on('mouse:move', this.mouseMove.bind(this)) // 事件:移动鼠标 + canvas?.on('mouse:up', this.mouseUp.bind(this)) // 事件:抬起鼠标 + canvas?.on('mouse:dblclick', this.mouseDblclick.bind(this)) // 事件:双击鼠标 + } + // 事件:按下鼠标 + mouseDown(e) { + this.isMouseDown = true + if (this.isSpaceKeyDown) return + this.startPoint = e.absolutePointer + let currentElement = null + const { boardConfig, drawConfig } = this.FabricVue || {} + const isDraw = boardConfig.mode === TYPES.ActionMode.DRAW + if (isDraw) { + if (boardConfig.drawType == TYPES.DrawType.Shape) { // 形状绘画 + // switch (useShapeStore.shapeStyle) { + // case ShapeStyle.Rect: + // currentElement = new RectShape(e.absolutePointer) + // break + // case ShapeStyle.Circle: + // currentElement = new CircleShape(e.absolutePointer) + // break + // case ShapeStyle.Line: + // currentElement = new LineShape(e.absolutePointer) + // break + // case ShapeStyle.Ellipse: + // currentElement = new EllipseShape(e.absolutePointer) + // break + // case ShapeStyle.Triangle: + // currentElement = new TriangleShape(e.absolutePointer) + // break + // case ShapeStyle.ArrowLine: + // currentElement = new ArrowLineShape(e.absolutePointer) + // break + // case ShapeStyle.ArrowOutline: + // currentElement = new ArrowOutlineShape(e.absolutePointer) + // break + // case ShapeStyle.Cloud: + // currentElement = new CloudShape(e.absolutePointer) + // break + // case ShapeStyle.Tooltips: + // currentElement = new TooltipsShape(e.absolutePointer) + // break + // case ShapeStyle.Lightning: + // currentElement = new LightningShape(e.absolutePointer) + // break + // case ShapeStyle.Close: + // currentElement = new CloseShape(e.absolutePointer) + // break + // case ShapeStyle.Check: + // currentElement = new CheckShap(e.absolutePointer) + // break + // case ShapeStyle.Info: + // currentElement = new InfoShape(e.absolutePointer) + // break + // case ShapeStyle.Backspace: + // currentElement = new BackspaceShape(e.absolutePointer) + // break + // case ShapeStyle.Block: + // currentElement = new BlockShap(e.absolutePointer) + // break + // case ShapeStyle.Speaker: + // currentElement = new SpeakerShape(e.absolutePointer) + // break + // case ShapeStyle.Search: + // currentElement = new SearchShape(e.absolutePointer) + // break + // case ShapeStyle.InfoOutline: + // currentElement = new InfoOutlineShape(e.absolutePointer) + // break + // case ShapeStyle.Heart: + // currentElement = new HeartShape(e.absolutePointer) + // break + // case ShapeStyle.Alert: + // currentElement = new AlertShape(e.absolutePointer) + // break + // default: + // break + // } + } else if (boardConfig.drawType == TYPES.DrawType.FreeStyle) { // 自由绘画 + switch (drawConfig.drawStyle){ + // case TYPES.DrawStyle.Shape: // 多形状 + // currentElement = new ShapeElement() + // break + // case TYPES.DrawStyle.Pixels: // 像素 + // currentElement = new PixelsElement() + // break + // case TYPES.DrawStyle.Text: // 文本 + // currentElement = new DrawTextElement() + // break + // case TYPES.DrawStyle.MultiLine: // 多线 + // currentElement = new MultiLineElement() + // break + // case TYPES.DrawStyle.Reticulate: // 网状 + // currentElement = new ReticulateElement() + // break + // case TYPES.DrawStyle.Rainbow: // 彩虹 + // currentElement = new RainbowElement() + // break + // case TYPES.DrawStyle.Thorn: // 荆棘 + // currentElement = new ThornElement() + // break + // case TYPES.DrawStyle.MultiPoint: // 多点 + // currentElement = new MultiPointElement() + // break + // case TYPES.DrawStyle.Wiggle: // 波浪 + // currentElement = new WiggleElement() + // break + case TYPES.DrawStyle.Basic: // 基础画笔 + // FreeStyle.renderPencilBrush(this.FabricVue) + this.isDrawBasic = true + break + } + } + } + this.currentElement = currentElement + } + // 事件:移动鼠标 + mouseMove(e) { + if (this.isMouseDown) { + // 按下空格键,拖动画布,停止绘画。Press space, drag the canvas, stop drawing. + if (this.isSpaceKeyDown) { + canvas.relativePan(new fabric.Point(e.e.movementX, e.e.movementY)) + return + } + // 两个手指拖动,禁止绘画 two touch disabled drawing on mobile + if (this.FabricVue.evnet?.touchEvent?.isTwoTouch) return + const { boardConfig } = this.FabricVue||{} + const isDraw = boardConfig.mode === TYPES.ActionMode.DRAW && !!this.currentElement + if (isDraw) this.currentElement.addPosition(e.absolutePointer) + } + } + // 事件:抬起鼠标 + mouseUp(e) { + this.isMouseDown = false + if (this.currentElement) { + const { x: startX, y: startY } = this.startPoint + const { x: endX, y: endY } = e.absolutePointer + if (startX === endX && startY === endY) { // 无变化 + this.currentElement.destroy() + } + const isLine = this.currentElement instanceof LineShape || this.currentElement instanceof ArrowLineShape + if (isLine) this.currentElement?.mouseUp() + this.FabricVue?.history?.saveState() + } + if (this.isDrawBasic) { // 基础画笔-无element对象-但是要保存 + this.FabricVue?.history?.saveState() + } + } + // 事件:双击鼠标 + mouseDblclick(e) { + if (e?.absolutePointer) { + const { x, y } = e.absolutePointer + this.FabricVue.textElement?.loadText(x, y) + } + } + // 设置空格键状态 + setSpaceKeyDownState(isSpaceKeyDown) { + this.isSpaceKeyDown = isSpaceKeyDown + } + // 钩子:更新ink + changeInkHookFn(fn) { + this.updateInkHook = fn + } +} +// -- 主事件:canvas +export class CanvasEvent { + FabricVue // fabric-canvas 对象 + clickEvent // 点击事件 CanvasClickEvent + zoomEvent // 缩放事件 CanvasZoomEvent + objectEvent // 对象事件 ObjectEvent + windowEvent // window事件 WindowEvent + touchEvent // 触摸事件 CanvasTouchEvent + constructor(fabricVue) { + this.FabricVue = fabricVue + this.clickEvent = new CanvasClickEvent(fabricVue) + + // const zoomEvent = new CanvasZoomEvent() + // this.zoomEvent = zoomEvent + + // const objectEvent = new ObjectEvent() + // this.objectEvent = objectEvent + + // const windowEvent = new WindowEvent() + // this.windowEvent = windowEvent + + // const touchEvent = new CanvasTouchEvent() + } + // 移除事件 + removeEvent() { + this.windowEvent.removeWindowEvent() + this.touchEvent.removeTouchEvent() + } +} +// 历史类 +export class History { + FabricVue + diffs = [] + canvasData = {} + index = 0 + + constructor(fabricVue) { + this.FabricVue = fabricVue + const canvas = fabricVue.canvas + if (canvas) { + const canvasJson = Utils.getCanvasJSON(canvas) + this.canvasData = Utils.cloneDeep(canvasJson ?? {}) + } + } + // 缓存相关数据json + saveState() { + const canvas = this.FabricVue?.canvas + if (canvas) { + this.diffs = this.diffs.slice(0, this.index) + const canvasJson = Utils.getCanvasJSON() + const delta = diff(canvasJson, this.canvasData) + this.diffs.push(delta) + + // More than 50 operations, remove initial state + if (this.diffs.length > 50) { + this.diffs.shift() + } else { + this.index++ + } + this.canvasData = Utils.cloneDeep(canvasJson ?? {}) + } + } + // 撤销 + undo() { + const canvas = this.FabricVue?.canvas + if (canvas && this.index > 0) { + const delta = this.diffs[this.index - 1] + this.index-- + const canvasJson = patch(this.canvasData, delta) + // 重新加载数据 + canvas.loadFromJSON(canvasJson, () => { + Utils.handleCanvasJSONLoaded(canvas) + canvas.requestRenderAll() // 批量重绘 + this.canvasData = Utils.cloneDeep(canvasJson ?? {}) + // this.FabricVue.triggerHook() // 重新加载-自定义钩子 + // if ((delta)?.backgroundImage) { // 重新加载背景 + // handleBackgroundImageWhenCanvasSizeChange() + // } + }) + } + } + // 退回|重做 + redo() { + const canvas = this.FabricVue?.canvas + if (this.index < this.diffs.length && canvas) { + const delta = this.diffs[this.index] + this.index++ + const canvasJson = unpatch(this.canvasData, delta) + canvas.loadFromJSON(canvasJson, () => { + Utils.handleCanvasJSONLoaded(canvas) + canvas.requestRenderAll() + this.canvasData = Utils.cloneDeep(canvasJson ?? {}) + + // FabricVue.triggerHook() + // if ((delta)?.backgroundImage) { + // bgUtils.handleBackgroundImageWhenCanvasSizeChange() + // } + }) + } + } + + clean() { + this.FabricVue?.canvas?.clear() + this.index = 0 + this.diffs = [] + this.canvasData = {} + } + + initHistory() { + const canvas = this.FabricVue.canvas + if (canvas) { + const canvasJson = Utils.getCanvasJSON() + this.canvasData = canvasJson + this.index = 0 + this.diffs = [] + } + } +} +// 封装fabric-canvas export class fabricVue { - canvas = null // fabric-canvas 对象 - evnet = null // 事件对象 - history = null // 历史记录 - textElement // 文本节点 - hookFn = [] // 钩子 + canvas = null // fabric-canvas 对象 + evnet = null // 事件对象 + history = null // 历史记录 + textElement // 文本节点 + hookFn = [] // 钩子 + defConfig // 默认配置-canvas + drawConfig // 默认配置-画笔 + boardConfig // 默认配置-画板 + publicConfig // 公共属性-配置 + /** 构造函数 */ constructor() { // this.textElement = new TextElement() // 创建文本节点 + const initLanguage = ['en', 'en-US', 'en-us'].includes(navigator.language) ? 'en' : 'zh' + this.drawConfig = { // 默认配置 + drawWidth: 1, + drawColors: ['#000000'], + shadowWidth: 0, + shadowColor: '#000000', + drawTextValue: 'draw', + drawStyle: TYPES.DrawStyle.Basic, + drawShapeCount: 2, + materialType: TYPES.MATERIAL_TYPE.CRAYON, + drawShape: TYPES.DrawShape.Bubble, + eraserWidth: 20, + multiColorType: TYPES.MultiColorType.COL, + textFontFamily: 'Georgia', + openAutoDraw: false, + fontStyles: [], + } + this.boardConfig = { // 默认配置 + mode: TYPES.ActionMode.DRAW, + drawType: TYPES.DrawType.FreeStyle, + language: initLanguage, + canvasWidth: 1, + canvasHeight: 1, + backgroundColor: 'rgba(255, 255, 255, 1)', + backgroundOpacity: 1, + hasBackgroundImage: false, + backgroundImageOpacity: 1, + isObjectCaching: true, + openGuideLine: false, + } + this.defConfig = { // 默认配置-canvas + // 它用于指定选中对象时显示的选择框的颜色 + selectionColor: 'rgba(101, 204, 138, 0.3)', + // 用于控制在组合(group)对象时是否保留每个对象的堆叠顺序 + preserveObjectStacking: true, + // 它用于控制是否在高分辨率显示器(例如 Retina 显示器)上启用图像的像素级缩放 + enableRetinaScaling: true, + // 锁定背景,不受缩放影响 false + backgroundVpt: false, + // 默认全屏模式 + width: window.innerWidth, + height: window.innerHeight, + } + this.publicConfig = { // 公共属性配置 + Object: { // 默认配置-对象上 + // 设置对象边框的颜色(Rect、Circle、Ellipse、Path) + borderColor: '#65CC8A', + // 设置对象角落的颜色(Rect、RoundRect、Circle 和 Ellipse 类型的对象) + cornerColor: '#65CC8A', + // 设置角落的形状(Rect、RoundRect、Circle 和 Ellipse 类型的对象) + // circle rect round [圆形,直角(默认),圆角] + cornerStyle: 'circle', + // 设置边框虚线的样式(Rect、Circle、Ellipse、Path) + borderDashArray: [3, 3], + // 矩形(Rect)、圆角矩形(RoundRect)、圆形(Circle)和椭圆形(Ellipse)对象的角落是否透明 + transparentCorners: false + }, + Line: { + // 设置边缘连接方式为 miter bevel round|尖角(默认) 斜角切割线 圆形连接 + strokeLineJoin: 'round', + // 设置结束连接方式为 butt round square|直角(默认) 圆形 正方形 + strokeLineCap: 'round' + } + } } /** * 初始化canvas @@ -29,189 +779,80 @@ export class fabricVue { * @returns boolean 是否初始化成功 */ initCanvas(canvasEl, option = {}) { - // const useBoardStore = fabricStore.useBoardStore() - return new Promise(async (resolve) => { + return new Promise(async(resolve) => { + if (!canvasEl) resolve(false) this.canvas = new fabric.Canvas(canvasEl, { - selectionColor: 'rgba(101, 204, 138, 0.3)', - preserveObjectStacking: true, - enableRetinaScaling: true, - backgroundVpt: false, - ...option + ...this.defConfig, ...option }) - fabric.Object.prototype.set({ - borderColor: '#65CC8A', - cornerColor: '#65CC8A', - cornerStyle: 'circle', - borderDashArray: [3, 3], - transparentCorners: false - }) - fabric.Line.prototype.strokeLineJoin = 'round' - fabric.Line.prototype.strokeLineCap = 'round' - - // if (isMobile()) { - // brushMouseMixin.initCanvas(this.canvas) - // } - // // 创建辅助线 - // alignGuideLine.init(this.canvas, useBoardStore.openGuideLine) - - this.evnet = new CanvasEvent() // 创建相关事件 - this.handleMode() - - await this.initCanvasStorage() - + fabric.Object.prototype.set(this.publicConfig.Object) + fabric.Line.prototype.set(this.publicConfig.Line) + this.evnet = new CanvasEvent(this) // 创建相关事件 + this.history = new History(this) // 创建历史记录 + // await this.initCanvasStorage() + this.handleMode() // 默认模式 resolve(true) }) } /** - * 销毁canvas + * handle mode of operation + * @param mode current mode */ - removeCanvas() { - if (this.canvas) { - this?.canvas?.dispose() - this.evnet?.removeEvent() - this.canvas = null - } - } - /** - * Initialize the canvas cache - * @description 这里可以做一下初始加载json数据的操作 - */ - initCanvasStorage() { - const useBoardStore = fabricStore.useBoardStore() - const useFileStore = fabricStore.useFileStore() - return new Promise((resolve) => { - setTimeout(() => { - const file = useFileStore.currentFile - if (file && this.canvas) { - this.canvas.clear() - this.canvas.loadFromJSON(file.boardData, () => { - if (this.canvas) { - if (file.viewportTransform) { - this.canvas.setViewportTransform(file.viewportTransform) - } - if (file?.zoom && this.canvas.width && this.canvas.height) { - this.canvas.zoomToPoint( - new fabric.Point( - this.canvas.width / 2, - this.canvas.height / 2 - ), - file.zoom - ) - } - - this.canvas.setWidth(window.innerWidth * (file?.canvasWidth || 1)) - useBoardStore.updateCanvasWidth(file?.canvasWidth || 1) - this.canvas.setHeight(window.innerHeight * (file?.canvasHeight || 1)) - useBoardStore.initBackground() - - const height = file?.canvasHeight || 1 - useBoardStore.canvasHeight = height - this.updateCanvasHeight(height) - - commUtils.handleCanvasJSONLoaded(this.canvas) - - fabric.Object.prototype.set({ - objectCaching: useBoardStore.isObjectCaching - }) - this.canvas.renderAll() - this.triggerHook() - this.history = new History() - } - resolve(true) - }) - } else { - resolve(true) - } - }, 300) - }) - } - /** - * handle mode of operation - * @param mode current mode - */ handleMode(mode) { - if (!this.canvas) { - return - } - const useDrawStore = fabricStore.useDrawStore() - const useBoardStore = fabricStore.useBoardStore() - mode = mode || useBoardStore.mode - let isDrawingMode = false - let selection = false + if (!this.canvas) return + if (!mode) mode = this.boardConfig.mode const objectSet = { selectable: false, hoverCursor: 'default' } - switch (mode) { - case TYPES.ActionMode.DRAW: - if ( - useBoardStore.drawType === TYPES.DrawType.FreeStyle && - [TYPES.DrawStyle.Basic, TYPES.DrawStyle.Material, TYPES.DrawStyle.MultiColor].includes( - useDrawStore.drawStyle - ) - ) { - isDrawingMode = true - this.handleDrawStyle() + case TYPES.ActionMode.DRAW:{ // 绘画 + const isDraw = this.boardConfig.drawType === TYPES.DrawType.FreeStyle + if (isDraw) { + switch(this.drawConfig.drawStyle) { + case TYPES.DrawStyle.Basic: + FreeStyle.renderPencilBrush(this) + break + // case TYPES.DrawStyle.Material: + // this.canvas.isDrawingMode = true + // material.render({}) + // break + // case TYPES.DrawStyle.MultiColor: + // renderMultiColor({}) + // break + default: + this.canvas.isDrawingMode = false + break + } } + break} + case TYPES.ActionMode.ERASE: // 橡皮擦 + FreeStyle.renderEraserBrush(this) this.canvas.discardActiveObject() break - case TYPES.ActionMode.ERASE: - isDrawingMode = true - this.canvas.freeDrawingBrush = new fabric.EraserBrush( - this.canvas - ) - this.canvas.freeDrawingBrush.width = fabricUtils.getEraserWidth() - this.canvas.freeDrawingBrush.color = '#FFF' - this.canvas.discardActiveObject() - break - case TYPES.ActionMode.Board: - case TYPES.ActionMode.SELECT: + case TYPES.ActionMode.Board: // 画板-属性设置 + case TYPES.ActionMode.SELECT: // 选择 objectSet.selectable = true objectSet.hoverCursor = undefined - selection = true + this.canvas.selection = true break default: break } - this.canvas.isDrawingMode = isDrawingMode - this.canvas.selection = selection fabric.Object.prototype.set(objectSet) - this.canvas.forEachObject((obj) => { if (obj._customType === TYPES.ELEMENT_CUSTOM_TYPE.I_TEXT) { obj.selectable = objectSet.selectable obj.hoverCursor = objectSet.hoverCursor } }) - - this.canvas.requestRenderAll() } - - /** - * handle draw style - */ - handleDrawStyle() { - if (!this.canvas) { - return - } - const useDrawStore = fabricStore.useDrawStore() - const drawStyle = useDrawStore.drawStyle - switch (drawStyle) { - case TYPES.DrawStyle.Basic: - renderPencilBrush() - break - case TYPES.DrawStyle.Material: - this.canvas.isDrawingMode = true - material.render({}) - break - case TYPES.DrawStyle.MultiColor: - renderMultiColor({}) - break - default: - this.canvas.isDrawingMode = false - break + // 移除画布 + removeCanvas() { + if (this.canvas) { + this?.canvas?.dispose() + this.evnet?.removeEvent() + this.canvas = null } } /** @@ -269,7 +910,7 @@ export class fabricVue { const copys = targets.map((target) => { return new Promise((resolve) => { target?.clone((cloned) => { - const id = uuidv4() + const id = Utils.uuidv4('all') cloned.set({ left: (cloned?.left || 0) + 10, top: (cloned?.top || 0) + 10, @@ -292,7 +933,7 @@ export class fabricVue { } /** * Moving active objects via fabric's bringForward method - * 通过fabric的bringForward方法移动活动对象 + * 通过fabric的bringForward方法移动活动对象之上 */ bringForWard() { const canvas = this.canvas @@ -304,10 +945,9 @@ export class fabricVue { } } } - /** * Moving active objects via fabric's sendBackwards method - * 通过fabric的sendBackwards方法移动活动对象 + * 通过fabric的sendBackwards方法移动活动对象之下 */ seendBackWard() { const canvas = this.canvas @@ -319,10 +959,7 @@ export class fabricVue { } } } - /** - * Moving active objects via fabric's bringToFront method - * 通过fabric的bringToFront方法移动活动对象 - */ + // bringForWard 方法的简写 移动活动对象之上 bringToFront() { const canvas = this.canvas if (canvas) { @@ -333,10 +970,7 @@ export class fabricVue { } } } - /** - * Moving active objects via fabric's sendToBack method - * 通过fabric的sendToBack方法移动活动对象 - */ + // seendBackWard 方法的简写 移动活动对象之下 sendToBack() { const canvas = this.canvas if (canvas) { @@ -347,7 +981,6 @@ export class fabricVue { } } } - /** * Add hook fn to trigger on update|在更新时添加钩钩fn以触发 * @param fn hook fn () => void @@ -355,7 +988,6 @@ export class fabricVue { addHookFn(fn) { this.hookFn.push(fn) } - /** * remove trigger hook fn | 移除触发钩fn * @param fn hook fn () => void @@ -375,22 +1007,7 @@ export class fabricVue { fn?.() }) } - - updateCanvasWidth(width) { - if (this.canvas) { - this.canvas.setWidth(window.innerWidth * width) - bgUtils.handleBackgroundImageWhenCanvasSizeChange() - // useFileStore.getState().updateCanvasWidth(width) - } - } - updateCanvasHeight(height) { - if (this.canvas) { - const useFileStore = fabricStore.useFileStore() - this.canvas.setHeight(window.innerHeight * height) - useFileStore.updateBoardField('canvasHeight', height) - bgUtils.handleBackgroundImageWhenCanvasSizeChange() - } - } } +export const FabricVue = new fabricVue() +export default FabricVue -export const FabricVue = new fabricVue() \ No newline at end of file diff --git a/src/renderer/src/plugins/fabric/nodes/draw/index.js b/src/renderer/src/plugins/fabric/nodes/draw/index.js deleted file mode 100644 index d4ae072..0000000 --- a/src/renderer/src/plugins/fabric/nodes/draw/index.js +++ /dev/null @@ -1,27 +0,0 @@ -import { fabric } from 'fabric' -import { FabricVue } from '@/plugins/fabric' -import * as fabricUtils from '@/plugins/fabric/utils/draw' -import * as fabricStore from '@/store/modules/draw' -const useDrawStore = fabricStore.useDrawStore() -/** - * 基础 画笔 - * @returns void - */ -export const renderPencilBrush = () => { - const canvas = FabricVue.canvas - if (!canvas) { - return - } - - const pencilBrush = new fabric.PencilBrush(canvas) - canvas.isDrawingMode = true - canvas.freeDrawingBrush = pencilBrush - canvas.freeDrawingBrush.width = fabricUtils.getDrawWidth() - canvas.freeDrawingBrush.color = useDrawStore.drawColors[0] - canvas.freeDrawingBrush.shadow = new fabric.Shadow({ - blur: fabricUtils.getShadowWidth(), - offsetX: 0, - offsetY: 0, - color: useDrawStore.shadowColor - }) -} \ No newline at end of file diff --git a/src/renderer/src/plugins/fabric/nodes/draw/material.js b/src/renderer/src/plugins/fabric/nodes/draw/material.js deleted file mode 100644 index 3fa8343..0000000 --- a/src/renderer/src/plugins/fabric/nodes/draw/material.js +++ /dev/null @@ -1,125 +0,0 @@ - -/** - * @description: 材质 - */ -import { FabricVue } from '@/plugins/fabric' -import * as TYPES from '@/constants/draw' -import * as fabricStore from '@/store/modules/draw' - -export class Material { - initPromise = null // Initialize promise - crayonImage = null - carbonImage = null - clothImage = null - oilImage = null - crayonDarkImage = null - - constructor() { - this.initMaterial() - } - - async initMaterial() { - - this.initPromise = Promise.all([ - this.loadImage(TYPES.MATERIAL_TYPE.CRAYON), - this.loadImage(TYPES.MATERIAL_TYPE.CARBON), - this.loadImage(TYPES.MATERIAL_TYPE.CLOTH), - this.loadImage(TYPES.MATERIAL_TYPE.OIL), - this.loadImage(TYPES.MATERIAL_TYPE.CRAYON_DARK) - ]) - } - - render({materialType, color}) { - const useDrawStore = fabricStore.useDrawStore() - materialType = materialType || useDrawStore.materialType - color = color || useDrawStore.drawColors[0] - this.initPromise?.then(() => { - switch (materialType) { - case TYPES.MATERIAL_TYPE.CRAYON: - this.renderMaterial(this.crayonImage, color) - break - case TYPES.MATERIAL_TYPE.CARBON: - this.renderMaterial(this.carbonImage, color) - break - case TYPES.MATERIAL_TYPE.CLOTH: - this.renderMaterial(this.clothImage, color) - break - case TYPES.MATERIAL_TYPE.OIL: - this.renderMaterial(this.oilImage, color) - break - case TYPES.MATERIAL_TYPE.CRAYON_DARK: - this.renderMaterial(this.crayonDarkImage, color) - break - default: - break - } - }) - } - - renderMaterial(materialImg, color, opacity = 1) { - if (FabricVue.canvas) { - const useDrawStore = fabricStore.useDrawStore() - const patternBrush = new fabric.PatternBrush(FabricVue.canvas) - const patternCanvas = document.createElement('canvas') - patternCanvas.width = patternCanvas.height = 100 - const context = patternCanvas.getContext('2d') - if (context) { - context.fillStyle = color - context.fillRect(0, 0, 100, 100) - if (materialImg) { - context.globalAlpha = opacity - context.drawImage(materialImg, 0, 0, 100, 100) - } - } - patternBrush.getPatternSrc = () => { - return patternCanvas - } - patternBrush.getPatternSrcFunction = () => { - return patternCanvas - } - FabricVue.canvas.freeDrawingBrush = patternBrush - FabricVue.canvas.freeDrawingBrush.width = getDrawWidth() - FabricVue.canvas.freeDrawingBrush.shadow = new fabric.Shadow({ - blur: getShadowWidth(), - offsetX: 0, - offsetY: 0, - color: useDrawStore.shadowColor - }) - } - } - - loadImage(imageName) { - return new Promise((resolve) => { - const img = new Image() - // img.src = formatPublicUrl(`pattern/${imageName}.png`) - img.src = `pattern/${imageName}.png` - img.onload = () => { - switch (imageName) { - case TYPES.MATERIAL_TYPE.CARBON: - this.carbonImage = img - break - case TYPES.MATERIAL_TYPE.CRAYON: - this.crayonImage = img - break - case TYPES.MATERIAL_TYPE.CLOTH: - this.clothImage = img - break - case TYPES.MATERIAL_TYPE.OIL: - this.oilImage = img - break - case TYPES.MATERIAL_TYPE.CRAYON_DARK: - this.crayonDarkImage = img - break - default: - break - } - resolve(true) - } - img.onerror = () => { - resolve(false) - } - }) - } -} - -export const material = new Material() \ No newline at end of file diff --git a/src/renderer/src/plugins/fabric/nodes/draw/multiColor.js b/src/renderer/src/plugins/fabric/nodes/draw/multiColor.js deleted file mode 100644 index a37195c..0000000 --- a/src/renderer/src/plugins/fabric/nodes/draw/multiColor.js +++ /dev/null @@ -1,98 +0,0 @@ -/** - * 多个材质画刷 - * @param {*} params - * @returns - */ -import { FabricVue } from '@/plugins/fabric' -import * as TYPES from '@/constants/draw' -import * as fabricUtils from '@/plugins/fabric/utils/draw' -import * as fabricStore from '@/store/modules/draw' - -const COLOR_WIDTH = 5 - -export const renderMultiColor = (params={}) => { - const canvas = FabricVue.canvas - if (!canvas) { - return - } - const useDrawStore = fabricStore.useDrawStore() - const colors = params?.colors ?? useDrawStore.drawColors - const type = params?.type ?? useDrawStore.multiColorType - - const patternBrush = new fabric.PatternBrush(canvas) - const patternCanvas = document.createElement('canvas') - const context = patternCanvas.getContext('2d') - if (context) { - switch (type) { - case TYPES.MultiColorType.COL: - renderCol(patternCanvas, context, colors) - break - case TYPES.MultiColorType.ROW: - renderRow(patternCanvas, context, colors) - break - case TYPES.MultiColorType.CIRCLE: - renderCircle(patternCanvas, context, colors) - break - default: - break - } - - patternBrush.getPatternSrc = () => { - return patternCanvas - } - patternBrush.getPatternSrcFunction = () => { - return patternCanvas - } - - canvas.isDrawingMode = true - canvas.freeDrawingBrush = patternBrush - canvas.freeDrawingBrush.width = fabricUtils.getDrawWidth() - canvas.freeDrawingBrush.shadow = new fabric.Shadow({ - blur: fabricUtils.getShadowWidth(), - offsetX: 0, - offsetY: 0, - color: useDrawStore.shadowColor - }) - } -} - -function renderCol(canvas, context, colors=[]) { - canvas.width = COLOR_WIDTH * colors.length - canvas.height = 20 - colors.forEach((color, i) => { - context.fillStyle = color - context.fillRect(COLOR_WIDTH * i, 0, COLOR_WIDTH, 20) - }) -} - -function renderRow(canvas, context, colors=[]) { - canvas.width = 20 - canvas.height = COLOR_WIDTH * colors.length - colors.forEach((color, i) => { - context.fillStyle = color - context.fillRect(0, COLOR_WIDTH * i, 20, COLOR_WIDTH) - }) -} - -function renderCircle(canvas, context, colors=[]) { - const radius = 10 - const padding = 5 - const n = colors.length - - const canvasWidth = 2 * padding + n * radius * 2 + (n - 1) * padding - canvas.width = canvasWidth - canvas.height = radius * 2 + 2 * padding - - let x = padding + radius - const y = padding + radius - - // render multi circle - for (let i = 0; i < n; i++) { - context.beginPath() - context.fillStyle = colors[i] - context.arc(x, y, radius, 0, Math.PI * 2) - context.closePath() - context.fill() - x += 2 * radius + padding - } -} \ No newline at end of file diff --git a/src/renderer/src/plugins/fabric/test.js b/src/renderer/src/plugins/fabric/test.js deleted file mode 100644 index 7f49554..0000000 --- a/src/renderer/src/plugins/fabric/test.js +++ /dev/null @@ -1,223 +0,0 @@ -/** - * @description 封装fabric js - */ -import { fabric } from 'fabric' - -// 当前使用到的常量|类型(枚举) -export class TYPES { - static ActionMode = { - DRAW: 'draw', // 画笔模式 - ERASE: 'erase', // 橡皮擦模式 - SELECT: 'select', // 选择模式 - Board: 'board' // 画板模式 - } - // 画笔类型 - static DrawType = { - FreeStyle: 'freeStyle', - Shape: 'shape' - } - // 画笔样式 - static DrawStyle = { - Basic: 'basic', - Rainbow: 'rainbow', - Shape: 'shape', - Material: 'material', - Pixels: 'pixels', - MultiColor: 'multiColor', - Text: 'text', - MultiLine: 'multiLine', - Reticulate: 'reticulate', - MultiPoint: 'multiPoint', - Wiggle: 'wiggle', - Thorn: 'thorn' - } - // 各种形状 - static DrawShape = { - Bubble: 'bubble', - Star: 'star', - Love: 'love', - Butterfly: 'butterfly', - Snow: 'snow', - Music: 'music', - Sun: 'sun', - Moon: 'moon', - Leaf: 'leaf', - Flower: 'flower' - } - // 材质类型 - static MATERIAL_TYPE = { - CRAYON: 'crayon', - CARBON: 'carbon', - CLOTH: 'cloth', - OIL: 'oil', - CRAYON_DARK: 'crayonDark' - } - // 多颜色类型 - static MultiColorType = { - COL: 'col', - ROW: 'row', - CIRCLE: 'circle' - } - // 画笔元素 - static FREESTYLE_ELEMENT_CUSTOM_TYPE = { - IMAGE: 'image', - I_TEXT: 'itext', - RAINBOW: 'rainbow', - SHAPE: 'shape', - PIXELS: 'pixels', - DRAW_TEXT: 'drawText', - MULTI_LINE: 'multiLine', - RETICULATE: 'reticulate', - MULTI_POINT: 'multiPoint', - WIGGLE: 'wiggle', - THORN: 'thorn' - } - // 形状元素 - static SHAPE_ELEMENT_CUSTOM_TYPE = { - SHAPE_LINE: 'shapeLine', - SHAPE_RECT: 'shapeRect', - SHAPE_CIRCLE: 'shapeCircle', - SHAPE_ELLIPSE: 'shapeEllipse', - SHAPE_TRIANGLE: 'shapeTriangle', - SHAPE_ARROW_LINE: 'shapeArrowLine', - SHAPE_ARROW_OUTLINE: 'shapeArrowOutline', - SHAPE_CLOUD: 'shapeCloud', - SHAPE_TOOLTIPS: 'shapeTooltips', - SHAPE_LIGHTNING: 'shapeLightning', - SHAPE_CLOSE: 'shapeClose', - SHAPE_CHECK: 'shapeCheck', - SHAPE_INFO: 'shapeInfo', - SHAPE_BACKSPACE: 'shapeBackspace', - SHAPE_BLOCK: 'shapeBlock', - SHAPE_SPEAKER: 'shapeSpeaker', - SHAPE_SEARCH: 'shapeSearch', - SHAPE_INFO_OUTLINE: 'shapeInfoOutline', - SHAPE_HEART: 'shapeHeart', - SHAPE_ALERT: 'shapeAlert' - } - // 元素类型 - static ELEMENT_CUSTOM_TYPE = { - ...this.FREESTYLE_ELEMENT_CUSTOM_TYPE, - ...this.SHAPE_ELEMENT_CUSTOM_TYPE - } -} -// 自由绘画 FreeStyle -// export class FreeStyle { -// } -export const FreeStyle = { - renderPencilBrush: (canvas) => { - const pencilBrush = new fabric.PencilBrush(canvas) - canvas.isDrawingMode = true - canvas.freeDrawingBrush = pencilBrush - canvas.freeDrawingBrush.width = fabricUtils.getDrawWidth() - canvas.freeDrawingBrush.color = useDrawStore.drawColors[0] - canvas.freeDrawingBrush.shadow = new fabric.Shadow({ - blur: fabricUtils.getShadowWidth(), - offsetX: 0, - offsetY: 0, - color: useDrawStore.shadowColor - }) - } -} -// 封装fabric-canvas -export class fabricVue { - canvas = null // fabric-canvas 对象 - evnet = null // 事件对象 - history = null // 历史记录 - textElement // 文本节点 - hookFn = [] // 钩子 - defConfig // 默认配置-canvas - drawConfig // 默认配置-画笔 - boardConfig // 默认配置-画板 - publicConfig // 公共属性-配置 - - /** 构造函数 */ - constructor() { - // this.textElement = new TextElement() // 创建文本节点 - const initLanguage = ['en', 'en-US', 'en-us'].includes(navigator.language) ? 'en' : 'zh' - this.drawConfig = { // 默认配置 - drawWidth: 1, - drawColors: ['#000000'], - shadowWidth: 0, - shadowColor: '#000000', - drawTextValue: 'draw', - drawStyle: TYPES.DrawStyle.Basic, - drawShapeCount: 2, - materialType: TYPES.MATERIAL_TYPE.CRAYON, - drawShape: TYPES.DrawShape.Bubble, - eraserWidth: 20, - multiColorType: TYPES.MultiColorType.COL, - textFontFamily: 'Georgia', - openAutoDraw: false, - fontStyles: [], - } - this.boardConfig = { // 默认配置 - mode: TYPES.ActionMode.DRAW, - drawType: TYPES.DrawType.FreeStyle, - language: initLanguage, - canvasWidth: 1, - canvasHeight: 1, - backgroundColor: 'rgba(255, 255, 255, 1)', - backgroundOpacity: 1, - hasBackgroundImage: false, - backgroundImageOpacity: 1, - isObjectCaching: true, - openGuideLine: false, - } - this.defConfig = { // 默认配置-canvas - // 它用于指定选中对象时显示的选择框的颜色 - selectionColor: 'rgba(101, 204, 138, 0.3)', - // 用于控制在组合(group)对象时是否保留每个对象的堆叠顺序 - preserveObjectStacking: true, - // 它用于控制是否在高分辨率显示器(例如 Retina 显示器)上启用图像的像素级缩放 - enableRetinaScaling: true, - // 锁定背景,不受缩放影响 false - backgroundVpt: false, - // 默认全屏模式 - width: window.innerWidth, - height: window.innerHeight, - } - this.publicConfig = { // 公共属性配置 - Object: { // 默认配置-对象上 - // 设置对象边框的颜色(Rect、Circle、Ellipse、Path) - borderColor: '#65CC8A', - // 设置对象角落的颜色(Rect、RoundRect、Circle 和 Ellipse 类型的对象) - cornerColor: '#65CC8A', - // 设置角落的形状(Rect、RoundRect、Circle 和 Ellipse 类型的对象) - // circle rect round [圆形,直角(默认),圆角] - cornerStyle: 'circle', - // 设置边框虚线的样式(Rect、Circle、Ellipse、Path) - borderDashArray: [3, 3], - // 矩形(Rect)、圆角矩形(RoundRect)、圆形(Circle)和椭圆形(Ellipse)对象的角落是否透明 - transparentCorners: false - }, - Line: { - // 设置边缘连接方式为 miter bevel round|尖角(默认) 斜角切割线 圆形连接 - strokeLineJoin: 'round', - // 设置结束连接方式为 butt round square|直角(默认) 圆形 正方形 - strokeLineCap: 'round' - } - } - } - /** - * 初始化canvas - * @param {*} canvasEl canvas元素 - * @returns boolean 是否初始化成功 - */ - initCanvas(canvasEl, option = {}) { - return new Promise(async(resolve) => { - if (!canvasEl) resolve(false) - this.canvas = new fabric.Canvas(canvasEl, { - ...this.defConfig, ...option - }) - // this.evnet = new CanvasEvent() // 创建相关事件 - // this.history = new History() // 创建历史记录 - // await this.initCanvasStorage() - resolve(true) - }) - } - -} -export const FabricVue = new fabricVue() -export default FabricVue - diff --git a/src/renderer/src/plugins/fabric/utils/bg.js b/src/renderer/src/plugins/fabric/utils/bg.js deleted file mode 100644 index 61dfd60..0000000 --- a/src/renderer/src/plugins/fabric/utils/bg.js +++ /dev/null @@ -1,77 +0,0 @@ - -/** - * 工具:背景相关的颜色、背景图片 - */ -import { fabric } from 'fabric' -import { FabricVue } from '@/plugins/fabric' -import * as fabricStore from '@/store/modules/draw' - - -/** - * 事件: 缩放变化-更新背景图片 - */ -export const handleBackgroundImageWhenCanvasSizeChange = (isRender = true) => { - const backgroundImage = FabricVue?.canvas?.backgroundImage - if (backgroundImage) { - setBackgroundImage(backgroundImage) - if (isRender) { - FabricVue.canvas?.requestRenderAll() - } - } -} -/** - * 更新 fabric 加载画布背景图片 - * @param {*} data string - * @returns - */ -export const updateCanvasBackgroundImage = (data) => { - const canvas = FabricVue.canvas - if (!canvas) { - return - } - fabric.Image.fromURL( - data, - (image) => { - setBackgroundImage(image) - - canvas.setBackgroundImage(image, () => { - FabricVue.render() - }) - }, - {crossOrigin: 'anonymous'} - ) -} -/** - * 设置-更新背景图片 - * @param {*} image fabric.Image - * @returns - */ -export const setBackgroundImage = (image) => { - const canvas = FabricVue?.canvas - if (!canvas) { - return - } - const useBoardStore = fabricStore.useBoardStore() - const canvasWidth = canvas.getWidth() - const canvasHeight = canvas.getHeight() - - const imgWidth = image.width - const imgHeight = image.height - - const scaleWidth = canvasWidth / imgWidth - const scaleHeight = canvasHeight / imgHeight - - const scale = Math.min(scaleWidth, scaleHeight) - image.scale(scale) - - const imgLeft = canvasWidth / 2 - (imgWidth * scale) / 2 - const imgTop = canvasHeight / 2 - (imgHeight * scale) / 2 - - image.set({ - left: imgLeft, - top: imgTop, - originX: 'left', - originY: 'top', - opacity: useBoardStore.backgroundImageOpacity - }) -} \ No newline at end of file diff --git a/src/renderer/src/plugins/fabric/utils/color.js b/src/renderer/src/plugins/fabric/utils/color.js deleted file mode 100644 index 6e4350e..0000000 --- a/src/renderer/src/plugins/fabric/utils/color.js +++ /dev/null @@ -1,82 +0,0 @@ -/** - * hexToRgba - * @param hex hexadecimal color - * @param alpha - * @returns rgba - */ -export function hexToRgba(hex, alpha = 1) { - const bigint = parseInt(hex.slice(1), 16) - const r = (bigint >> 16) & 255 - const g = (bigint >> 8) & 255 - const b = bigint & 255 - return `rgba(${r}, ${g}, ${b}, ${alpha})` -} - -export function rgbaToHex(rgba) { - if (!rgba) { - return rgba - } - - const values = rgba.match(/\d+/g) - const hex = `#${( - (1 << 24) + - (parseInt(values[0]) << 16) + - (parseInt(values[1]) << 8) + - parseInt(values[2]) - ) - .toString(16) - .slice(1)}` - return hex -} - -/** - * get rgba alpha - * @param rgbaString rgba color - * @returns - */ -export function getAlphaFromRgba(rgbaString) { - const match = rgbaString.match(/rgba?\((\d+),\s*(\d+),\s*(\d+),\s*([\d.]+)\)/) - - if (match) { - return parseFloat(match[4]) - } else { - return 1 - } -} - -/** - * Get the rgba of the new alpha - * @param rgbaColor - * @param newAlpha - * @returns newRgbaColor - */ -export function changeAlpha(rgbaColor, newAlpha) { - const match = rgbaColor.match(/rgba?\((.*?)\)/) - if (!match) { - return rgbaColor - } - - const rgbPart = match[1].split(',').map((i) => i.trim()) - newAlpha = Math.min(1, Math.max(0, newAlpha)) - - const newRgbaColor = `rgba(${rgbPart[0]}, ${rgbPart[1]}, ${rgbPart[2]}, ${newAlpha})` - return newRgbaColor -} - -export function isHexColor(color) { - return /^#([0-9A-Fa-f]{3}){1,2}$/.test(color) -} - -export function isRgbaColor(color) { - return /^rgba?\((\d+,\s*){2}\d+,\s*(0(\.\d+)?|1(\.0)?)\)$/.test(color) -} - -export function getColorFormat(color) { - if (isHexColor(color)) { - return 'hex' - } else if (isRgbaColor(color)) { - return 'rgba' - } else { - return 'unknown' - } -} diff --git a/src/renderer/src/plugins/fabric/utils/draw.js b/src/renderer/src/plugins/fabric/utils/draw.js deleted file mode 100644 index 459cb7c..0000000 --- a/src/renderer/src/plugins/fabric/utils/draw.js +++ /dev/null @@ -1,33 +0,0 @@ -import * as fabricStore from '@/store/modules/draw' -import { FabricVue } from "@/plugins/fabric"; -// import { v4 as uuidv4 } from 'uuid' -const useDrawStore = fabricStore.useDrawStore() - -export const getDrawWidth = (width) => { - return ( - (width ?? useDrawStore.drawWidth) / - (FabricVue.canvas?.getZoom() ?? 1) - ) -} - -export const getEraserWidth = (width) => { - return ( - (width ?? useDrawStore.eraserWidth) / - (FabricVue.canvas?.getZoom() ?? 1) - ) -} - -export const getShadowWidth = (width) => { - return ( - (width ?? useDrawStore.shadowWidth) / - (FabricVue.canvas?.getZoom() ?? 1) - ) -} - -// export const setObjectAttr = (obj, type) => { -// const id = uuidv4() -// obj.set({ -// id, -// _customType: type -// }) -// } diff --git a/src/renderer/src/plugins/fabric/utils/index.js b/src/renderer/src/plugins/fabric/utils/index.js deleted file mode 100644 index 0d98900..0000000 --- a/src/renderer/src/plugins/fabric/utils/index.js +++ /dev/null @@ -1,165 +0,0 @@ -/** - * 工具类 - */ -import { FabricVue } from '@/plugins/fabric' -import * as TYPES from '@/constants/draw' -import * as lineUtils from '@/plugins/fabric/utils/shape/line' -import * as arrowLineUtils from '@/plugins/fabric/utils/shape/arrowLine' - -/** - * compare version - * @param v1 - * @param v2 - * @returns 0: v1 === v2; 1: v1 > v2; -1: v1 < v2 - */ -export const compareVersion = (v1, v2) => { - const v1s = v1.split('.') - const v2s = v2.split('.') - const len = Math.max(v1s.length, v2s.length) - - while (v1s.length < len) { - v1s.push('0') - } - while (v2s.length < len) { - v1s.push('0') - } - - for (let i = 0; i < len; i++) { - const num1 = parseInt(v1s[i]) - const num2 = parseInt(v2s[i]) - - if (num1 > num2) { - return 1 - } else if (num1 < num2) { - return -1 - } - } - return 0 -} - -/** - * get random integer - * @param min min range - * @param max max range - */ -export const getRandomInt = (min, max) => { - return Math.floor(Math.random() * (max - min + 1)) + min -} - -/** - * get random float - * @param min min range - * @param max max range - */ -export const getRandomFloat = (min, max) => { - return Math.random() * (max - min) + min -} - -/** - * handle static file paths in the public folder - * @param originUrl - * @returns publicDir + originUrl - */ -export const formatPublicUrl = (originUrl) => { - if (originUrl && typeof originUrl === 'string') { - return `${import.meta.env.BASE_URL}${originUrl}` - } - return '' -} - -/** - * get the distance between two points - * @param start - * @param end - * @returns distance - */ -export const getDistance = (start, end) => { - return Math.sqrt(Math.pow(start.x - end.x, 2) + Math.pow(start.y - end.y, 2)) -} - -/** - * is it mobile - */ -export const isMobile = () => { - return /phone|pad|pod|iPhone|iPod|ios|iPad|Android|Mobile|BlackBerry|IEMobile|MQQBrowser|JUC|Fennec|wOSBrowser|BrowserNG|WebOS|Symbian|Windows Phone|Mobi|Android|iPhone|iPad/i.test( - navigator.userAgent - ) -} -// 生成UUID -export const getUUID = () => { - const timestamp = Date.now(); - // 时间戳部分 - const timestampPart = timestamp.toString(16).padStart(12, '0'); - - // 生成随机数 - const rnd = crypto.getRandomValues(new Uint8Array(4)); - const randomPart = rnd.map(byte => byte.toString(16).padStart(2, '0')).join(''); - - // 应用变体和版本标识符 - const version = (timestamp >= 0x0100000000000000) ? (timestamp & 0x00000000FFFF0000) : 0x4000; - const variant = (rnd[0] & 0x0F) | 0x80; - - // 合并所有部分并返回 - return `${timestampPart}-${randomPart}-${version.toString(16).padStart(4, '0')}-${variant.toString(16).padStart(4, '0')}-${rnd[3].toString(16).padStart(4, '0')}`; -} - -/** loadCanvas */ -export const getCanvasJSON = () => { - const canvas = FabricVue?.canvas - if (canvas) { - return ( - // 返回额外属性 - canvas.toDatalessJSON([ - 'id', - '_customType', - 'perPixelTargetFind', - 'objectCaching' - ]) ?? {} - ) - } - return {} -} - -/** - * Handling canvas json loaded data - * Used to initialize undo redo - * @param canvas fabric.Canvas - */ -export const handleCanvasJSONLoaded = (canvas) => { - canvas.getObjects().forEach((obj) => { - if (obj._customType === TYPES.ELEMENT_CUSTOM_TYPE.SHAPE_LINE) { - const points = obj.points - const lastControl = points.length - 1 - obj.controls = points.reduce(function (acc, point, index) { - acc['p' + index] = new fabric.Control({ - positionHandler: lineUtils.polygonPositionHandler, - actionHandler: lineUtils.anchorWrapper( - index > 0 ? index - 1 : lastControl, - lineUtils.actionHandler - ), - actionName: 'polylineEndPoint', - pointIndex: index - }) - return acc - }, {}) - } - - if (obj._customType === TYPES.ELEMENT_CUSTOM_TYPE.SHAPE_ARROW_LINE) { - const paths = (obj).path - obj.controls = paths - .slice(0, paths.length - 4) - .reduce(function (acc, point, index) { - acc['p' + index] = new fabric.Control({ - positionHandler: arrowLineUtils.pathPositionHandler, - actionHandler: arrowLineUtils.anchorWrapper( - index > 0 ? index - 1 : paths.length - 5, - arrowLineUtils.actionHandler - ), - actionName: 'pathEndPoint', - pointIndex: index - }) - return acc - }, {}) - } - }) -} \ No newline at end of file diff --git a/src/renderer/src/plugins/fabric/utils/shape/arrowLine.js b/src/renderer/src/plugins/fabric/utils/shape/arrowLine.js deleted file mode 100644 index 1c3a0cf..0000000 --- a/src/renderer/src/plugins/fabric/utils/shape/arrowLine.js +++ /dev/null @@ -1,164 +0,0 @@ -import { fabric } from 'fabric' - -export function calculateArrowSlidePath( - paths, - startX, - startY, - endX, - endY -) { - const angleRad = Math.atan2(endY - startY, endX - startX) - const arrowWidthHalf = 10 - - const arrowLeftX = endX - arrowWidthHalf * Math.cos(angleRad - Math.PI / 6) - const arrowLeftY = endY - arrowWidthHalf * Math.sin(angleRad - Math.PI / 6) - const arrowRightX = endX - arrowWidthHalf * Math.cos(angleRad + Math.PI / 6) - const arrowRightY = endY - arrowWidthHalf * Math.sin(angleRad + Math.PI / 6) - - paths[paths.length - 4][1] = endX - paths[paths.length - 4][2] = endY - paths[paths.length - 3][1] = arrowLeftX - paths[paths.length - 3][2] = arrowLeftY - paths[paths.length - 2][1] = endX - paths[paths.length - 2][2] = endY - paths[paths.length - 1][1] = arrowRightX - paths[paths.length - 1][2] = arrowRightY - - return paths -} - -/** - * - * @param {*} object fabric.Path - * @returns - */ -export function getObjectSizeWithStroke(object) { - const stroke = new fabric.Point( - object.strokeUniform ? 1 / (object.scaleX) : 1, - object.strokeUniform ? 1 / (object.scaleY) : 1 - ).multiply(object.strokeWidth) - return new fabric.Point( - (object.width) + stroke.x, - (object.height) + stroke.y - ) -} - -/** - * - * @param {*} this fabric.Control - * @param {*} dim - * @param {*} finalMatrix - * @param {*} fabricObject fabric.Path - * @returns fabric.Control['positionHandler'] - */ -export const pathPositionHandler = function ( - fabricControl, - dim, - finalMatrix, - fabricObject -) { - const paths = fabricObject.path - const x = paths[fabricControl.pointIndex][1] - fabricObject.pathOffset.x - const y = paths[fabricControl.pointIndex][2] - fabricObject.pathOffset.y - return fabric.util.transformPoint( - { x, y }, - fabric.util.multiplyTransformMatrices( - fabricObject.canvas?.viewportTransform, - fabricObject.calcTransformMatrix() - ) - ) -} - -/** - * - * @param {*} eventData - * @param {*} transform - * @param {*} x - * @param {*} y - * @returns fabric.Control['actionHandler'] - */ -export const actionHandler = function ( - eventData, - transform, - x, - y -) { - const pathObj = transform.target, - currentControl = pathObj.controls[pathObj.__corner], - mouseLocalPosition = pathObj.toLocalPoint( - new fabric.Point(x, y), - 'center', - 'center' - ), - pathBaseSize = getObjectSizeWithStroke(pathObj), - size = pathObj._getTransformedDimensions(0, 0), - finalPointPosition = { - x: - (mouseLocalPosition.x * pathBaseSize.x) / size.x + pathObj.pathOffset.x, - y: (mouseLocalPosition.y * pathBaseSize.y) / size.y + pathObj.pathOffset.y - } - - const path = pathObj.path - path[currentControl.pointIndex][1] = finalPointPosition.x - path[currentControl.pointIndex][2] = finalPointPosition.y - - if ( - currentControl.pointIndex === path?.length - 5 || - currentControl.pointIndex === path?.length - 6 - ) { - const point1 = path[path.length - 6] - const point2 = path[path.length - 5] - const newPath = calculateArrowSlidePath( - path, - point1[1], - point1[2], - point2[1], - point2[2] - ) - pathObj._setPath(newPath) - } - - return true -} - -/** - * - * @param {*} anchorIndex - * @param {*} fn fabric.Control['actionHandler'] - * @returns MouseEvent fabric.Transform - */ -export function anchorWrapper( - anchorIndex, - fn -) { - return function ( - eventData, - transform, - x, - y - ) { - const fabricObject = transform.target - const paths = fabricObject.path - const absolutePoint = fabric.util.transformPoint( - { - x: paths[anchorIndex][1] - fabricObject.pathOffset.x, - y: paths[anchorIndex][2] - fabricObject.pathOffset.y - }, - fabricObject.calcTransformMatrix() - ) - const actionPerformed = fn(eventData, transform, x, y) - fabricObject._setPath(fabricObject.path) - const pathBaseSize = getObjectSizeWithStroke(fabricObject) - - const newX = - (paths[anchorIndex][1] - fabricObject.pathOffset.x) / pathBaseSize.x - const newY = - (paths[anchorIndex][2] - fabricObject.pathOffset.y) / pathBaseSize.y - fabricObject.setPositionByOrigin( - absolutePoint, - (newX + 0.5), - (newY + 0.5) - ) - return actionPerformed - } -} diff --git a/src/renderer/src/plugins/fabric/utils/shape/line.js b/src/renderer/src/plugins/fabric/utils/shape/line.js deleted file mode 100644 index e63fd59..0000000 --- a/src/renderer/src/plugins/fabric/utils/shape/line.js +++ /dev/null @@ -1,109 +0,0 @@ -/** - * 线 - */ -import { fabric } from 'fabric' - -/** - * - * @param {*} object fabric.Polyline - * @returns - */ -export function getObjectSizeWithStroke(object) { - const stroke = new fabric.Point( - object.strokeUniform ? 1 / (object.scaleX) : 1, - object.strokeUniform ? 1 / (object.scaleY) : 1 - ).multiply(object.strokeWidth) - return new fabric.Point( - (object.width) + stroke.x, - (object.height) + stroke.y - ) -} - -/** - * - * @param {*} this fabric.Control - * @param {*} dim - * @param {*} finalMatrix - * @param {*} fabricObject fabric.Polyline - * @returns - */ -export const polygonPositionHandler = function ( - fabricControl, dim, finalMatrix,fabricObject) { - const points = fabricObject.points - const x = points[fabricControl.pointIndex].x - fabricObject.pathOffset.x - const y = points[fabricControl.pointIndex].y - fabricObject.pathOffset.y - return fabric.util.transformPoint( - { x, y }, - fabric.util.multiplyTransformMatrices( - fabricObject.canvas?.viewportTransform, - fabricObject.calcTransformMatrix() - ) - ) -} - -/** - * - * @param {*} eventData - * @param {*} transform - * @param {*} x - * @param {*} y - * @returns fabric.Control['actionHandler'] - */ -export const actionHandler = function ( - eventData, transform,x, y) { - const polygon = transform.target, - currentControl = polygon.controls[polygon.__corner], - mouseLocalPosition = polygon.toLocalPoint( - new fabric.Point(x, y), - 'center', - 'center' - ), - polygonBaseSize = getObjectSizeWithStroke(polygon), - size = polygon._getTransformedDimensions(0, 0), - finalPointPosition = { - x: - (mouseLocalPosition.x * polygonBaseSize.x) / size.x + - polygon.pathOffset.x, - y: - (mouseLocalPosition.y * polygonBaseSize.y) / size.y + - polygon.pathOffset.y - } - - const points = polygon.points - points[currentControl.pointIndex] = finalPointPosition - - return true -} -/** - * - * @param {*} anchorIndex number - * @param {*} fn fabric.Control['actionHandler'] - * @returns function (eventData, transform, x, y ) | MouseEvent fabric.Transform number number - */ -export function anchorWrapper(anchorIndex, fn) { - return function (eventData, transform, x, y ) { - const fabricObject = transform.target - const points = fabricObject.points - const absolutePoint = fabric.util.transformPoint( - { - x: points[anchorIndex].x - fabricObject.pathOffset.x, - y: points[anchorIndex].y - fabricObject.pathOffset.y - }, - fabricObject.calcTransformMatrix() - ) - const actionPerformed = fn(eventData, transform, x, y) - fabricObject._setPositionDimensions({}) - const polygonBaseSize = getObjectSizeWithStroke(fabricObject) - - const newX = - (points[anchorIndex].x - fabricObject.pathOffset.x) / polygonBaseSize.x - const newY = - (points[anchorIndex].y - fabricObject.pathOffset.y) / polygonBaseSize.y - fabricObject.setPositionByOrigin( - absolutePoint, - (newX + 0.5), - (newY + 0.5) - ) - return actionPerformed - } -} \ No newline at end of file diff --git a/src/renderer/src/store/modules/draw.js b/src/renderer/src/store/modules/draw.js deleted file mode 100644 index bf486e1..0000000 --- a/src/renderer/src/store/modules/draw.js +++ /dev/null @@ -1,295 +0,0 @@ -/** - * @description fabric.js 缓存画板 - */ - -import { defineStore } from "pinia"; -import * as TYPES from "@/constants/draw"; -import { FabricVue } from "@/plugins/fabric"; -import * as fabricUtils from '@/plugins/fabric/utils/draw' -import * as colorUtils from '@/plugins/fabric/utils/color' -import * as bgUtils from '@/plugins/fabric/utils/bg' -import * as commUtils from '@/plugins/fabric/utils' -import { material } from '@/plugins/fabric/nodes/draw/material' -import { renderMultiColor } from '@/plugins/fabric/nodes/draw/multiColor' - -// 画板模块-缓存 json相关 -export const BOARD_VERSION = '1.0.0' - -const getFileObj = (data) => ({ - id: commUtils.getUUID(), - title: 'paint-board', - boardVersion: BOARD_VERSION, - boardData: {}, - zoom: 1, - canvasWidth: 1, - canvasHeight: 1 - , ...data -}) -const initFileObj = getFileObj() -export const useFileStore = defineStore( - 'file', - { - state() { - return { - currentId: initFileObj.id, - currentIndex: 0, - currentFile: initFileObj, - files: [ initFileObj ], - } - }, - actions: { - // update file | 更新文件 - updateFile(file = {}) { - if (file?.id != this.currentId) return - this.currentFile = file - this.files[this.currentIndex] = data - }, - // update board data | 更新画板数据 - updateBoardData(data) { - this.currentFile.boardData = data - this.files[this.currentIndex].boardData = data - }, - // update board zoom | 更新画板缩放 - updateBoardZoom(zoom){ - this.currentFile.zoom = zoom - this.files[this.currentIndex].zoom = zoom - }, - // update board field | 更新画板字段 - updateBoardField(key, value) { - if(key == 'id') return - this.currentFile[key] = value - this.files[this.currentIndex][key] = value - }, - // 创建新画板 - createBoard(option = {}) { - delete option?.id - let fileObj = getFileObj(option) - this.currentFile = fileObj - this.currentId = fileObj.id - this.files.push(fileObj) - this.currentIndex = this.files.length - 1 - return fileObj - } - } - } -) - -// 主模块-缓存 -const initLanguage = ['en', 'en-US', 'en-us'].includes(navigator.language) ? 'en' : 'zh' -export const useBoardStore = defineStore( - 'board', - { - state() { - return { - mode: TYPES.ActionMode.DRAW, - drawType: TYPES.DrawType.FreeStyle, - language: initLanguage, - canvasWidth: 1, - canvasHeight: 1, - backgroundColor: 'rgba(255, 255, 255, 1)', - backgroundOpacity: 1, - hasBackgroundImage: false, - backgroundImageOpacity: 1, - isObjectCaching: true, - openGuideLine: false, - } - }, - actions: { - // update mode | 更新模式 - updateMode(mode) { - if (this.mode != mode) FabricVue.handleMode(mode) - this.mode = mode - }, - // update draw type | 更新画笔类型 - updateDrawType(drawType) { - if (this.drawType != drawType){ - this.drawType = drawType - FabricVue.handleMode() - } - }, - // update language | 更新语言 - updateLanguage(language) {this.language = language}, - updateCanvasWidth(width) { - if (this.canvasWidth !== width){ - this.canvasHeight = width - FabricVue.updateCanvasWidth(width) - } - }, - // init background | 初始化背景 - initBackground() { - const backgroundColor = FabricVue?.canvas?.backgroundColor - console.log('xxxx', backgroundColor, FabricVue?.canvas) - if (backgroundColor && typeof backgroundColor === 'string') { - const type = colorUtils.getColorFormat(backgroundColor) - if (type === 'hex') { - const color = colorUtils.hexToRgba(backgroundColor) - const opacity = colorUtils.getAlphaFromRgba(color) - this.backgroundColor = color - this.backgroundOpacity = opacity - } else if (type === 'rgba') { - const opacity = colorUtils.getAlphaFromRgba(backgroundColor) - this.backgroundColor = backgroundColor - this.backgroundOpacity = opacity - } - } else if (FabricVue?.canvas) { - if (this.backgroundColor) { - FabricVue.canvas.backgroundColor = this.backgroundColor - } else { - FabricVue.canvas.backgroundColor = 'rgba(255, 255, 255, 1)' - this.backgroundColor = 'rgba(255, 255, 255, 1)' - this.backgroundOpacity = 1 - } - } - - const backgroundImage = FabricVue?.canvas?.backgroundImage - if (backgroundImage) { - bgUtils.handleBackgroundImageWhenCanvasSizeChange() - this.hasBackgroundImage = true - this.backgroundOpacity = backgroundImage.opacity - } else { - this.hasBackgroundImage = true - this.backgroundOpacity = 1 - } - } - } - } -) - -// 画笔模块-缓存 -export const useDrawStore = defineStore( - 'draw', - { - state() { - return { - drawWidth: 1, - drawColors: ['#000000'], - shadowWidth: 0, - shadowColor: '#000000', - drawTextValue: 'draw', - drawStyle: TYPES.DrawStyle.Basic, - drawShapeCount: 2, - materialType: TYPES.MATERIAL_TYPE.CRAYON, - drawShape: TYPES.DrawShape.Bubble, - eraserWidth: 20, - multiColorType: TYPES.MultiColorType.COL, - textFontFamily: 'Georgia', - openAutoDraw: false, - fontStyles: [], - } - }, - actions: { - // update draw width | 更新画笔宽度 - updateDrawWidth(w) { - const oldW = this.drawWidth - if (oldW != w && FabricVue.canvas) { - FabricVue.canvas.freeDrawingBrush.width = fabricUtils.getDrawWidth(w) - this.drawWidth = w - } - }, - // update draw colors | 更新画笔颜色 - updateDrawColors(drawColors = []) { - const drawStyle = this.drawStyle - switch(drawStyle) { - case TYPES.DrawStyle.Basic: - if (FabricVue.canvas) { - FabricVue.canvas.freeDrawingBrush.color = drawColors[0] - } - break - case TYPES.DrawStyle.Material: - if (this.drawColors[0] != drawColors[0]) { - material.render({}) - } - break - case TYPES.DrawStyle.MultiColor: - renderMultiColor({colors: drawColors}) - break - } - this.drawColors = drawColors - }, - // update shadow width | 更新画笔阴影宽度 - updateShadowWidth(shadowWidth) { - if (FabricVue.canvas) { - (FabricVue.canvas.freeDrawingBrush.shadow).blur = fabricUtils.getShadowWidth(shadowWidth) - } - this.shadowWidth = shadowWidth - }, - // update shadow color | 更新画笔阴影颜色 - updateShadowColor(shadowColor) { - if (FabricVue.canvas) { - (FabricVue.canvas.freeDrawingBrush.shadow).color = shadowColor - } - this.shadowColor = shadowColor - }, - // update shadow Shape | 更新画笔阴影形状 - updateDrawShape(drawShape) { - this.drawShape = drawShape - }, - // update draw style | 更新画笔阴影样式 - updateDrawStyle(drawStyle) { - this.drawStyle = drawStyle - FabricVue.handleDrawStyle() - }, - // update draw shape count | 更新画笔形状数量 - updateDrawShapeCount(drawShapeCount){ - this.drawShapeCount = drawShapeCount - }, - // update draw text value | 更新画笔文字 - updateDrawTextValue(drawTextValue){ - this.drawTextValue = drawTextValue - }, - // update material type | 更新材质类型 - updateMaterialType(materialType) { - if(this.materialType != materialType) { - material.render({materialType}) - this.materialType = materialType - } - }, - // update eraser width | 更新橡皮擦宽度 - updateEraserWidth(eraserWidth) { - const oldW = this.drawWidth - if (oldW != eraserWidth && FabricVue.canvas) { - FabricVue.canvas.freeDrawingBrush.width = fabricUtils.getEraserWidth(eraserWidth) - this.eraserWidth = eraserWidth - } - }, - // update multi color type | 更新多色材质类型 - updateMultiColorType(multiColorType) { - if(this.multiColorType != multiColorType) { - renderMultiColor({type: multiColorType}) - this.multiColorType = multiColorType - } - }, - // update text font family | 更新文字字体 - updateTextFontFamily(fontFamily){ - this.textFontFamily = fontFamily - }, - // update auto draw state | 更新自动绘图状态 - updateAutoDrawState() { - this.openAutoDraw = !this.openAutoDraw - }, - // update font styles | 更新字体样式 - updateFontStyles(type) { - const ind = this.fontStyles.findIndex(item => item === type) - if (ind >= 0) this.fontStyles.splice(ind, 1) - else this.fontStyles.push(type) - }, - } - } -) - - - - -// interface BoardState { -// mode: string // operating mode -// drawType: string // draw type -// language: string // i18n language 'zh' 'en' -// canvasWidth: number // canvas width 0.1 ~ 1 -// canvasHeight: number // canvas height 0.1 ~ 1 -// backgroundColor: string // canvas background color -// backgroundOpacity: number // canvas background color opacity -// hasBackgroundImage: boolean // canvas background image -// backgroundImageOpacity: number // canvas background Image opacity -// isObjectCaching: boolean // fabric objectCaching -// openGuideLine: boolean // does the guide line show -// } \ No newline at end of file diff --git a/src/renderer/src/views/tool/test.vue b/src/renderer/src/views/tool/test.vue index 153d10c..d49b599 100644 --- a/src/renderer/src/views/tool/test.vue +++ b/src/renderer/src/views/tool/test.vue @@ -1,5 +1,6 @@ From b4e78679225506ffdc2e825966b6fefbcb758f5b Mon Sep 17 00:00:00 2001 From: zdg Date: Wed, 31 Jul 2024 11:23:37 +0800 Subject: [PATCH 2/6] =?UTF-8?q?=E6=B5=8B=E8=AF=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/renderer/public/imgs/erase.svg | 3 ++ src/renderer/src/plugins/fabric/index.js | 5 ++- src/renderer/src/views/tool/test.vue | 47 ++++++++++++++++++++++-- 3 files changed, 49 insertions(+), 6 deletions(-) create mode 100644 src/renderer/public/imgs/erase.svg diff --git a/src/renderer/public/imgs/erase.svg b/src/renderer/public/imgs/erase.svg new file mode 100644 index 0000000..a746074 --- /dev/null +++ b/src/renderer/public/imgs/erase.svg @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/src/renderer/src/plugins/fabric/index.js b/src/renderer/src/plugins/fabric/index.js index cb16fb9..4d6926b 100644 --- a/src/renderer/src/plugins/fabric/index.js +++ b/src/renderer/src/plugins/fabric/index.js @@ -624,7 +624,7 @@ export class History { const canvas = this.FabricVue?.canvas if (canvas) { this.diffs = this.diffs.slice(0, this.index) - const canvasJson = Utils.getCanvasJSON() + const canvasJson = Utils.getCanvasJSON(canvas) const delta = diff(canvasJson, this.canvasData) this.diffs.push(delta) @@ -686,7 +686,7 @@ export class History { initHistory() { const canvas = this.FabricVue.canvas if (canvas) { - const canvasJson = Utils.getCanvasJSON() + const canvasJson = Utils.getCanvasJSON(canvas) this.canvasData = canvasJson this.index = 0 this.diffs = [] @@ -1010,4 +1010,5 @@ export class fabricVue { } export const FabricVue = new fabricVue() export default FabricVue +export const Fabric = fabric diff --git a/src/renderer/src/views/tool/test.vue b/src/renderer/src/views/tool/test.vue index d49b599..1deb706 100644 --- a/src/renderer/src/views/tool/test.vue +++ b/src/renderer/src/views/tool/test.vue @@ -1,6 +1,9 @@ From 89ef820366765311cc27e2f2080dcf27af6d766c Mon Sep 17 00:00:00 2001 From: zdg Date: Wed, 31 Jul 2024 17:22:03 +0800 Subject: [PATCH 3/6] =?UTF-8?q?=E6=A9=A1=E7=9A=AE=E6=93=A6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- package.json | 2 +- src/renderer/public/imgs/erase.svg | 3 - src/renderer/src/plugins/fabric/index.js | 790 +++++++++++++++++- .../src/views/tool/components/board.vue | 14 +- src/renderer/src/views/tool/sphere.vue | 3 +- src/renderer/src/views/tool/test.vue | 65 +- 6 files changed, 814 insertions(+), 63 deletions(-) delete mode 100644 src/renderer/public/imgs/erase.svg diff --git a/package.json b/package.json index 5d0c6f5..4894dc7 100644 --- a/package.json +++ b/package.json @@ -30,7 +30,7 @@ "electron-log": "^5.1.7", "electron-updater": "^6.1.7", "element-plus": "^2.7.6", - "fabric-with-erasing": "^1.0.1", + "fabric": "^5.3.0", "js-cookie": "^3.0.5", "jsencrypt": "^3.3.2", "jsondiffpatch": "0.6.0", diff --git a/src/renderer/public/imgs/erase.svg b/src/renderer/public/imgs/erase.svg deleted file mode 100644 index a746074..0000000 --- a/src/renderer/public/imgs/erase.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - \ No newline at end of file diff --git a/src/renderer/src/plugins/fabric/index.js b/src/renderer/src/plugins/fabric/index.js index 4d6926b..01cf1d7 100644 --- a/src/renderer/src/plugins/fabric/index.js +++ b/src/renderer/src/plugins/fabric/index.js @@ -1,10 +1,776 @@ /** * @description 封装fabric js */ -// import { fabric } from 'fabric' -import { fabric } from 'fabric-with-erasing' +import { fabric } from 'fabric' +// import { fabric } from 'fabric-with-erasing' +// import fabric from './fabric' +// import * as fabric from 'fabric' import { diff, unpatch, patch } from 'jsondiffpatch' +function baseBrush() { + /** ERASER_START */ + + /** + * add `eraser` to enlivened props + */ + fabric.Object.ENLIVEN_PROPS.push('eraser'); + + var __drawClipPath = fabric.Object.prototype._drawClipPath; + var _needsItsOwnCache = fabric.Object.prototype.needsItsOwnCache; + var _toObject = fabric.Object.prototype.toObject; + var _getSvgCommons = fabric.Object.prototype.getSvgCommons; + var __createBaseClipPathSVGMarkup = fabric.Object.prototype._createBaseClipPathSVGMarkup; + var __createBaseSVGMarkup = fabric.Object.prototype._createBaseSVGMarkup; + + fabric.Object.prototype.cacheProperties.push('eraser'); + fabric.Object.prototype.stateProperties.push('eraser'); + + /** + * @fires erasing:end + */ + fabric.util.object.extend(fabric.Object.prototype, { + /** + * Indicates whether this object can be erased by {@link fabric.EraserBrush} + * The `deep` option introduces fine grained control over a group's `erasable` property. + * When set to `deep` the eraser will erase nested objects if they are erasable, leaving the group and the other objects untouched. + * When set to `true` the eraser will erase the entire group. Once the group changes the eraser is propagated to its children for proper functionality. + * When set to `false` the eraser will leave all objects including the group untouched. + * @tutorial {@link http://fabricjs.com/erasing#erasable_property} + * @type boolean | 'deep' + * @default true + */ + erasable: true, + + /** + * @tutorial {@link http://fabricjs.com/erasing#eraser} + * @type fabric.Eraser + */ + eraser: undefined, + + /** + * @override + * @returns Boolean + */ + needsItsOwnCache: function () { + return _needsItsOwnCache.call(this) || !!this.eraser; + }, + + /** + * draw eraser above clip path + * @override + * @private + * @param {CanvasRenderingContext2D} ctx + * @param {fabric.Object} clipPath + */ + _drawClipPath: function (ctx, clipPath) { + __drawClipPath.call(this, ctx, clipPath); + if (this.eraser) { + // update eraser size to match instance + var size = this._getNonTransformedDimensions(); + this.eraser.isType('eraser') && this.eraser.set({ + width: size.x, + height: size.y + }); + __drawClipPath.call(this, ctx, this.eraser); + } + }, + + /** + * Returns an object representation of an instance + * @param {Array} [propertiesToInclude] Any properties that you might want to additionally include in the output + * @return {Object} Object representation of an instance + */ + toObject: function (propertiesToInclude) { + var object = _toObject.call(this, ['erasable'].concat(propertiesToInclude)); + if (this.eraser && !this.eraser.excludeFromExport) { + object.eraser = this.eraser.toObject(propertiesToInclude); + } + return object; + }, + + /* _TO_SVG_START_ */ + /** + * Returns id attribute for svg output + * @override + * @return {String} + */ + getSvgCommons: function () { + return _getSvgCommons.call(this) + (this.eraser ? 'mask="url(#' + this.eraser.clipPathId + ')" ' : ''); + }, + + /** + * create svg markup for eraser + * use to achieve erasing for svg, credit: https://travishorn.com/removing-parts-of-shapes-in-svg-b539a89e5649 + * must be called before object markup creation as it relies on the `clipPathId` property of the mask + * @param {Function} [reviver] + * @returns + */ + _createEraserSVGMarkup: function (reviver) { + if (this.eraser) { + this.eraser.clipPathId = 'MASK_' + fabric.Object.__uid++; + return [ + '', + this.eraser.toSVG(reviver), + '', '\n' + ].join(''); + } + return ''; + }, + + /** + * @private + */ + _createBaseClipPathSVGMarkup: function (objectMarkup, options) { + return [ + this._createEraserSVGMarkup(options && options.reviver), + __createBaseClipPathSVGMarkup.call(this, objectMarkup, options) + ].join(''); + }, + + /** + * @private + */ + _createBaseSVGMarkup: function (objectMarkup, options) { + return [ + this._createEraserSVGMarkup(options && options.reviver), + __createBaseSVGMarkup.call(this, objectMarkup, options) + ].join(''); + } + /* _TO_SVG_END_ */ + }); + + var __restoreObjectsState = fabric.Group.prototype._restoreObjectsState; + fabric.util.object.extend(fabric.Group.prototype, { + /** + * @private + * @param {fabric.Path} path + */ + _addEraserPathToObjects: function (path) { + this._objects.forEach(function (object) { + fabric.EraserBrush.prototype._addPathToObjectEraser.call( + fabric.EraserBrush.prototype, + object, + path + ); + }); + }, + + /** + * Applies the group's eraser to its objects + * @tutorial {@link http://fabricjs.com/erasing#erasable_property} + */ + applyEraserToObjects: function () { + var _this = this, eraser = this.eraser; + if (eraser) { + delete this.eraser; + var transform = _this.calcTransformMatrix(); + eraser.clone(function (eraser) { + var clipPath = _this.clipPath; + eraser.getObjects('path') + .forEach(function (path) { + // first we transform the path from the group's coordinate system to the canvas' + var originalTransform = fabric.util.multiplyTransformMatrices( + transform, + path.calcTransformMatrix() + ); + fabric.util.applyTransformToObject(path, originalTransform); + if (clipPath) { + clipPath.clone(function (_clipPath) { + var eraserPath = fabric.EraserBrush.prototype.applyClipPathToPath.call( + fabric.EraserBrush.prototype, + path, + _clipPath, + transform + ); + _this._addEraserPathToObjects(eraserPath); + }, ['absolutePositioned', 'inverted']); + } + else { + _this._addEraserPathToObjects(path); + } + }); + }); + } + }, + + /** + * Propagate the group's eraser to its objects, crucial for proper functionality of the eraser within the group and nested objects. + * @private + */ + _restoreObjectsState: function () { + this.erasable === true && this.applyEraserToObjects(); + return __restoreObjectsState.call(this); + } + }); + + /** + * An object's Eraser + * @private + * @class fabric.Eraser + * @extends fabric.Group + * @memberof fabric + */ + fabric.Eraser = fabric.util.createClass(fabric.Group, { + /** + * @readonly + * @static + */ + type: 'eraser', + + /** + * @default + */ + originX: 'center', + + /** + * @default + */ + originY: 'center', + + drawObject: function (ctx) { + ctx.save(); + ctx.fillStyle = 'black'; + ctx.fillRect(-this.width / 2, -this.height / 2, this.width, this.height); + ctx.restore(); + this.callSuper('drawObject', ctx); + }, + + /** + * eraser should retain size + * dimensions should not change when paths are added or removed + * handled by {@link fabric.Object#_drawClipPath} + * @override + * @private + */ + _getBounds: function () { + // noop + }, + + /* _TO_SVG_START_ */ + /** + * Returns svg representation of an instance + * use to achieve erasing for svg, credit: https://travishorn.com/removing-parts-of-shapes-in-svg-b539a89e5649 + * for masking we need to add a white rect before all paths + * + * @param {Function} [reviver] Method for further parsing of svg representation. + * @return {String} svg representation of an instance + */ + _toSVG: function (reviver) { + var svgString = ['\n']; + var x = -this.width / 2, y = -this.height / 2; + var rectSvg = [ + '\n' + ].join(''); + svgString.push('\t\t', rectSvg); + for (var i = 0, len = this._objects.length; i < len; i++) { + svgString.push('\t\t', this._objects[i].toSVG(reviver)); + } + svgString.push('\n'); + return svgString; + }, + /* _TO_SVG_END_ */ + }); + + /** + * Returns {@link fabric.Eraser} instance from an object representation + * @static + * @memberOf fabric.Eraser + * @param {Object} object Object to create an Eraser from + * @param {Function} [callback] Callback to invoke when an eraser instance is created + */ + fabric.Eraser.fromObject = function (object, callback) { + var objects = object.objects; + fabric.util.enlivenObjects(objects, function (enlivenedObjects) { + var options = fabric.util.object.clone(object, true); + delete options.objects; + fabric.util.enlivenObjectEnlivables(object, options, function () { + callback && callback(new fabric.Eraser(enlivenedObjects, options, true)); + }); + }); + }; + + var __renderOverlay = fabric.Canvas.prototype._renderOverlay; + /** + * @fires erasing:start + * @fires erasing:end + */ + fabric.util.object.extend(fabric.Canvas.prototype, { + /** + * Used by {@link #renderAll} + * @returns boolean + */ + isErasing: function () { + return ( + this.isDrawingMode && + this.freeDrawingBrush && + this.freeDrawingBrush.type === 'eraser' && + this.freeDrawingBrush._isErasing + ); + }, + + /** + * While erasing the brush clips out the erasing path from canvas + * so we need to render it on top of canvas every render + * @param {CanvasRenderingContext2D} ctx + */ + _renderOverlay: function (ctx) { + __renderOverlay.call(this, ctx); + if (this.isErasing() && !this.freeDrawingBrush.inverted) { + this.freeDrawingBrush._render(); + } + } + }); + + /** + * EraserBrush class + * Supports selective erasing meaning that only erasable objects are affected by the eraser brush. + * Supports **inverted** erasing meaning that the brush can "undo" erasing. + * + * In order to support selective erasing, the brush clips the entire canvas + * and then draws all non-erasable objects over the erased path using a pattern brush so to speak (masking). + * If brush is **inverted** there is no need to clip canvas. The brush draws all erasable objects without their eraser. + * This achieves the desired effect of seeming to erase or unerase only erasable objects. + * After erasing is done the created path is added to all intersected objects' `eraser` property. + * + * In order to update the EraserBrush call `preparePattern`. + * It may come in handy when canvas changes during erasing (i.e animations) and you want the eraser to reflect the changes. + * + * @tutorial {@link http://fabricjs.com/erasing} + * @class fabric.EraserBrush + * @extends fabric.PencilBrush + * @memberof fabric + */ + fabric.EraserBrush = fabric.util.createClass( + fabric.PencilBrush, + /** @lends fabric.EraserBrush.prototype */ { + type: 'eraser', + + /** + * When set to `true` the brush will create a visual effect of undoing erasing + */ + inverted: false, + + /** + * @private + */ + _isErasing: false, + + /** + * + * @private + * @param {fabric.Object} object + * @returns boolean + */ + _isErasable: function (object) { + return object.erasable !== false; + }, + + /** + * @private + * This is designed to support erasing a collection with both erasable and non-erasable objects. + * Iterates over collections to allow nested selective erasing. + * Prepares the pattern brush that will draw on the top context to achieve the desired visual effect. + * If brush is **NOT** inverted render all non-erasable objects. + * If brush is inverted render all erasable objects that have been erased with their clip path inverted. + * This will render the erased parts as if they were not erased. + * + * @param {fabric.Collection} collection + * @param {CanvasRenderingContext2D} ctx + * @param {{ visibility: fabric.Object[], eraser: fabric.Object[], collection: fabric.Object[] }} restorationContext + */ + _prepareCollectionTraversal: function (collection, ctx, restorationContext) { + collection.forEachObject(function (obj) { + if (obj.forEachObject && obj.erasable === 'deep') { + // traverse + this._prepareCollectionTraversal(obj, ctx, restorationContext); + } + else if (!this.inverted && obj.erasable && obj.visible) { + // render only non-erasable objects + obj.visible = false; + collection.dirty = true; + restorationContext.visibility.push(obj); + restorationContext.collection.push(collection); + } + else if (this.inverted && obj.visible) { + // render only erasable objects that were erased + if (obj.erasable && obj.eraser) { + obj.eraser.inverted = true; + obj.dirty = true; + collection.dirty = true; + restorationContext.eraser.push(obj); + restorationContext.collection.push(collection); + } + else { + obj.visible = false; + collection.dirty = true; + restorationContext.visibility.push(obj); + restorationContext.collection.push(collection); + } + } + }, this); + }, + + /** + * Prepare the pattern for the erasing brush + * This pattern will be drawn on the top context, achieving a visual effect of erasing only erasable objects + * @todo decide how overlay color should behave when `inverted === true`, currently draws over it which is undesirable + * @private + */ + preparePattern: function () { + if (!this._patternCanvas) { + this._patternCanvas = fabric.util.createCanvasElement(); + } + var canvas = this._patternCanvas; + canvas.width = this.canvas.width; + canvas.height = this.canvas.height; + var patternCtx = canvas.getContext('2d'); + if (this.canvas._isRetinaScaling()) { + var retinaScaling = this.canvas.getRetinaScaling(); + this.canvas.__initRetinaScaling(retinaScaling, canvas, patternCtx); + } + var backgroundImage = this.canvas.backgroundImage, + bgErasable = backgroundImage && this._isErasable(backgroundImage), + overlayImage = this.canvas.overlayImage, + overlayErasable = overlayImage && this._isErasable(overlayImage); + if (!this.inverted && ((backgroundImage && !bgErasable) || !!this.canvas.backgroundColor)) { + if (bgErasable) { this.canvas.backgroundImage = undefined; } + this.canvas._renderBackground(patternCtx); + if (bgErasable) { this.canvas.backgroundImage = backgroundImage; } + } + else if (this.inverted && (backgroundImage && bgErasable)) { + var color = this.canvas.backgroundColor; + this.canvas.backgroundColor = undefined; + this.canvas._renderBackground(patternCtx); + this.canvas.backgroundColor = color; + } + patternCtx.save(); + patternCtx.transform.apply(patternCtx, this.canvas.viewportTransform); + var restorationContext = { visibility: [], eraser: [], collection: [] }; + this._prepareCollectionTraversal(this.canvas, patternCtx, restorationContext); + this.canvas._renderObjects(patternCtx, this.canvas._objects); + restorationContext.visibility.forEach(function (obj) { obj.visible = true; }); + restorationContext.eraser.forEach(function (obj) { + obj.eraser.inverted = false; + obj.dirty = true; + }); + restorationContext.collection.forEach(function (obj) { obj.dirty = true; }); + patternCtx.restore(); + if (!this.inverted && ((overlayImage && !overlayErasable) || !!this.canvas.overlayColor)) { + if (overlayErasable) { this.canvas.overlayImage = undefined; } + __renderOverlay.call(this.canvas, patternCtx); + if (overlayErasable) { this.canvas.overlayImage = overlayImage; } + } + else if (this.inverted && (overlayImage && overlayErasable)) { + var color = this.canvas.overlayColor; + this.canvas.overlayColor = undefined; + __renderOverlay.call(this.canvas, patternCtx); + this.canvas.overlayColor = color; + } + }, + + /** + * Sets brush styles + * @private + * @param {CanvasRenderingContext2D} ctx + */ + _setBrushStyles: function (ctx) { + this.callSuper('_setBrushStyles', ctx); + ctx.strokeStyle = 'black'; + }, + + /** + * **Customiztion** + * + * if you need the eraser to update on each render (i.e animating during erasing) override this method by **adding** the following (performance may suffer): + * @example + * ``` + * if(ctx === this.canvas.contextTop) { + * this.preparePattern(); + * } + * ``` + * + * @override fabric.BaseBrush#_saveAndTransform + * @param {CanvasRenderingContext2D} ctx + */ + _saveAndTransform: function (ctx) { + this.callSuper('_saveAndTransform', ctx); + this._setBrushStyles(ctx); + ctx.globalCompositeOperation = ctx === this.canvas.getContext() ? 'destination-out' : 'source-over'; + }, + + /** + * We indicate {@link fabric.PencilBrush} to repaint itself if necessary + * @returns + */ + needsFullRender: function () { + return true; + }, + + /** + * + * @param {fabric.Point} pointer + * @param {fabric.IEvent} options + * @returns + */ + onMouseDown: function (pointer, options) { + if (!this.canvas._isMainEvent(options.e)) { + return; + } + this._prepareForDrawing(pointer); + // capture coordinates immediately + // this allows to draw dots (when movement never occurs) + this._captureDrawingPath(pointer); + + // prepare for erasing + this.preparePattern(); + this._isErasing = true; + this.canvas.fire('erasing:start'); + this._render(); + }, + + /** + * Rendering Logic: + * 1. Use brush to clip canvas by rendering it on top of canvas (unnecessary if `inverted === true`) + * 2. Render brush with canvas pattern on top context + * + */ + _render: function () { + var ctx; + if (!this.inverted) { + // clip canvas + ctx = this.canvas.getContext(); + this.callSuper('_render', ctx); + } + // render brush and mask it with image of non erasables + ctx = this.canvas.contextTop; + this.canvas.clearContext(ctx); + this.callSuper('_render', ctx); + ctx.save(); + var t = this.canvas.getRetinaScaling(), s = 1 / t; + ctx.scale(s, s); + ctx.globalCompositeOperation = 'source-in'; + ctx.drawImage(this._patternCanvas, 0, 0); + ctx.restore(); + }, + + /** + * Creates fabric.Path object + * @override + * @private + * @param {(string|number)[][]} pathData Path data + * @return {fabric.Path} Path to add on canvas + * @returns + */ + createPath: function (pathData) { + var path = this.callSuper('createPath', pathData); + path.globalCompositeOperation = this.inverted ? 'source-over' : 'destination-out'; + path.stroke = this.inverted ? 'white' : 'black'; + return path; + }, + + /** + * Utility to apply a clip path to a path. + * Used to preserve clipping on eraser paths in nested objects. + * Called when a group has a clip path that should be applied to the path before applying erasing on the group's objects. + * @param {fabric.Path} path The eraser path in canvas coordinate plane + * @param {fabric.Object} clipPath The clipPath to apply to the path + * @param {number[]} clipPathContainerTransformMatrix The transform matrix of the object that the clip path belongs to + * @returns {fabric.Path} path with clip path + */ + applyClipPathToPath: function (path, clipPath, clipPathContainerTransformMatrix) { + var pathInvTransform = fabric.util.invertTransform(path.calcTransformMatrix()), + clipPathTransform = clipPath.calcTransformMatrix(), + transform = clipPath.absolutePositioned ? + pathInvTransform : + fabric.util.multiplyTransformMatrices( + pathInvTransform, + clipPathContainerTransformMatrix + ); + // when passing down a clip path it becomes relative to the parent + // so we transform it acoordingly and set `absolutePositioned` to false + clipPath.absolutePositioned = false; + fabric.util.applyTransformToObject( + clipPath, + fabric.util.multiplyTransformMatrices( + transform, + clipPathTransform + ) + ); + // We need to clip `path` with both `clipPath` and it's own clip path if existing (`path.clipPath`) + // so in turn `path` erases an object only where it overlaps with all it's clip paths, regardless of how many there are. + // this is done because both clip paths may have nested clip paths of their own (this method walks down a collection => this may reccur), + // so we can't assign one to the other's clip path property. + path.clipPath = path.clipPath ? fabric.util.mergeClipPaths(clipPath, path.clipPath) : clipPath; + return path; + }, + + /** + * Utility to apply a clip path to a path. + * Used to preserve clipping on eraser paths in nested objects. + * Called when a group has a clip path that should be applied to the path before applying erasing on the group's objects. + * @param {fabric.Path} path The eraser path + * @param {fabric.Object} object The clipPath to apply to path belongs to object + * @param {Function} callback Callback to be invoked with the cloned path after applying the clip path + */ + clonePathWithClipPath: function (path, object, callback) { + var objTransform = object.calcTransformMatrix(); + var clipPath = object.clipPath; + var _this = this; + path.clone(function (_path) { + clipPath.clone(function (_clipPath) { + callback(_this.applyClipPathToPath(_path, _clipPath, objTransform)); + }, ['absolutePositioned', 'inverted']); + }); + }, + + /** + * Adds path to object's eraser, walks down object's descendants if necessary + * + * @fires erasing:end on object + * @param {fabric.Object} obj + * @param {fabric.Path} path + */ + _addPathToObjectEraser: function (obj, path) { + var _this = this; + // object is collection, i.e group + if (obj.forEachObject && obj.erasable === 'deep') { + var targets = obj._objects.filter(function (_obj) { + return _obj.erasable; + }); + if (targets.length > 0 && obj.clipPath) { + this.clonePathWithClipPath(path, obj, function (_path) { + targets.forEach(function (_obj) { + _this._addPathToObjectEraser(_obj, _path); + }); + }); + } + else if (targets.length > 0) { + targets.forEach(function (_obj) { + _this._addPathToObjectEraser(_obj, path); + }); + } + return; + } + // prepare eraser + var eraser = obj.eraser; + if (!eraser) { + eraser = new fabric.Eraser(); + obj.eraser = eraser; + } + // clone and add path + path.clone(function (path) { + // http://fabricjs.com/using-transformations + var desiredTransform = fabric.util.multiplyTransformMatrices( + fabric.util.invertTransform( + obj.calcTransformMatrix() + ), + path.calcTransformMatrix() + ); + fabric.util.applyTransformToObject(path, desiredTransform); + eraser.addWithUpdate(path); + obj.set('dirty', true); + obj.fire('erasing:end', { + path: path + }); + if (obj.group && Array.isArray(_this.__subTargets)) { + _this.__subTargets.push(obj); + } + }); + }, + + /** + * Add the eraser path to canvas drawables' clip paths + * + * @param {fabric.Canvas} source + * @param {fabric.Canvas} path + * @returns {Object} canvas drawables that were erased by the path + */ + applyEraserToCanvas: function (path) { + var canvas = this.canvas; + var drawables = {}; + [ + 'backgroundImage', + 'overlayImage', + ].forEach(function (prop) { + var drawable = canvas[prop]; + if (drawable && drawable.erasable) { + this._addPathToObjectEraser(drawable, path); + drawables[prop] = drawable; + } + }, this); + return drawables; + }, + + /** + * On mouseup after drawing the path on contextTop canvas + * we use the points captured to create an new fabric path object + * and add it to every intersected erasable object. + */ + _finalizeAndAddPath: function () { + var ctx = this.canvas.contextTop, canvas = this.canvas; + ctx.closePath(); + if (this.decimate) { + this._points = this.decimatePoints(this._points, this.decimate); + } + + // clear + canvas.clearContext(canvas.contextTop); + this._isErasing = false; + + var pathData = this._points && this._points.length > 1 ? + this.convertPointsToSVGPath(this._points) : + null; + if (!pathData || this._isEmptySVGPath(pathData)) { + canvas.fire('erasing:end'); + // do not create 0 width/height paths, as they are + // rendered inconsistently across browsers + // Firefox 4, for example, renders a dot, + // whereas Chrome 10 renders nothing + canvas.requestRenderAll(); + return; + } + + var path = this.createPath(pathData); + // needed for `intersectsWithObject` + path.setCoords(); + // commense event sequence + canvas.fire('before:path:created', { path: path }); + + // finalize erasing + var drawables = this.applyEraserToCanvas(path); + var _this = this; + this.__subTargets = []; + var targets = []; + canvas.forEachObject(function (obj) { + if (obj.erasable && obj.intersectsWithObject(path, true, true)) { + _this._addPathToObjectEraser(obj, path); + targets.push(obj); + } + }); + // fire erasing:end + canvas.fire('erasing:end', { + path: path, + targets: targets, + subTargets: this.__subTargets, + drawables: drawables + }); + delete this.__subTargets; + + canvas.requestRenderAll(); + this._resetShadow(); + + // fire event 'path' created + canvas.fire('path:created', { path: path }); + } + } + ); + + /** ERASER_END */ +} +baseBrush() // 加载橡皮擦到组件上 // 当前使用到的常量|类型(枚举) ============================ export class TYPES { static ActionMode = { @@ -378,10 +1144,21 @@ export const FreeStyle = { const canvas = fabricVue?.canvas const drawConfig = fabricVue?.drawConfig const eraserBrush = new fabric.EraserBrush(canvas) + const width = Utils.getWidth(drawConfig.eraserWidth, fabricVue) canvas.isDrawingMode = true canvas.freeDrawingBrush = eraserBrush - canvas.freeDrawingBrush.width = Utils.getWidth(drawConfig.eraserWidth, fabricVue) + canvas.freeDrawingBrush.width = width canvas.freeDrawingBrush.color = '#FFF' + // FabricVue.canvas.freeDrawingCursor = `url(/imgs/erase.svg) 10 10,crosshair` + canvas.freeDrawingCursor = FreeStyle.reaserSvg(width) + }, + reaserSvg: (width, color = '#ccc') => { // 橡皮擦-鼠标样式(自定义) + const svg = ` + + + ` + const svgUrl = `data:image/svg+xml;base64,${btoa(svg)}` + return `url(${svgUrl}) ${width/2} ${width}, crosshair` } } // 事件类 @@ -600,8 +1377,8 @@ export class CanvasEvent { } // 移除事件 removeEvent() { - this.windowEvent.removeWindowEvent() - this.touchEvent.removeTouchEvent() + this.windowEvent?.removeWindowEvent() + this.touchEvent?.removeTouchEvent() } } // 历史类 @@ -860,7 +1637,7 @@ export class fabricVue { */ deleteObject() { // Disable deletion in text input state - if (this.textElement.isTextEditing) { + if (this.textElement?.isTextEditing) { return } if (this.canvas) { @@ -1010,5 +1787,4 @@ export class fabricVue { } export const FabricVue = new fabricVue() export default FabricVue -export const Fabric = fabric diff --git a/src/renderer/src/views/tool/components/board.vue b/src/renderer/src/views/tool/components/board.vue index 6a501a4..435112f 100644 --- a/src/renderer/src/views/tool/components/board.vue +++ b/src/renderer/src/views/tool/components/board.vue @@ -5,19 +5,21 @@ diff --git a/src/renderer/src/views/tool/sphere.vue b/src/renderer/src/views/tool/sphere.vue index 51d39dc..3117e88 100644 --- a/src/renderer/src/views/tool/sphere.vue +++ b/src/renderer/src/views/tool/sphere.vue @@ -1,6 +1,6 @@ \ No newline at end of file diff --git a/src/renderer/src/views/tool/sphere.vue b/src/renderer/src/views/tool/sphere.vue index 3117e88..6c9ad30 100644 --- a/src/renderer/src/views/tool/sphere.vue +++ b/src/renderer/src/views/tool/sphere.vue @@ -42,12 +42,10 @@ const btnList = [ // ==== 方法 === const tabChange = (val) => { // 切换tab-change - console.log('xxxx', val) switch (val) { - case 'brush': + case 'brush': // 画笔 break - case 'eraser': - + case 'eraser': // 板擦 break case 'interact': break From ee53207d31eb39ab7edcad781c65d1512314f414 Mon Sep 17 00:00:00 2001 From: zdg Date: Thu, 1 Aug 2024 10:45:47 +0800 Subject: [PATCH 5/6] =?UTF-8?q?=E5=85=B3=E9=97=AD=E6=89=93=E5=8D=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/renderer/src/utils/tool.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/renderer/src/utils/tool.js b/src/renderer/src/utils/tool.js index 4515f47..e87c0d0 100644 --- a/src/renderer/src/utils/tool.js +++ b/src/renderer/src/utils/tool.js @@ -69,7 +69,7 @@ export const createWindow = async (type, data) => { // parent: mainWin, // 父窗口 // autoClose: true, // 关闭窗口后自动关闭 } - data.isConsole = true // 是否开启控制台 + // data.isConsole = true // 是否开启控制台 data.option = {...defOption, ...option} const win = await toolWindow(data) win.type = type // 唯一标识 From dbf6b665a851a3c4e1b2e0b5b2f2bb4324971d29 Mon Sep 17 00:00:00 2001 From: zdg Date: Fri, 2 Aug 2024 09:21:57 +0800 Subject: [PATCH 6/6] =?UTF-8?q?=E5=B7=A5=E5=85=B7=E6=A0=8F-=E6=8B=96?= =?UTF-8?q?=E5=8A=A8=20=E6=8A=98=E5=8F=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/renderer/src/directive/drag.js | 81 ++++++++++++++++++++++++ src/renderer/src/utils/tool.js | 7 +-- src/renderer/src/views/tool/sphere.vue | 87 +++++++++++++++++++++----- 3 files changed, 155 insertions(+), 20 deletions(-) create mode 100644 src/renderer/src/directive/drag.js diff --git a/src/renderer/src/directive/drag.js b/src/renderer/src/directive/drag.js new file mode 100644 index 0000000..9eddc29 --- /dev/null +++ b/src/renderer/src/directive/drag.js @@ -0,0 +1,81 @@ +/** + * @description: v-drag + * @author zdg + * @date 2023-06-07 + */ +// 工具包 +const utils = { + // Creates an event handler that can be used in Vue code + // setup start moving end + vueDragEvent: (el, action) => { + el.dispatchEvent(new Event(`drag-${action}`)) + }, + dragStart: (el, target, axis, snap, e) => { + // el.style.cursor = 'move' + // el.onmousedown = function (e) { + // const disX = e.clientX - el.offsetLeft + // const disY = e.clientY - el.offsetTop + // document.onmousemove = function (e) { + // const left = e.clientX - disX + // const top= e.clientY - disY + // } + // } + }, +} +// 首次向元素添加可拖动配置 | Add draggable configuration to element for the first time +const mountedHook = (el, binding) => { + console.log(el, binding) + const value = binding.value || {} + const handleSelector = value instanceof Object ? value.el : value // 获取元素 + const isOpen = value instanceof Object ? value.open || true : true // 是否开启拖拽 默认:开启 + const handleArray = [] // 拖拽元素 + if (!isOpen) return false // 没有开启不加载后面的代码 + let axis + // Store all the DOM elements that will be used as handles. + // They can be declared using a string with a CSS tag, class or id, or using Vue refs. + if (!!handleSelector) { + if (handleSelector instanceof HTMLElement) { + handleArray.push(handleSelector); + } else { + // handleArray.push(document.querySelectorAll(handleSelector)); + document.querySelectorAll(handleSelector).forEach((child) => { + handleArray.push(child); + }); + } + } + if (handleArray.length !== 0) { + // Define move element and apply CSS class + // el.classList.add(window.data.class.usesHandle); + + handleArray.forEach((grabElement) => { + // Apply CSS class to each grab element + // grabElement.classList.add(window.data.class.handle); + + // Add events to start drag with handle + grabElement.onmousedown = (e) => utils.dragStart(grabElement, el, axis, e); + grabElement.ontouchstart = (e) => utils.dragStart(grabElement, el, axis, e); + }); + } else { + // Add events to start drag without handle + el.onmousedown = (e) => utils.dragStart(el, el, axis, e); + el.ontouchstart = (e) => utils.dragStart(el, el, axis, e); + } + // Vue event on setup + utils.vueDragEvent(el, 'setup') +} +export default { + // Hooks for Vue3 + mounted(el, binding) { + mountedHook(el, binding) + }, + // Hooks for Vue2 + inserted(el, binding) { + mountedHook(el, binding) + }, + + update(el, binding){ + }, + updated(el, binding){ + + }, +} \ No newline at end of file diff --git a/src/renderer/src/utils/tool.js b/src/renderer/src/utils/tool.js index e87c0d0..a0dd8d4 100644 --- a/src/renderer/src/utils/tool.js +++ b/src/renderer/src/utils/tool.js @@ -175,9 +175,9 @@ const eventHandles = (type, win) => { const setIgnore = (_, ignore) => {win.setIgnoreMouseEvents(ignore, {forward: true})} Remote.ipcMain.on('tool-sphere:set:ignore', setIgnore) // 关闭窗口 - Remote.ipcMain.once('tool-sphere:close', () => { win.destroy() }) + Remote.ipcMain.once('tool-sphere:close', () => { win&&win.destroy() }) // 放大监听-测试 - Remote.ipcMain.once('maximize-window', () => {win.destroy()}) + Remote.ipcMain.once('maximize-window', () => {win&&win.destroy()}) const on = { onClosed: () => {Remote.ipcMain.off('tool-sphere:set:ignore', setIgnore)} } @@ -185,8 +185,7 @@ const eventHandles = (type, win) => { break} case 'open-PDF': { // 最小化窗口 minimize() - Remote.ipcMain.once('open-PDF:minimize', () => {win.destroy()}) - win.webContents.openDevTools() + Remote.ipcMain.once('open-PDF:minimize', () => {win&&win.destroy()}) publicMethods() // 加载公共方法 break} default: diff --git a/src/renderer/src/views/tool/sphere.vue b/src/renderer/src/views/tool/sphere.vue index 6c9ad30..8f84c87 100644 --- a/src/renderer/src/views/tool/sphere.vue +++ b/src/renderer/src/views/tool/sphere.vue @@ -2,11 +2,16 @@
- - - - - +
+
+ +
+
- - +
+