画笔 Fabric

This commit is contained in:
zdg 2024-07-23 17:20:36 +08:00
parent 7782de699f
commit 666c4becd2
17 changed files with 1009 additions and 32 deletions

View File

@ -26,6 +26,7 @@
"electron-dl-manager": "^3.0.0", "electron-dl-manager": "^3.0.0",
"electron-updater": "^6.1.7", "electron-updater": "^6.1.7",
"element-plus": "^2.7.6", "element-plus": "^2.7.6",
"fabric": "5.3.0",
"js-cookie": "^3.0.5", "js-cookie": "^3.0.5",
"jsencrypt": "^3.3.2", "jsencrypt": "^3.3.2",
"pinia": "^2.1.7", "pinia": "^2.1.7",

View File

@ -171,7 +171,8 @@ app.on('ready', () => {
createWork(data) createWork(data)
}) })
// zdg: 创建工具窗口-如 悬浮球 // zdg: 创建工具窗口-如 悬浮球
// Tool(app, shell, BrowserWindow, ipcMain) Tool()
// 打开-登录窗口
createLoginWindow() createLoginWindow()
app.on('activate', function () { app.on('activate', function () {
if (BrowserWindow.getAllWindows().length === 0) createLoginWindow() if (BrowserWindow.getAllWindows().length === 0) createLoginWindow()

View File

@ -1,16 +1,18 @@
/** /**
* @description: electron 封装的工具函数 * @description: electron 封装的工具函数
*/ */
// import { app, shell, BrowserWindow, ipcMain } from 'electron' import { app, shell, BrowserWindow, ipcMain } from 'electron'
let electron = {} import { is } from '@electron-toolkit/utils'
export default function(app, shell, BrowserWindow, ipcMain) { // const baseUrl = 'http://localhost:5173/#' // 开发环境使用
electron = { app, shell, BrowserWindow, ipcMain } const baseUrl = process.env['ELECTRON_RENDERER_URL']+'/#' // 开发环境使用
export default function() {
// 创建工具-悬浮球 // 创建工具-悬浮球
ipcMain.on('create-tool-sphere', (e, data) => { ipcMain.on('create-tool-sphere', async(e, data) => {
console.log('测试xxxx', data) console.log('测试xxxx', data)
const res = createTools(...data) // 执行逻辑 await createTools(data) // 执行逻辑
e.reply('create-tool-sphere-reply', {a: 1111}) // 返回结果 e.reply('create-tool-sphere-reply', {code: 200, msg: 'success'}) // 返回结果
}) })
} }
/** /**
@ -22,22 +24,35 @@ export default function(app, shell, BrowserWindow, ipcMain) {
* @author: zdg * @author: zdg
* @date 2021-07-05 14:07:01 * @date 2021-07-05 14:07:01
*/ */
function createTools(url, width = 800, height = 600, option={}) { function createTools({url, width = 800, height = 600, option={}}) {
if (!electron.BrowserWindow) return const devUrl = `${baseUrl}${url}`
const win = new electron.BrowserWindow({ const buildUrl = `file://${__dirname}/index.html${url}`
width, height, const urlAll = is.dev ? devUrl : buildUrl
type: 'toolbar', //创建的窗口类型为工具栏窗口 return new Promise((resolve) => {
frame: false, //要创建无边框窗口 let win = new BrowserWindow({
resizable: false, //禁止窗口大小缩放 width, height,
transparent: true, //设置透明 type: 'toolbar', //创建的窗口类型为工具栏窗口
alwaysOnTop: true, //窗口是否总是显示在其他窗口之前 frame: false, //要创建无边框窗口
webPreferences: { resizable: false, //禁止窗口大小缩放
nodeIntegration: true, transparent: true, //设置透明
contextIsolation: false, alwaysOnTop: true, //窗口是否总是显示在其他窗口之前
webSecurity: false webPreferences: {
}, nodeIntegration: true, // nodeApi调用
...option contextIsolation: false, // 沙箱取消
webSecurity: false // 跨域关闭
},
...option
})
// console.log(urlAll)
// url = 'https://www.baidu.com'
console.log(urlAll)
win.loadURL(urlAll)
win.once('ready-to-show', () => {
win.show()
resolve(win)
})
win.on('closed', () => {
win = null
})
}) })
win.loadURL(url)
return win
} }

View File

@ -1,4 +1,4 @@
import { contextBridge } from 'electron' import { contextBridge, ipcRenderer } from 'electron'
import { electronAPI } from '@electron-toolkit/preload' import { electronAPI } from '@electron-toolkit/preload'
// Custom APIs for renderer // Custom APIs for renderer
@ -10,6 +10,7 @@ const api = {}
if (process.contextIsolated) { if (process.contextIsolated) {
try { try {
contextBridge.exposeInMainWorld('electron', electronAPI) contextBridge.exposeInMainWorld('electron', electronAPI)
contextBridge.exposeInMainWorld('electronAPI')
contextBridge.exposeInMainWorld('api', api) contextBridge.exposeInMainWorld('api', api)
} catch (error) { } catch (error) {
console.error(error) console.error(error)

View File

@ -0,0 +1,105 @@
/**
* @description 静态常量-用于fabric 中定义的常量
* @author zdg
* @date 2023-06-07
*/
export const DrawType = {
FreeStyle: 'freeStyle',
Shape: 'shape'
}
export const 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'
}
export const DrawShape = {
Bubble: 'bubble',
Star: 'star',
Love: 'love',
Butterfly: 'butterfly',
Snow: 'snow',
Music: 'music',
Sun: 'sun',
Moon: 'moon',
Leaf: 'leaf',
Flower: 'flower'
}
export const MATERIAL_TYPE = {
CRAYON: 'crayon',
CARBON: 'carbon',
CLOTH: 'cloth',
OIL: 'oil',
CRAYON_DARK: 'crayonDark'
}
export const MultiColorType = {
COL: 'col',
ROW: 'row',
CIRCLE: 'circle'
}
// index.js 迁移至当前
export const ActionMode = {
DRAW: 'draw',
ERASE: 'erase',
SELECT: 'select',
Board: 'board'
}
export const 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'
}
export const 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'
}
export const ELEMENT_CUSTOM_TYPE = {
// freeStyle
...FREESTYLE_ELEMENT_CUSTOM_TYPE,
// shape
...SHAPE_ELEMENT_CUSTOM_TYPE
}

View File

@ -0,0 +1,152 @@
{
"tool": {
"draw": "Draw",
"eraser": "Eraser",
"select": "Select",
"board": "Board"
},
"title": {
"drawType": "Draw Type",
"drawStyle": "Draw Style",
"drawWidth": "Draw Width",
"drawColor": "Draw Color",
"shadow": "Shadow",
"AI": "AI",
"shapeType": "Shape Type",
"shapeCount": "Shape Count",
"materialType": "Material Type",
"multiColorType": "MultiColor Type",
"drawText": "Draw Text",
"fontFamily": "Font Family",
"shapeLinePointCount": "Point Count",
"borderType": "Border Type",
"borderStyle": "Border Style",
"fillStyle": "Fill Style",
"eraserWidth": "Eraser Width",
"opacity": "Opacity",
"layer": "Layer",
"imageFilters": "Image Filters",
"fontStyle": "Font Style",
"canvasBackground": "Canvas Background",
"canvasSize": "Canvas Size",
"drawCache": "Draw Cache",
"guideLine": "GuideLine"
},
"canvasSize": {
"width": "Width",
"height": "Height"
},
"drawType": {
"freeStyle": "FreeStyle",
"shape": "Shape"
},
"style": {
"basic": "Basic",
"rainbow": "Rainbow",
"shape": "Shape",
"material": "Material",
"pixels": "Pixels",
"multiColor": "MultiColor",
"text": "Text",
"multiLine": "MultiLine",
"reticulate": "Reticulate",
"multiPoint": "MultiPoint",
"wiggle": "Wiggle",
"thorn": "Thorn"
},
"operate": {
"undo": "undo",
"redo": "redo",
"copy": "copy",
"delete": "delete",
"text": "add text",
"image": "upload image",
"clean": "clean",
"save": "Save as image",
"fileList": "File List"
},
"info": {
"welecome": "Welcome to star",
"FreeStyle": {
"line1": "Provides 12 different styles of brushes, including Basic Brush, Rainbow Brush, Multi-Shape Brush, Multi-Material Brush, Pixel Brush, Multi-Color Brush, Text Brush, Multi-Line Connection Brush, Reticulate Brush, Multi-Point Connection Brush, Wiggle Brush, Thorn Brush. Satisfy the diversified drawing",
"line2": "All brushes support color and brush width configurations",
"line3": "MultiShape allows flexible configuration of shape types and quantities",
"line4": "Material and MultiColor allows you to adjust the material type of the brush",
"line5": "Text drawing support for cyclic text and font configuration"
},
"ShapeDraw": {
"line1": "A variety of common shapes are provided for drawing, with support for multi-point segments and arrows. The shapes support border and fill styles."
},
"EraserMode": {
"line1": "Eraser mode linearly erases all content",
"line2": "Supports linear width configurations"
},
"SelectMode": {
"line1": "In selection mode, you can click on the content of the drawing to select it",
"line2": "Click handle supports drag and drop, zoom and rotate operations, providing flexible editing options",
"line3": "Selecting images supports multiple filter configurations",
"line4": "Support for font configuration when selecting text",
"line5": "Layer settings are supported for all drawings, including Move Layer Up, Move Layer Down, Move to Top, and Move to Bottom",
"line6": "All drawings support transparency configurations"
},
"BoardMode": {
"line1": "The drawing board supports background configuration, including colour, background image, and transparency",
"line2": "The drawing board supports customized width and height configurations",
"line3": "Support for drawing cache enable. Enabling caching will improve drawing performance in the presence of large amounts of drawing content, while disabling caching will improve canvas clarity.",
"line4": "Supports guide line on and off"
},
"BorderConfig": {
"line1": "The bottom left button shows the current zoom ratio in real time, click it to reset the zoom ratio",
"line2": "The list of buttons in the center, in order from left to right, are: Undo, Redo, Copy Current Selection, Delete Current Selection, Draw Text, Upload Image, Clear Drawing, Save as Image, and Open File List",
"line3": "PC: Hold down the Space key and click the left mouse button to move the canvas, scroll the mouse wheel to realize the canvas zoom, hold down the Backspace key to delete the selected content, and hold down the Ctrl + V keys to paste the clipboard image at the same time",
"line4": "Mobile: support for dragging and zooming the canvas after a two-finger press"
},
"FileConfig": {
"line1": "Multi-file configuration: support multiple canvas switching, each canvas can be customized title, add, delete, and provide upload and download"
}
},
"cleanModal":{
"title": "Confirm clearing content?",
"confirm": "Confirmed",
"cancel": "Cancel"
},
"deleteFileModal": {
"title": "Confirm deleting the current file?",
"confirm": "Confirmed",
"cancel": "Cancel"
},
"toast": {
"uploadFileFail": "Upload failed, please try again"
},
"filters": {
"Sepia": "Sepia",
"Invert": "Invert",
"BlackWhite": "BlackWhite",
"Grayscale": "Grayscale",
"Blur": "Blur",
"Vintage": "Vintage",
"BlendColor": "BlendColor",
"Brownie": "Brownie",
"Kodachrome": "Kodachrome",
"Pixelate": "Pixelate",
"Polaroid": "Polaroid",
"Technicolor": "Technicolor",
"Brightness": "Brightness",
"Noise": "Noise",
"Convolute": "Emboss"
},
"fontStyle": {
"bold": "Bold",
"italic": "Italic",
"underLine": "UnderLine",
"lineThrough": "LineThrough"
},
"boardConfig": {
"cacheTip": "In the presence of a large amount of drawing content, enabling caching will improve drawing performance, while disabling caching will improve canvas sharpness"
},
"request": {
"tip": "Please feel free to draw...",
"loading": "Loading data, please wait...",
"error": "Something went wrong. Please try again later."
}
}

View File

@ -0,0 +1,21 @@
// import i18n from 'i18next'
// import { initReactI18next } from 'react-i18next'
// import en from './en.json'
// import zh from './zh.json'
// import useBoardStore from '@/store/board'
// const lang = useBoardStore.getState().language
// i18n.use(initReactI18next).init({
// resources: {
// en: {
// translation: en
// },
// zh: {
// translation: zh
// }
// },
// lng: lang
// })
// export default i18n

View File

@ -0,0 +1,152 @@
{
"tool": {
"draw": "绘画",
"eraser": "橡皮擦",
"select": "选择",
"board": "画板"
},
"title": {
"drawType": "绘画类型",
"drawStyle": "绘画风格",
"drawWidth": "画笔宽度",
"drawColor": "画笔颜色",
"shadow": "阴影",
"AI": "AI",
"shapeType": "形状类型",
"shapeCount": "形状数量",
"materialType": "素材类型",
"multiColorType": "多色类型",
"drawText": "文字内容",
"fontFamily": "字体",
"shapeLinePointCount": "线段端点",
"borderType": "边框类型",
"borderStyle": "边框样式",
"fillStyle": "填充样式",
"eraserWidth": "橡皮擦宽度",
"opacity": "透明度",
"layer": "图层",
"imageFilters": "图像滤镜",
"fontStyle": "字体样式",
"canvasBackground": "画板背景",
"canvasSize": "画板尺寸",
"drawCache": "绘制缓存",
"guideLine": "辅助线"
},
"canvasSize": {
"width": "宽度",
"height": "高度"
},
"drawType": {
"freeStyle": "自由绘画",
"shape": "形状绘画"
},
"style": {
"basic": "基础",
"rainbow": "彩虹 ",
"shape": "多形状",
"material": "素材",
"pixels": "像素",
"multiColor": "多色",
"text": "文字",
"multiLine": "多线连接",
"reticulate": "网状",
"multiPoint": "多点连接",
"wiggle": "波浪曲线",
"thorn": "荆棘"
},
"operate": {
"undo": "撤销",
"redo": "重做",
"copy": "复制",
"delete": "删除",
"text": "添加文字",
"image": "上传图片",
"clean": "清除画板",
"save": "保存为图片",
"fileList": "文件列表"
},
"info": {
"welecome": "欢迎Star",
"FreeStyle": {
"line1": "提供了 12 种不同风格的画笔,包括基本画笔,彩虹画笔,多形状画笔,多素材画笔,像素画笔,多色画笔,文字画笔,多线连接画笔,网状画笔,多点连接画笔,波浪曲线画笔,荆棘画笔。满足多样化的绘画",
"line2": "所有画笔均支持颜色和画笔宽度的配置",
"line3": "多形状可以灵活配置形状类型和数量",
"line4": "素材和多色可以调整画笔的素材类型",
"line5": "文字绘制支持循环文案配置和字体修改"
},
"ShapeDraw": {
"line1": "提供了多种常见形状的绘制, 并支持多端点线段以及箭头. 并且这些形状均支持边框和填充的样式配置"
},
"EraserMode": {
"line1": "橡皮擦模式可以线性擦除所有内容",
"line2": "支持线性宽度配置"
},
"SelectMode": {
"line1": "在选择模式下,可以通过点击绘画内容进行框选",
"line2": "点击手柄支持拖拽、缩放和旋转操作,提供灵活的编辑方式",
"line3": "选择图片支持多种滤镜配置",
"line4": "选择文字时,支持字体设置",
"line5": "所有绘制内容均支持图层设置,包括向上移动层级、向下移动层级、移动至顶层和移动至底层",
"line6": "所有绘制内容支持透明度配置"
},
"BoardMode": {
"line1": "画板支持配置背景配置, 包括颜色, 背景图, 透明度",
"line2": "画板支持自定义宽高配置",
"line3": "支持绘制缓存开启. 在存在大量绘制内容的情况下,启用缓存将提高绘制性能,而禁用缓存则会提升画布清晰度",
"line4": "支持辅助线的开启与关闭"
},
"BorderConfig": {
"line1": "左下角按钮实时显示当前缩放比例,点击即可重置缩放比例",
"line2": "中间按钮列表按从左到右的顺序分别为:撤销、反撤销、复制当前选择内容、删除当前选择内容、绘制文字、上传图片、清除绘制内容、保存为图片、打开文件列表",
"line3": "电脑端:按住 Space 键并点击鼠标左键可移动画布,滚动鼠标滚轮实现画布缩放,按住 Backspace 键可删除已选内容,同时按住 Ctrl 键 + V 键可粘贴剪贴板图片",
"line4": "移动端:支持双指按压后拖拽和缩放画布"
},
"FileConfig": {
"line1": "多文件配置:支持多个画布切换,每个画布可自定义标题、增加、删除,并提供上传和下载功能"
}
},
"cleanModal":{
"title": "确认清除内容?",
"confirm": "确认",
"cancel": "取消"
},
"deleteFileModal": {
"title": "确认删除当前文件吗?",
"confirm": "确认",
"cancel": "取消"
},
"toast": {
"uploadFileFail": "上传失败,请重试"
},
"filters": {
"Sepia": "复古",
"Invert": "底片",
"BlackWhite": "黑白",
"Grayscale": "灰度",
"Blur": "模糊",
"Vintage": "古典",
"BlendColor": "混色",
"Brownie": "棕仙",
"Kodachrome": "胶片",
"Pixelate": "像素",
"Polaroid": "宝丽来",
"Technicolor": "彩色",
"Brightness": "增亮",
"Noise": "噪点",
"Convolute": "浮雕"
},
"fontStyle": {
"bold": "粗体",
"italic": "斜体",
"underLine": "下划线",
"lineThrough": "删除线"
},
"boardConfig": {
"cacheTip": "在存在大量绘制内容的情况下,启用缓存将提高绘制性能,而禁用缓存则会提升画布清晰度"
},
"request": {
"tip": "请自由绘画...",
"loading": "正在加载数据,请稍候...",
"error": "出错了。请稍后再试。"
}
}

View File

@ -0,0 +1,168 @@
/**
* 二次封装fabric
*/
import { fabric } from 'fabric'
import * as TYPES from '@/constants/draw'
import * as fabricUtils from '@/plugins/fabric/utils/draw'
import * as fabricStore from '@/store/modules/draw'
// 节点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 = [] // 钩子
/** 构造函数 */
constructor() {
// this.textElement = new TextElement() // 创建文本节点
}
/**
* 初始化canvas
* @param {*} canvasEl canvas元素
* @returns boolean 是否初始化成功
*/
static initCanvas(canvasEl, option = {}) {
return new Promise(async (resolve) => {
this.canvas = new fabric.Canvas(canvasEl, {
selectionColor: 'rgba(101, 204, 138, 0.3)',
preserveObjectStacking: true,
enableRetinaScaling: true,
backgroundVpt: false,
...option
})
fabric.Object.prototype.set({
borderColor: '#65CC8A',
cornerColor: '#65CC8A',
cornerStyle: 'circle',
borderDashArray: [3, 3],
transparentCorners: false
})
fabric.Line.prototype.strokeLineJoin = 'round'
fabric.Line.prototype.strokeLineCap = 'round'
// if (isMobile()) {
// brushMouseMixin.initCanvas(this.canvas)
// }
// // 创建辅助线
// alignGuideLine.init(this.canvas, useBoardStore.openGuideLine)
// this.evnet = new CanvasEvent() // 创建相关事件
this.handleMode()
await this.initCanvasStorage()
resolve(true)
})
}
/**
* 销毁canvas
*/
static removeCanvas() {
if (this.canvas) {
this?.canvas?.dispose()
this.evnet?.removeEvent()
this.canvas = null
}
}
/**
* Initialize the canvas cache
* @description 这里可以做一下初始加载json数据的操作
*/
static initCanvasStorage() {
return new Promise((resolve) => {
setTimeout(() => {
// TODO: 获取缓存数据
})
})
}
/**
* handle mode of operation
* @param mode current mode
*/
static handleMode(mode = useBoardStore.mode) {
if (!this.canvas) {
return
}
let isDrawingMode = false
let selection = false
const objectSet = {
selectable: false,
hoverCursor: 'default'
}
switch (mode) {
case TYPES.ActionMode.DRAW:
if (
useBoardStore.drawType === TYPES.DrawType.FreeStyle &&
[TYPES.DrawStyle.Basic, TYPES.DrawStyle.Material, TYPES.DrawStyle.MultiColor].includes(
useDrawStore.drawStyle
)
) {
isDrawingMode = true
this.handleDrawStyle()
}
this.canvas.discardActiveObject()
break
case TYPES.ActionMode.ERASE:
isDrawingMode = true
this.canvas.freeDrawingBrush = new fabric.EraserBrush(
this.canvas
)
this.canvas.freeDrawingBrush.width = fabricUtils.getEraserWidth()
this.canvas.freeDrawingBrush.color = '#FFF'
this.canvas.discardActiveObject()
break
case TYPES.ActionMode.Board:
case TYPES.ActionMode.SELECT:
objectSet.selectable = true
objectSet.hoverCursor = undefined
selection = true
break
default:
break
}
this.canvas.isDrawingMode = isDrawingMode
this.canvas.selection = selection
fabric.Object.prototype.set(objectSet)
this.canvas.forEachObject((obj) => {
if (obj._customType === TYPES.ELEMENT_CUSTOM_TYPE.I_TEXT) {
obj.selectable = objectSet.selectable
obj.hoverCursor = objectSet.hoverCursor
}
})
this.canvas.requestRenderAll()
}
/**
* handle draw style
*/
static handleDrawStyle() {
if (!this.canvas) {
return
}
const drawStyle = useDrawStore.drawStyle
switch (drawStyle) {
case TYPES.DrawStyle.Basic:
renderPencilBrush()
break
case TYPES.DrawStyle.Material:
this.canvas.isDrawingMode = true
material.render({})
break
case TYPES.DrawStyle.MultiColor:
renderMultiColor({})
break
default:
this.canvas.isDrawingMode = false
break
}
}
}

View File

@ -0,0 +1,27 @@
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

@ -0,0 +1,124 @@
/**
* @description: 材质
*/
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
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 = useDrawStore.materialType,
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 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

@ -0,0 +1,33 @@
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

@ -0,0 +1,79 @@
/**
* 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
)
}

View File

@ -8,7 +8,7 @@ export const toolRouters = [
path: '/tool', path: '/tool',
children: [ children: [
{ {
path: '/sphere', path: 'sphere',
component: () => import('@/views/tool/sphere.vue'), component: () => import('@/views/tool/sphere.vue'),
name: 'toolSphere', name: 'toolSphere',
meta: {title: '悬浮球'} meta: {title: '悬浮球'}

View File

@ -0,0 +1,82 @@
/**
* @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'
// 主模块-缓存
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,
}
}
}
)
// 画笔模块-缓存
export const useDrawStore = defineStore(
'draw',
{
state() {
return {
drawWidth: 10,
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: {
updateDrawWidth(w) {
const oldW = this.drawWidth
if (oldW != w && FabricVue.canvas) {
FabricVue.canvas.freeDrawingBrush.width = fabricUtils.getDrawWidth(w)
this.drawWidth = w
}
},
}
}
)
// 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

@ -34,11 +34,11 @@ const isDialogOpen = ref(false)
const openDialog = () => { const openDialog = () => {
isDialogOpen.value = true isDialogOpen.value = true
} }
onMounted(async () => { // onMounted(async () => {
const params = { url: '/tools/sphere', width: 120, height: 120 } // const params = { url: '/tool/sphere', width: 400, height: 400 }
const res = await createTools('sphere', params) // const res = await createTools('sphere', params)
console.log('消息返回:', res) // console.log('', res)
}) // })
// //
const getData = (data) => { const getData = (data) => {
const { textBook, node } = data const { textBook, node } = data

View File

@ -2,10 +2,26 @@
<div> <div>
xxxxx xxxxx
</div> </div>
<canvas ref="canvasRef" />
</template> </template>
<script setup> <script setup>
// electron // electron
import { ref, onMounted } from 'vue'
import { FabricVue } from '@/plugins/fabric'
// import * as fabricStore from '@/store/modules/draw'
// let useDrawStore = fabricStore.useDrawStore()
// console.log(useDrawStore)
let canvasRef = ref(null)
onMounted(() => {
console.log(canvasRef, FabricVue)
if (canvasRef.value) {
const option = { width: 900, height: 900 }
FabricVue.initCanvas(canvasRef.value, option)
}
})
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>