From 79d36ea6ba813f60541da931a5d84f2aac69ea3d Mon Sep 17 00:00:00 2001 From: zdg Date: Wed, 24 Jul 2024 16:20:35 +0800 Subject: [PATCH] =?UTF-8?q?=E7=94=BB=E6=9D=BF-=E7=94=BB=E7=AC=94?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- package.json | 1 + .../src/plugins/fabric/event/clickEvent.js | 232 +++++++++++++++ .../src/plugins/fabric/event/index.js | 35 +++ src/renderer/src/plugins/fabric/history.js | 120 ++++++++ src/renderer/src/plugins/fabric/index.js | 264 ++++++++++++++++-- .../src/plugins/fabric/nodes/draw/material.js | 11 +- .../plugins/fabric/nodes/draw/multiColor.js | 98 +++++++ src/renderer/src/plugins/fabric/utils/bg.js | 77 +++++ .../src/plugins/fabric/utils/color.js | 82 ++++++ .../src/plugins/fabric/utils/index.js | 86 ++++++ .../plugins/fabric/utils/shape/arrowLine.js | 164 +++++++++++ .../src/plugins/fabric/utils/shape/line.js | 109 ++++++++ src/renderer/src/store/modules/draw.js | 208 ++++++++++++++ 13 files changed, 1464 insertions(+), 23 deletions(-) create mode 100644 src/renderer/src/plugins/fabric/event/clickEvent.js create mode 100644 src/renderer/src/plugins/fabric/event/index.js create mode 100644 src/renderer/src/plugins/fabric/history.js create mode 100644 src/renderer/src/plugins/fabric/nodes/draw/multiColor.js create mode 100644 src/renderer/src/plugins/fabric/utils/bg.js create mode 100644 src/renderer/src/plugins/fabric/utils/color.js create mode 100644 src/renderer/src/plugins/fabric/utils/shape/arrowLine.js create mode 100644 src/renderer/src/plugins/fabric/utils/shape/line.js diff --git a/package.json b/package.json index c84e5f1..62c23fe 100644 --- a/package.json +++ b/package.json @@ -29,6 +29,7 @@ "fabric": "5.3.0", "js-cookie": "^3.0.5", "jsencrypt": "^3.3.2", + "jsondiffpatch": "0.6.0", "pinia": "^2.1.7", "pinia-plugin-persistedstate": "^3.2.1", "vue-cropper": "^1.0.3", diff --git a/src/renderer/src/plugins/fabric/event/clickEvent.js b/src/renderer/src/plugins/fabric/event/clickEvent.js new file mode 100644 index 0000000..98c5816 --- /dev/null +++ b/src/renderer/src/plugins/fabric/event/clickEvent.js @@ -0,0 +1,232 @@ +/** + * @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) => { + 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() + // } + console.log('xxxx') + this.isDrawBasic = true + break + default: + break + } + } + } + this.currentElement = currentElement + }) + + // 事件:移动鼠标 + canvas?.on('mouse:move', (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 (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) => { + 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() + window.test = FabricVue + }) + + 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 new file mode 100644 index 0000000..263d152 --- /dev/null +++ b/src/renderer/src/plugins/fabric/event/index.js @@ -0,0 +1,35 @@ +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 new file mode 100644 index 0000000..ab8cf15 --- /dev/null +++ b/src/renderer/src/plugins/fabric/history.js @@ -0,0 +1,120 @@ +/** + * 历史记录 + */ +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/plugins/fabric/index.js b/src/renderer/src/plugins/fabric/index.js index 735bd57..277112f 100644 --- a/src/renderer/src/plugins/fabric/index.js +++ b/src/renderer/src/plugins/fabric/index.js @@ -4,19 +4,21 @@ 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' + // 节点node import { renderPencilBrush } from '@/plugins/fabric/nodes/draw' -const useDrawStore = fabricStore.useDrawStore() -const useBoardStore = fabricStore.useBoardStore() - -export class FabricVue { - static canvas = null // fabric-canvas 对象 - static evnet = null // 事件对象 - static history = null // 历史记录 - static textElement // 文本节点 - static hookFn = [] // 钩子 +export class fabricVue { + canvas = null // fabric-canvas 对象 + evnet = null // 事件对象 + history = null // 历史记录 + textElement // 文本节点 + hookFn = [] // 钩子 /** 构造函数 */ constructor() { // this.textElement = new TextElement() // 创建文本节点 @@ -26,7 +28,8 @@ export class FabricVue { * @param {*} canvasEl canvas元素 * @returns boolean 是否初始化成功 */ - static initCanvas(canvasEl, option = {}) { + initCanvas(canvasEl, option = {}) { + // const useBoardStore = fabricStore.useBoardStore() return new Promise(async (resolve) => { this.canvas = new fabric.Canvas(canvasEl, { selectionColor: 'rgba(101, 204, 138, 0.3)', @@ -51,7 +54,7 @@ export class FabricVue { // // 创建辅助线 // alignGuideLine.init(this.canvas, useBoardStore.openGuideLine) - // this.evnet = new CanvasEvent() // 创建相关事件 + this.evnet = new CanvasEvent() // 创建相关事件 this.handleMode() await this.initCanvasStorage() @@ -62,7 +65,7 @@ export class FabricVue { /** * 销毁canvas */ - static removeCanvas() { + removeCanvas() { if (this.canvas) { this?.canvas?.dispose() this.evnet?.removeEvent() @@ -73,21 +76,66 @@ export class FabricVue { * Initialize the canvas cache * @description 这里可以做一下初始加载json数据的操作 */ - static initCanvasStorage() { + initCanvasStorage() { + const useBoardStore = fabricStore.useBoardStore() + const useFileStore = fabricStore.useFileStore() return new Promise((resolve) => { setTimeout(() => { - // TODO: 获取缓存数据 - }) + 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 */ - static handleMode(mode = useBoardStore.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 const objectSet = { @@ -144,10 +192,11 @@ export class FabricVue { /** * handle draw style */ - static handleDrawStyle() { + handleDrawStyle() { if (!this.canvas) { return } + const useDrawStore = fabricStore.useDrawStore() const drawStyle = useDrawStore.drawStyle switch (drawStyle) { case TYPES.DrawStyle.Basic: @@ -165,4 +214,183 @@ export class FabricVue { break } } -} \ No newline at end of file + /** + * delete active objects + */ + deleteObject() { + // Disable deletion in text input state + if (this.textElement.isTextEditing) { + return + } + if (this.canvas) { + const activeObjects = this.canvas.getActiveObjects() + if (activeObjects?.length) { + this.canvas.discardActiveObject() + activeObjects?.forEach((obj) => { + this.canvas?.remove(obj) + }) + this.render() + } + } + } + /** + * render and save history state + */ + render() { + if (this.canvas) { + this.canvas?.requestRenderAll() + this.history?.saveState() + } + } + /** + * save as Image + */ + saveImage() { + if (this.canvas) { + const link = document.createElement('a') + link.href = this.canvas.toDataURL() + link.download = 'paint-board.png' + link.click() + } + } + /** + * copy active objects + */ + copyObject() { + const canvas = this.canvas + if (!canvas) { + return + } + const targets = canvas.getActiveObjects() + if (targets.length <= 0) { + return + } + canvas.discardActiveObject() + const copys = targets.map((target) => { + return new Promise((resolve) => { + target?.clone((cloned) => { + const id = uuidv4() + cloned.set({ + left: (cloned?.left || 0) + 10, + top: (cloned?.top || 0) + 10, + evented: true, + id, + perPixelTargetFind: true + }) + resolve(cloned) + canvas.add(cloned) + }) + }) + }) + Promise.all(copys).then((objs) => { + const activeSelection = new fabric.ActiveSelection(objs, { + canvas: canvas + }) + canvas.setActiveObject(activeSelection) + this.render() + }) + } + /** + * Moving active objects via fabric's bringForward method + * 通过fabric的bringForward方法移动活动对象 + */ + bringForWard() { + const canvas = this.canvas + if (canvas) { + const object = canvas.getActiveObject() + if (object) { + canvas.bringForward(object, true) + this.render() + } + } + } + + /** + * Moving active objects via fabric's sendBackwards method + * 通过fabric的sendBackwards方法移动活动对象 + */ + seendBackWard() { + const canvas = this.canvas + if (canvas) { + const object = canvas.getActiveObject() + if (object) { + canvas.sendBackwards(object, true) + this.render() + } + } + } + /** + * Moving active objects via fabric's bringToFront method + * 通过fabric的bringToFront方法移动活动对象 + */ + bringToFront() { + const canvas = this.canvas + if (canvas) { + const object = canvas.getActiveObject() + if (object) { + canvas.bringToFront(object) + this.render() + } + } + } + /** + * Moving active objects via fabric's sendToBack method + * 通过fabric的sendToBack方法移动活动对象 + */ + sendToBack() { + const canvas = this.canvas + if (canvas) { + const object = canvas.getActiveObject() + if (object) { + canvas.sendToBack(object) + this.render() + } + } + } + + /** + * Add hook fn to trigger on update|在更新时添加钩钩fn以触发 + * @param fn hook fn () => void + */ + addHookFn(fn) { + this.hookFn.push(fn) + } + + /** + * remove trigger hook fn | 移除触发钩fn + * @param fn hook fn () => void + */ + removeHookFn(fn) { + const hookIndex = this.hookFn.findIndex((v) => v === fn) + if (hookIndex > -1) { + this.hookFn.splice(hookIndex, 1) + } + } + + /** + * trigger hook fn + */ + triggerHook() { + this.hookFn.map((fn) => { + 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() \ 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 index 6e37f1f..3fa8343 100644 --- a/src/renderer/src/plugins/fabric/nodes/draw/material.js +++ b/src/renderer/src/plugins/fabric/nodes/draw/material.js @@ -5,7 +5,6 @@ import { FabricVue } from '@/plugins/fabric' import * as TYPES from '@/constants/draw' import * as fabricStore from '@/store/modules/draw' -const useDrawStore = fabricStore.useDrawStore() export class Material { initPromise = null // Initialize promise @@ -20,6 +19,7 @@ export class Material { } async initMaterial() { + this.initPromise = Promise.all([ this.loadImage(TYPES.MATERIAL_TYPE.CRAYON), this.loadImage(TYPES.MATERIAL_TYPE.CARBON), @@ -29,10 +29,10 @@ export class Material { ]) } - render({ - materialType = useDrawStore.materialType, - color = useDrawStore.drawColors[0] - }) { + 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: @@ -58,6 +58,7 @@ export class Material { 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 diff --git a/src/renderer/src/plugins/fabric/nodes/draw/multiColor.js b/src/renderer/src/plugins/fabric/nodes/draw/multiColor.js new file mode 100644 index 0000000..a37195c --- /dev/null +++ b/src/renderer/src/plugins/fabric/nodes/draw/multiColor.js @@ -0,0 +1,98 @@ +/** + * 多个材质画刷 + * @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/utils/bg.js b/src/renderer/src/plugins/fabric/utils/bg.js new file mode 100644 index 0000000..61dfd60 --- /dev/null +++ b/src/renderer/src/plugins/fabric/utils/bg.js @@ -0,0 +1,77 @@ + +/** + * 工具:背景相关的颜色、背景图片 + */ +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 new file mode 100644 index 0000000..6e4350e --- /dev/null +++ b/src/renderer/src/plugins/fabric/utils/color.js @@ -0,0 +1,82 @@ +/** + * 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/index.js b/src/renderer/src/plugins/fabric/utils/index.js index 1b567d7..0d98900 100644 --- a/src/renderer/src/plugins/fabric/utils/index.js +++ b/src/renderer/src/plugins/fabric/utils/index.js @@ -1,3 +1,11 @@ +/** + * 工具类 + */ +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 @@ -77,3 +85,81 @@ export const isMobile = () => { 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 new file mode 100644 index 0000000..1c3a0cf --- /dev/null +++ b/src/renderer/src/plugins/fabric/utils/shape/arrowLine.js @@ -0,0 +1,164 @@ +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 new file mode 100644 index 0000000..e63fd59 --- /dev/null +++ b/src/renderer/src/plugins/fabric/utils/shape/line.js @@ -0,0 +1,109 @@ +/** + * 线 + */ +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 index 7f86190..3f85c56 100644 --- a/src/renderer/src/store/modules/draw.js +++ b/src/renderer/src/store/modules/draw.js @@ -6,6 +6,73 @@ 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' @@ -26,6 +93,59 @@ export const useBoardStore = defineStore( 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 + 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) { + 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 + } + } } } ) @@ -53,6 +173,7 @@ export const useDrawStore = defineStore( } }, actions: { + // update draw width | 更新画笔宽度 updateDrawWidth(w) { const oldW = this.drawWidth if (oldW != w && FabricVue.canvas) { @@ -60,6 +181,93 @@ export const useDrawStore = defineStore( 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) + }, } } )