Merge pull request 'zdg' (#92) from zdg into main

Reviewed-on: #92
This commit is contained in:
zhengdegang 2024-08-02 09:23:13 +08:00
commit f32dd848cc
24 changed files with 1831 additions and 2051 deletions

View File

@ -31,7 +31,7 @@
"electron-log": "^5.1.7", "electron-log": "^5.1.7",
"electron-updater": "^6.1.7", "electron-updater": "^6.1.7",
"element-plus": "^2.7.6", "element-plus": "^2.7.6",
"fabric": "5.3.0", "fabric": "^5.3.0",
"js-cookie": "^3.0.5", "js-cookie": "^3.0.5",
"jsencrypt": "^3.3.2", "jsencrypt": "^3.3.2",
"jsondiffpatch": "0.6.0", "jsondiffpatch": "0.6.0",

View File

@ -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){
},
}

View File

@ -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<Array<number>>
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
}
}

View File

@ -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()
}
}

View File

@ -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))
}

File diff suppressed because it is too large Load Diff

View File

@ -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
})
}

View File

@ -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()

View File

@ -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
}
}

View File

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

View File

@ -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
})
}

View File

@ -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'
}
}

View File

@ -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
// })
// }

View File

@ -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
}, {})
}
})
}

View File

@ -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
}
}

View File

@ -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
}
}

View File

@ -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
// }

View File

@ -69,9 +69,9 @@ export const createWindow = async (type, data) => {
// parent: mainWin, // 父窗口 // parent: mainWin, // 父窗口
// autoClose: true, // 关闭窗口后自动关闭 // autoClose: true, // 关闭窗口后自动关闭
} }
// data.isConsole = true // 是否开启控制台
data.option = {...defOption, ...option} data.option = {...defOption, ...option}
const win = await toolWindow(data) const win = await toolWindow(data)
win.setTitle('窗口标题: 我的自定义参数')
win.type = type // 唯一标识 win.type = type // 唯一标识
win.show() win.show()
win.setFullScreen(true) // 设置窗口为全屏 win.setFullScreen(true) // 设置窗口为全屏
@ -110,7 +110,7 @@ export const createWindow = async (type, data) => {
* @author: zdg * @author: zdg
* @date 2021-07-05 14:07:01 * @date 2021-07-05 14:07:01
*/ */
export function toolWindow({url, isFile, isConsole, option={}}) { export function toolWindow({url, isConsole, option={}}) {
// width = window.screen.width // width = window.screen.width
let width = option?.width || 800 let width = option?.width || 800
let height = option?.height || 600 let height = option?.height || 600
@ -175,9 +175,9 @@ const eventHandles = (type, win) => {
const setIgnore = (_, ignore) => {win.setIgnoreMouseEvents(ignore, {forward: true})} const setIgnore = (_, ignore) => {win.setIgnoreMouseEvents(ignore, {forward: true})}
Remote.ipcMain.on('tool-sphere:set:ignore', setIgnore) 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 = { const on = {
onClosed: () => {Remote.ipcMain.off('tool-sphere:set:ignore', setIgnore)} onClosed: () => {Remote.ipcMain.off('tool-sphere:set:ignore', setIgnore)}
} }
@ -185,8 +185,7 @@ const eventHandles = (type, win) => {
break} break}
case 'open-PDF': { case 'open-PDF': {
// 最小化窗口 minimize() // 最小化窗口 minimize()
Remote.ipcMain.once('open-PDF:minimize', () => {win.destroy()}) Remote.ipcMain.once('open-PDF:minimize', () => {win&&win.destroy()})
win.webContents.openDevTools()
publicMethods() // 加载公共方法 publicMethods() // 加载公共方法
break} break}
default: default:

View File

@ -4,20 +4,37 @@
</template> </template>
<script setup> <script setup>
// //
import { ref, onMounted } from 'vue' import { ref, onMounted, watchEffect } from 'vue'
import { FabricVue } from '@/plugins/fabric' import {FabricVue, TYPES} from '@/plugins/fabric'
import { useBoardStore, useDrawStore } from '@/store/modules/draw' const canvasRef = ref(null) //
const canvasRef = ref(null) const isMouse = ref(true) //
const props = defineProps({
modelValue: String
})
//
onMounted(async() => { onMounted(async() => {
if (canvasRef.value) { if (canvasRef.value) {
useBoardStore().backgroundColor = 'transparent' FabricVue.drawConfig.drawColors = ['red']
useDrawStore().drawColors = ['red'] FabricVue.boardConfig.backgroundColor = 'transparent'
const option = { freeDrawingCursor: 'default' } const option = { freeDrawingCursor: 'default' }
await FabricVue.initCanvas(canvasRef.value, option) await FabricVue.initCanvas(canvasRef.value, option)
// FabricVue.canvas.backgroundColor = 'transparent' }
// FabricVue.canvas.setWidth(500) })
// FabricVue.canvas.setHeight(500) //
watchEffect(() => {
// console.log('board : ', props.modelValue)
isMouse.value = false
switch(props.modelValue) {
case 'select': //
FabricVue.handleMode(TYPES.ActionMode.OTHER)
break
case 'brush': //
FabricVue.handleMode(TYPES.ActionMode.DRAW)
FabricVue.canvas.freeDrawingCursor = 'default'
break
case 'eraser': //
FabricVue.handleMode(TYPES.ActionMode.ERASE)
break
} }
}) })
</script> </script>

View File

@ -1,12 +1,17 @@
<template> <template>
<div class="warp-all"> <div class="warp-all">
<board-vue></board-vue> <board-vue v-model="tabActive"></board-vue>
<!-- 底部工具栏 --> <!-- 底部工具栏 -->
<el-row id="test" class="tool-bottom-all" @mouseenter="mouseChange(0)" @mouseleave="mouseChange(1)"> <div ref="tool" class="tool-bottom-all" :style="dataPos.style"
<el-col :span="3" class="flex justify-center items-center"> @mouseenter="mouseChange(0)" @mouseleave="mouseChange(1)">
<div class="c-logo"><el-image :src="logo" @click="tabChange('close')" /></div> <div @mousedown="e => dargHandle(e,'down')"
</el-col> @mousemove="e => dargHandle(e,'move')"
<el-col :span="20"> @mouseup="e => dargHandle(e,'up')">
<div class="c-logo" @click="logoHandle" title="拖动 | 折叠 | 展开">
<el-image :src="logo" />
</div>
</div>
<div class="tool-btns" v-show="!isFold">
<el-segmented class="c-btns" v-model="tabActive" :options="btnList" size="large" block <el-segmented class="c-btns" v-model="tabActive" :options="btnList" size="large" block
@change="tabChange"> @change="tabChange">
<template #default="{item}"> <template #default="{item}">
@ -16,22 +21,27 @@
</div> </div>
</template> </template>
</el-segmented> </el-segmented>
</el-col> </div>
</el-row> </div>
</div> </div>
</template> </template>
<script setup> <script setup>
// electron // electron
import { onMounted, ref } from 'vue' import { onMounted, ref, reactive } from 'vue'
import logo from '@root/resources/icon.png' // logo import logo from '@root/resources/icon.png' // logo
import boardVue from './components/board.vue' import boardVue from './components/board.vue' //
import vDrag from '@/directive/drag'
import { tryOnUnmounted } from '@vueuse/core'
// const Remote = require('@electron/remote') // remote // const Remote = require('@electron/remote') // remote
const { ipcRenderer } = require('electron') const { ipcRenderer } = require('electron')
const tool = ref()
const tabActive = ref('select') const tabActive = ref('select') //
const btnList = [ const isFold = ref(false) //
const isDrag = ref(false) //
const dataPos = reactive({style:{}}) //
const btnList = [ //
{ label: '选择', value: 'select', icon: 'icon-mouse' }, { label: '选择', value: 'select', icon: 'icon-mouse' },
{ label: '画笔', value: 'brush', icon: 'icon-huabi' }, { label: '画笔', value: 'brush', icon: 'icon-huabi' },
{ label: '板擦', value: 'eraser', icon: 'icon-xiangpica' }, { label: '板擦', value: 'eraser', icon: 'icon-xiangpica' },
@ -39,14 +49,13 @@ const btnList = [
{ label: '聚焦', value: 'focus', icon: 'icon-jujiao' }, { label: '聚焦', value: 'focus', icon: 'icon-jujiao' },
{ label: '更多', value: 'more', icon: 'icon-xiazai9' }, { label: '更多', value: 'more', icon: 'icon-xiazai9' },
] ]
let offsetX = 0, offsetY = 0, dragtime = 0
// ==== === // ==== ===
const tabChange = (val) => { // tab-change const tabChange = (val) => { // tab-change
console.log('xxxx', val)
switch (val) { switch (val) {
case 'brush': case 'brush': //
break break
case 'eraser': case 'eraser': //
break break
case 'interact': case 'interact':
break break
@ -61,6 +70,48 @@ const tabChange = (val) => { // 切换tab-change
break break
} }
} }
const logoHandle = (e,t) => { // logo - |
if (Date.now() - dragtime < 200) {
isFold.value = !isFold.value
}
console.log('click', isDrag.value)
}
const dargHandle = (e, type) => { //
e.preventDefault(); //
if (type == 'down') {
dragtime = Date.now()
return isDrag.value = true
} else if (type == 'up') {
return isDrag.value = false
} else {
if (!isDrag.value) return false
if (!e.clientX&&!e.clientY){ // 0
offsetX = 0
offsetY = 0
return false
}
if (!offsetX&&!offsetY) { // ,
setStyle()
} else {
const x = e.clientX - offsetX
const y = e.clientY - offsetY
setStyle(x, y)
}
offsetX = e.clientX
offsetY = e.clientY
}
}
const setStyle = (x, y) => { // -
if (offsetX && offsetY) { //
const {left, top} = dataPos.style
const ox = parseInt(left.replace('px',''))
const oy = parseInt(top.replace('px',''))
dataPos.style = {...dataPos.style, left: ox + x + 'px', top: oy + y + 'px'}
} else { //
const {left, top} = tool.value.getBoundingClientRect() //
dataPos.style = {bottom: 'unset', transform:'unset', left: left+'px', top: top+'px'}
}
}
const mouseChange = (bool) => { // 穿 const mouseChange = (bool) => { // 穿
let resBool = false let resBool = false
if (tabActive.value == 'select') resBool = !!bool if (tabActive.value == 'select') resBool = !!bool
@ -74,7 +125,8 @@ const mouseChange = (bool) => { // 鼠标移入工具栏 是否穿透
} }
// //
.tool-bottom-all{ .tool-bottom-all{
width: 45vw; // width: 45vw;
display: flex;
position: fixed; position: fixed;
bottom: 3em; bottom: 3em;
left: 50%; left: 50%;
@ -89,9 +141,11 @@ const mouseChange = (bool) => { // 鼠标移入工具栏 是否穿透
height: 5rem; height: 5rem;
border-radius: 50%; border-radius: 50%;
box-shadow: 0px 0px 8px rgb(0 0 0 / 40%); box-shadow: 0px 0px 8px rgb(0 0 0 / 40%);
position: absolute; // &:hover{
left: 0; // .el-image{transform: scale(1.1);}
// }
} }
.tool-btns{margin: 0 35px 0 7px;}
.c-btn{ .c-btn{
i{font-size: 2rem;} i{font-size: 2rem;}
} }

View File

@ -1,5 +1,9 @@
<template> <template>
<canvas ref="canvasRef" /> <canvas ref="canvasRef" />
<button @click="eraseTo">橡皮擦
<i class="iconfont icon-xiangpica"></i>
</button>
<button @click="close">销毁</button>
</template> </template>
<script setup> <script setup>
@ -7,23 +11,38 @@
import { ref, onMounted } from 'vue' import { ref, onMounted } from 'vue'
// import { FabricVue } from '@/plugins/fabric' // import { FabricVue } from '@/plugins/fabric'
// import { useBoardStore } from '@/store/modules/draw' // import { useBoardStore } from '@/store/modules/draw'
import FabricVue from '@/plugins/fabric/test' import {FabricVue, TYPES} from '@/plugins/fabric'
let canvasRef = ref(null) let canvasRef = ref(null)
let canvas = null
onMounted(async() => { onMounted(async() => {
console.log(canvasRef, FabricVue) // console.log(canvasRef, FabricVue)
// canvasRef.value = 123
if (canvasRef.value) { if (canvasRef.value) {
// useBoardStore().backgroundColor = 'transparent' // useBoardStore().backgroundColor = 'transparent'
const option = { freeDrawingCursor: 'default' } const option = { freeDrawingCursor: 'default' }
// await FabricVue.initCanvas(canvasRef.value, option) await FabricVue.initCanvas(canvasRef.value, option)
// FabricVue.canvas.setWidth(500) // FabricVue.canvas.setWidth(500)
// FabricVue.canvas.setHeight(500) // FabricVue.canvas.setHeight(500)
await FabricVue.initCanvas(canvasRef.value, option)
// const pencilBrush = new fabric.PencilBrush(canvas)
FabricVue.canvas.isDrawingMode = true
} }
// if (canvasRef.value) {
// canvas = new fabric.Canvas(canvasRef.value,{
// isDrawingMode: true,
// freeDrawingCursor: 'default',
// backgroundColor: 'transparent',
// width: window.innerWidth,
// height: window.innerHeight
// })
// canvas.isDrawingMode = true
// }
}) })
const eraseTo = () => { //
FabricVue.handleMode(TYPES.ActionMode.ERASE)
// FabricVue.removeCanvas()
// FabricVue.canvas.dispose()
// canvas.dispose()
}
const close = () => { FabricVue.removeCanvas() }
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>