Vue3 + fabric实现的图片绘制功能
技术栈
Vue3、fabric@5.2.4
fabric.js官网:https://fabricjs.com/
效果图如下:

功能介绍
预设颜色
支持自选颜色,选择颜色后,绘制图形和画笔的颜色匹配预设颜色
2.
图形绘制
单击矩形后,可在画布中进行绘制;绘制完成点击矩形取消矩形绘制。圆形/箭头/画笔/马赛克/文字 同理。
3.
返回操作
点击返回,取消最后一次的绘制操作(这里没有做没有绘制图形的禁用操作)
4.
下载图片
绘制完成后,点击下载图片按钮,可将图片下载到本地
5.
图片上传和背景设置
点击图片上传,选择一张图片(base64格式展示,后端返回的也是base64格式),点击设置背景,将设置画布的背景为目标图片
操作步骤
- 选择图片
- 设置背景
- 选择"预设颜色"
- 根据需求,进行图片绘制
- 点击图片下载,结束
注意点
- 返回功能(也就是清除功能)参考
returnF函数即可,覆盖多数情况,如果使用清除功能可直接复制代码,不要有遗漏,避免无法实现清除 - 当前的绘制功能支持的是实时绘制,对于绘制后的图片的旋转,缩放操作还未完全实现。
源码分享
html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>图片编辑</title>
<style>
body {
height: 100vh;
width: 100vw;
padding: 0;
margin: 0;
}
#app {
height: 100%;
width: 100%;
display: flex;
align-items: center;
flex-direction: column;
}
.canvas-container {
width: 500px;
height: 360px;
border: 1px solid #ccc;
position: relative;
}
.color-select-container {
height: 24px;
width: 470px;
display: flex;
align-items: center;
margin: 6px 0;
background-color: #ebebeb;
padding: 0 16px;
}
.label {
font-size: 12px;
}
.preset-color {
width: 14px;
height: 14px;
border-radius: 5px;
cursor: pointer;
border: 2px solid transparent;
transition: transform 0.2s ease;
margin-right: 6px;
}
.preset-color:last-child {
margin-right: 0;
}
.preset-color:hover {
transform: scale(1.1);
}
.active-preset-color {
border-color: #333;
}
.img-tool {
height: 40px;
width: 470px;
display: flex;
align-items: center;
justify-content: space-between;
background-color: #ebebeb;
border-top: 1px solid #e0e0e0;
padding: 0 16px;
}
button {
padding: 4px 8px;
border: none;
border-radius: 4px;
background-color: #4b6cb7;
color: white;
cursor: pointer;
transition: all 0.3s ease;
}
button:hover {
background-color: #3a5aa0;
}
button.active {
background-color: #2c4a8b;
}
.upload-container {
height: 40px;
width: 470px;
display: flex;
align-items: center;
background-color: #ebebeb;
border-top: 1px solid #e0e0e0;
padding: 0 16px;
margin: 12px 0;
}
.file-input-wrapper {
position: relative;
display: inline-block;
margin-right: 12px;
}
.disabled-btn {
background-color: #d3d3d3;
color: #999;
cursor: not-allowed;
border-color: #999;
}
.upload-btn {
display: inline-block;
background: #4b6cb7;
color: white;
border: none;
border-radius: 4px;
font-weight: 500;
height: 24px;
width: 100px;
transition: all 0.3s ease;
box-shadow: 0 4px 15px rgba(75, 108, 183, 0.3);
}
.file-input {
position: absolute;
left: 0;
top: 0;
opacity: 0;
width: 100%;
height: 100%;
cursor: pointer;
}
.preview-section {
width: 100%;
max-width: 460px;
border: 2px dashed #ddd;
border-radius: 12px;
padding: 20px;
background: #f8f9fa;
text-align: center;
height: 300px;
}
.preview-image {
max-width: 100%;
max-height: 100%;
object-fit: contain;
}
</style>
<script src="https://cdn.jsdelivr.net/npm/vue@3.2.47/dist/vue.global.prod.js"></script>
<script src="https://cdn.jsdelivr.net/npm/fabric@5.2.4/dist/fabric.min.js"></script>
</head>
<body>
<div id="app">
<div class="canvas-container">
<canvas ref="canvasRef" width="500" height="360"></canvas>
</div>
<div class="color-select-container">
<span class="label">预设颜色:</span>
<span class="preset-color" :class="{ 'active-preset-color': presetColor === '#FF5252' }"
style="background-color: #FF5252;" data-color="#FF5252" @click="changePresetColor('#FF5252')"></span>
<span class="preset-color" :class="{ 'active-preset-color': presetColor === '#FF9800' }"
style="background-color: #FF9800;" data-color="#FF9800" @click="changePresetColor('#FF9800')"></span>
<span class="preset-color" :class="{ 'active-preset-color': presetColor === '#FFEB3B' }"
style="background-color: #FFEB3B;" data-color="#FFEB3B" @click="changePresetColor('#FFEB3B')"></span>
<span class="preset-color" :class="{ 'active-preset-color': presetColor === '#4CAF50' }"
style="background-color: #4CAF50;" data-color="#4CAF50" @click="changePresetColor('#4CAF50')"></span>
<span class="preset-color" :class="{ 'active-preset-color': presetColor === '#2196F3' }"
style="background-color: #2196F3;" data-color="#2196F3" @click="changePresetColor('#2196F3')"></span>
<span class="preset-color" :class="{ 'active-preset-color': presetColor === '#9C27B0' }"
style="background-color: #9C27B0;" data-color="#9C27B0" @click="changePresetColor('#9C27B0')"></span>
<span class="preset-color" :class="{ 'active-preset-color': presetColor === '#795548' }"
style="background-color: #795548;" data-color="#795548" @click="changePresetColor('#795548')"></span>
<span class="preset-color" :class="{ 'active-preset-color': presetColor === '#607D8B' }"
style="background-color: #607D8B;" data-color="#607D8B" @click="changePresetColor('#607D8B')"></span>
</div>
<div class="img-tool">
<button @click="setRectMode"
:class="{ active: currentMode === 'rect', 'disabled-btn': !isCanDownload }">矩形</button>
<button @click="setCircleMode"
:class="{ active: currentMode === 'circle', 'disabled-btn': !isCanDownload }">圆形</button>
<button @click="setArrowMode"
:class="{ active: currentMode === 'arrow', 'disabled-btn': !isCanDownload }">箭头</button>
<button @click="toggleHuaBiStatus"
:class="{ active: currentMode === 'huabi', 'disabled-btn': !isCanDownload }">画笔</button>
<button @click="toggleMosaic"
:class="{ active: currentMode === 'masaike', 'disabled-btn': !isCanDownload }">马赛克</button>
<button @click="addText" :class="{ active: currentMode === 'text', 'disabled-btn': !isCanDownload }">文字</button>
<button @click="returnF" :class="{ 'disabled-btn': state.shapes.length === 0 }">返回</button>
<button @click="downloadImage" :class="{ 'disabled-btn': !isCanDownload }">下载图片</button>
</div>
<div class="upload-container">
<div class="file-input-wrapper">
<button class="upload-btn">选择图片文件</button>
<input type="file" ref="fileInputRef" class="file-input" accept="image/*">
</div>
<button :class="{ 'disabled-btn': !imageUrl }" @click="setBackgroundImageF">设置为背景</button>
</div>
<div class="preview-section">
<img :src="imageUrl" class="preview-image" alt="">
</div>
</div>
<script>
const { createApp, ref, onMounted, nextTick, reactive } = Vue;
createApp({
setup() {
const canvasRef = ref(null);
let fabricCanvas = null;
const currentMode = ref('');
const brushRef = ref(null); // 画笔对象
const isDrawingMosaic = ref(false); // 马赛克绘制状态
const blockSize = ref(5); // 马赛克块大小
const imgType = ref('');
const imageUrl = ref('');
const fileInputRef = ref(null)
const presetColor = ref('#FF5252')
const isCanDownload = ref(false)
const state = reactive({
currentShape: null,
isDown: false,
origX: 0,
origY: 0,
shapes: [],
tempArrow: null,
currentMosaicGroup: null,
isCreatingTextBox: false,
currentTextBox: null,
});
// 切换颜色
const changePresetColor = (val) => {
presetColor.value = val;
}
// 清除所有绘制相关的事件监听器
const clearDrawingEvents = () => {
fabricCanvas.off('mouse:down');
fabricCanvas.off('mouse:move');
fabricCanvas.off('mouse:up');
fabricCanvas.off('path:created'); // 清除画笔路径创建事件
fabricCanvas.isDrawingMode = false;
state.isDown = false;
state.currentShape = null;
state.tempArrow = null;
};
// 设置绘制模式
const setupDrawingMode = () => {
// 清除之前的所有事件监听器
clearDrawingEvents();
// 设置画布为不可选择
fabricCanvas.selection = false;
fabricCanvas.forEachObject(function (obj) {
obj.selectable = false;
obj.hasControls = false;
obj.hasBorders = false;
});
// 添加绘制形状的事件监听器
fabricCanvas.on('mouse:down', startDrawing);
fabricCanvas.on('mouse:move', continueDrawing);
fabricCanvas.on('mouse:up', stopDrawing);
}
// 开始绘制形状
const startDrawing = (o) => {
state.isDown = true;
const pointer = fabricCanvas.getPointer(o.e);
state.origX = pointer.x;
state.origY = pointer.y;
// 使用 currentMode.value 而不是 state.currentMode
if (currentMode.value === 'rect') {
// 创建矩形
state.currentShape = new fabric.Rect({
left: state.origX,
top: state.origY,
width: 0,
height: 0,
fill: 'transparent',
stroke: presetColor.value,
strokeWidth: 1,
selectable: false,
hasControls: false,
hasBorders: false
});
fabricCanvas.add(state.currentShape);
} else if (currentMode.value === 'circle') {
// 创建圆形
state.currentShape = new fabric.Circle({
left: state.origX,
top: state.origY,
radius: 0,
fill: 'transparent',
stroke: presetColor.value,
strokeWidth: 1,
selectable: false,
hasControls: false,
hasBorders: false
});
fabricCanvas.add(state.currentShape);
} else if (currentMode.value === 'arrow') {
// 箭头模式不需要在这里创建对象,会在 continueDrawing 中处理
} else if (currentMode.value === 'masaike') {
// 创建新的马赛克组
state.currentMosaicGroup = new fabric.Group([], {
selectable: false,
hasControls: false,
hasBorders: false,
evented: false,
});
state.currentMosaicGroup.type = 'masaike'
state.currentMosaicGroup.id = 'mosaic_group_' + Date.now() + '_' + Math.random().toString(36).substr(2, 9);
fabricCanvas.add(state.currentMosaicGroup);
applyMosaic(o);
}
}
// 继续绘制形状
const continueDrawing = (o) => {
if (!state.isDown) return;
const pointer = fabricCanvas.getPointer(o.e);
// 使用 currentMode.value 而不是 state.currentMode
if (currentMode.value === 'rect') {
// 更新矩形尺寸
if (state.origX > pointer.x) {
state.currentShape.set({ left: pointer.x });
}
if (state.origY > pointer.y) {
state.currentShape.set({ top: pointer.y });
}
state.currentShape.set({
width: Math.abs(state.origX - pointer.x),
height: Math.abs(state.origY - pointer.y)
});
} else if (currentMode.value === 'circle') {
// 更新圆形半径
const radius = Math.sqrt(
Math.pow(state.origX - pointer.x, 2) +
Math.pow(state.origY - pointer.y, 2)
) / 2;
state.currentShape.set({
radius: radius,
left: state.origX - radius,
top: state.origY - radius
});
} else if (currentMode.value === 'arrow') {
// 移除之前的临时箭头
if (state.tempArrow) {
fabricCanvas.remove(state.tempArrow);
}
// 创建新的临时箭头
state.tempArrow = createArrow(state.origX, state.origY, pointer.x, pointer.y);
fabricCanvas.add(state.tempArrow);
} else if (currentMode.value === 'masaike') {
applyMosaic(o);
}
fabricCanvas.renderAll();
}
// 停止绘制形状
const stopDrawing = () => {
state.isDown = false;
// 使用 currentMode.value 而不是 state.currentMode
if (currentMode.value === 'arrow') {
// 处理箭头绘制完成
if (state.tempArrow) {
// 将临时箭头转换为永久对象
const permanentArrow = createArrow(state.origX, state.origY,
state.tempArrow.x2, state.tempArrow.y2);
permanentArrow.set({
selectable: false,
hasControls: false,
hasBorders: false,
evented: false,
});
fabricCanvas.remove(state.tempArrow);
fabricCanvas.add(permanentArrow);
// 为箭头对象添加唯一标识符
permanentArrow.id = 'shape_' + Date.now() + '_' + Math.random().toString(36).substr(2, 9);
state.shapes.push(permanentArrow);
state.tempArrow = null;
}
} else if (currentMode.value === 'rect' || currentMode.value === 'circle') {
// 如果形状太小,则删除它
let isValid = false;
if (currentMode.value === 'rect') {
isValid = state.currentShape.width >= 5 && state.currentShape.height >= 5;
} else {
isValid = state.currentShape.radius >= 5;
}
if (isValid) {
// 设置绘制完成的形状为不可选择
state.currentShape.set({
selectable: false,
hasControls: false,
hasBorders: false,
evented: false
});
// 为形状对象添加唯一标识符
state.currentShape.id = 'shape_' + Date.now() + '_' + Math.random().toString(36).substr(2, 9);
state.shapes.push(state.currentShape);
} else {
fabricCanvas.remove(state.currentShape);
}
} else if (currentMode.value === 'masaike') {
// 将完成的马赛克组添加到形状数组
if (state.currentMosaicGroup && state.currentMosaicGroup._objects.length > 0) {
state.shapes.push(state.currentMosaicGroup);
state.currentMosaicGroup = null;
} else if (state.currentMosaicGroup) {
// 如果马赛克组是空的,从画布中移除
fabricCanvas.remove(state.currentMosaicGroup);
state.currentMosaicGroup = null;
}
}
fabricCanvas.renderAll();
}
// 创建箭头
const createArrow = (x1, y1, x2, y2) => {
// 计算箭头角度
const angle = Math.atan2(y2 - y1, x2 - x1);
// 创建主线
const line = new fabric.Line([x1, y1, x2, y2], {
stroke: presetColor.value,
strokeWidth: 1,
selectable: false,
evented: false
});
// 创建箭头头部
const arrowSize = 15;
const arrow1 = new fabric.Line(
[x2, y2,
x2 - arrowSize * Math.cos(angle - Math.PI / 6),
y2 - arrowSize * Math.sin(angle - Math.PI / 6)],
{
stroke: presetColor.value,
strokeWidth: 1,
selectable: false,
evented: false
}
);
const arrow2 = new fabric.Line(
[x2, y2,
x2 - arrowSize * Math.cos(angle + Math.PI / 6),
y2 - arrowSize * Math.sin(angle + Math.PI / 6)],
{
stroke: presetColor.value,
strokeWidth: 1,
selectable: false,
evented: false
}
);
// 创建箭头组
const arrowGroup = new fabric.Group([line, arrow1, arrow2], {
selectable: false,
hasControls: false,
hasBorders: false,
evented: false,
// 存储原始坐标以便后续使用
x1: x1,
y1: y1,
x2: x2,
y2: y2
});
return arrowGroup;
}
// 设置绘制矩形模式
const setRectMode = () => {
if (!isCanDownload.value) return;
if (currentMode.value === 'rect') {
currentMode.value = '';
clearDrawingEvents();
} else {
currentMode.value = 'rect';
setupDrawingMode();
}
}
// 设置绘制圆形模式
const setCircleMode = () => {
if (!isCanDownload.value) return;
if (currentMode.value === 'circle') {
currentMode.value = '';
clearDrawingEvents();
} else {
currentMode.value = 'circle';
setupDrawingMode();
}
}
// 设置绘制箭头模式
const setArrowMode = () => {
if (!isCanDownload.value) return;
if (currentMode.value === 'arrow') {
currentMode.value = '';
clearDrawingEvents();
} else {
currentMode.value = 'arrow';
setupDrawingMode();
}
}
// 创建文本框
const createTextBox = (x, y) => {
state.isCreatingTextBox = true;
// 如果已存在一个文本框且没有内容,则删除它
if (state.currentTextBox && (!state.currentTextBox.text || state.currentTextBox.text.trim() === '')) {
fabricCanvas.remove(state.currentTextBox);
// 从shapes数组中移除
const index = state.shapes.indexOf(state.currentTextBox);
if (index > -1) {
state.shapes.splice(index, 1);
}
}
// 创建一个新的可编辑文本框
const textbox = new fabric.Textbox('', { // 移除提示信息
left: x,
top: y,
width: 40,
fontSize: 18,
fill: presetColor.value, // 字体颜色为红色
borderColor: presetColor.value, // 边框颜色为红色
borderWidth: 1, // 边框宽度
transparentCorners: false,
textAlign: 'left', // 文字左对齐
editable: true, // 设置文本框为可编辑
});
// 为文本框添加唯一标识符
textbox.id = 'shape_' + Date.now() + '_' + Math.random().toString(36).substr(2, 9);
// 为文本框启用缩放、旋转控制点
textbox.setControlsVisibility({
tl: false,
tr: false,
bl: false,
br: false,
mt: false,
mb: false,
ml: false,
mr: false,
mtr: false,
});
fabricCanvas.add(textbox);
fabricCanvas.setActiveObject(textbox);
// 将文字对象添加到 shapes 数组,以便撤销功能
state.shapes.push(textbox);
// 保存当前文本框引用
state.currentTextBox = textbox;
// 文本框失去焦点时,如果内容为空则删除
textbox.on('editing:exited', () => {
if (!textbox.text || textbox.text.trim() === '') {
fabricCanvas.remove(textbox);
// 从shapes数组中移除
const index = state.shapes.indexOf(textbox);
if (index > -1) {
state.shapes.splice(index, 1);
}
// 清除当前文本框引用
if (state.currentTextBox === textbox) {
state.currentTextBox = null;
}
}
state.isCreatingTextBox = false;
});
// 自动进入编辑模式并聚焦
setTimeout(() => {
textbox.enterEditing();
textbox.hiddenTextarea.focus();
}, 10);
}
// 设置文字模式
const setupTextMode = () => {
// 清除之前的所有事件监听器
clearDrawingEvents();
// 设置画布为可选择
fabricCanvas.selection = true;
fabricCanvas.forEachObject(function (obj) {
obj.selectable = true;
obj.hasControls = true;
obj.hasBorders = true;
});
// 监听鼠标点击事件
fabricCanvas.on('mouse:down', (options) => {
if (currentMode.value === 'text' && !state.isCreatingTextBox) {
// 获取点击位置
const pointer = fabricCanvas.getPointer(options.e);
createTextBox(pointer.x, pointer.y);
}
});
};
// 添加文字
const addText = () => {
if (!isCanDownload.value) return;
// 如果当前已经是文字模式
if (currentMode.value === 'text') {
// 检查当前文本框是否有内容
if (state.currentTextBox && (!state.currentTextBox.text || state.currentTextBox.text.trim() === '')) {
// 如果没有内容,则删除文本框const objects = fabricCanvas.getObjects();
const objects = fabricCanvas.getObjects();
let objectToRemove = null;
for (let i = 0; i < objects.length; i++) {
if (objects[i].id === state.currentTextBox.id) {
objectToRemove = objects[i];
break;
}
}
if (objectToRemove) {
fabricCanvas.remove(objectToRemove);
fabricCanvas.renderAll();
}
// 从shapes数组中移除
const index = state.shapes.indexOf(state.currentTextBox);
if (index > -1) {
state.shapes.splice(index, 1);
}
state.currentTextBox = null;
}
// 退出文字模式
currentMode.value = '';
clearDrawingEvents();
} else {
// 进入文字模式
currentMode.value = 'text';
setupTextMode();
}
}
// 设置画笔模式
const setupBrushMode = () => {
// 清除之前的所有事件监听器
clearDrawingEvents();
// 启用画笔模式
fabricCanvas.isDrawingMode = true;
// 配置画笔
brushRef.value = new fabric.PencilBrush(fabricCanvas);
brushRef.value.width = 1;
brushRef.value.color = presetColor.value;
fabricCanvas.freeDrawingBrush = brushRef.value;
// 监听画笔路径创建事件
fabricCanvas.on('path:created', (e) => {
const path = e.path;
// 为画笔路径添加唯一标识符
path.id = 'shape_' + Date.now() + '_' + Math.random().toString(36).substr(2, 9);
// 设置路径不可选择
path.set({
selectable: false,
hasControls: false,
hasBorders: false,
evented: false
});
// 将画笔路径添加到形状数组
state.shapes.push(path);
});
}
// 设置马赛克模式
const setupMosaicMode = () => {
currentMode.value = 'masaike';
setupDrawingMode();
}
// 切换马赛克模式
const toggleMosaic = () => {
if (!isCanDownload.value) return;
if (currentMode.value == 'masaike') {
currentMode.value = '';
setupDrawingMode();
} else {
isDrawingMosaic.value = true;
currentMode.value = 'masaike';
setupMosaicMode();
}
};
const getRandomColor = () => {
const letters = '0123456789ABCDEF';
let color = '#';
for (let i = 0; i < 6; i++) {
color += letters[Math.floor(Math.random() * 16)];
}
return color;
};
const applyMosaic = (e) => {
if (!state.currentMosaicGroup) return;
const pointer = fabricCanvas.getPointer(e);
const left = Math.floor(pointer.x / blockSize.value) * blockSize.value;
const top = Math.floor(pointer.y / blockSize.value) * blockSize.value;
// 检查是否已经在这个位置绘制过马赛克块
const existingBlock = state.currentMosaicGroup._objects.find(obj =>
obj.left === left && obj.top === top
);
if (!existingBlock) {
const mosaicBlock = new fabric.Rect({
left: left,
top: top,
width: blockSize.value,
height: blockSize.value,
fill: getRandomColor(),
selectable: false,
hasControls: false,
hasBorders: false,
evented: false
});
// 将马赛克块添加到当前马赛克组
state.currentMosaicGroup.addWithUpdate(mosaicBlock);
fabricCanvas.renderAll();
}
}
// 切换画笔状态
const toggleHuaBiStatus = () => {
if (!isCanDownload.value) return;
if (currentMode.value != 'huabi') {
currentMode.value = 'huabi';
setupBrushMode();
} else {
currentMode.value = '';
fabricCanvas.isDrawingMode = false;
}
}
// 返回事件
const returnF = () => {
if (state.shapes.length > 0) {
const lastShape = state.shapes.pop();
const objects = fabricCanvas.getObjects();
let objectToRemove = null;
// 通过ID查找要删除的对象
for (let i = 0; i < objects.length; i++) {
if (objects[i].id === lastShape.id) {
objectToRemove = objects[i];
break;
}
}
if (objectToRemove) {
fabricCanvas.remove(objectToRemove);
fabricCanvas.renderAll();
}
fabricCanvas.remove(lastShape);
fabricCanvas.renderAll();
}
}
// 设备背景
const setBackgroundImageF = () => {
// 添加背景图片
fabric.Image.fromURL(imageUrl.value, (img) => {
img.scaleToWidth(600);
img.scaleToHeight(400);
fabricCanvas.setBackgroundImage(img, fabricCanvas.renderAll.bind(fabricCanvas), {
crossOrigin: 'anonymous'
});
});
isCanDownload.value = true
}
// 处理文件选择
const handleFileSelection = (file) => {
if (!file.type.startsWith('image/')) {
alert('请选择有效的图片文件!');
return;
}
imgType.value = file.name.split('.')[1]
// 创建FileReader对象
const reader = new FileReader();
// 文件读取完成事件
reader.onload = function (e) {
const base64 = e.target.result;
imageUrl.value = base64; // 将读取到的Base64数据赋值给imageUrl
};
// 读取文件为Data URL (Base64)
reader.readAsDataURL(file);
}
// 下载图片
const downloadImage = () => {
if (fabricCanvas.backgroundImage) {
const dataURL = fabricCanvas.toDataURL({
format: imgType.value,
multiplier: 2, // 提升分辨率
quality: 1,
});
const link = document.createElement('a');
link.href = dataURL;
link.download = `canvas-image.${imgType.value}`;
link.click();
}
}
onMounted(() => {
nextTick(() => {
fabricCanvas = new fabric.Canvas(canvasRef.value);
fabric.Object.prototype.transparentCorners = false;
fabric.Object.prototype.cornerColor = 'blue';
fabric.Object.prototype.cornerStyle = 'circle';
fileInputRef.value.addEventListener('change', function (event) {
const file = event.target.files[0];
if (file) {
handleFileSelection(file);
}
})
})
})
return {
state,
canvasRef,
currentMode,
fileInputRef,
imageUrl,
setRectMode,
downloadImage,
setCircleMode,
setArrowMode,
addText,
toggleHuaBiStatus,
toggleMosaic,
isDrawingMosaic,
setBackgroundImageF,
returnF,
presetColor,
changePresetColor,
isCanDownload,
}
}
}).mount('#app');
</script>
</body>
</html>
;
fabric.Object.prototype.cornerColor = 'blue';
fabric.Object.prototype.cornerStyle = 'circle';
fileInputRef.value.addEventListener('change', function (event) {
const file = event.target.files[0];
if (file) {
handleFileSelection(file);
}
})
})
})
return {
state,
canvasRef,
currentMode,
fileInputRef,
imageUrl,
setRectMode,
downloadImage,
setCircleMode,
setArrowMode,
addText,
toggleHuaBiStatus,
toggleMosaic,
isDrawingMosaic,
setBackgroundImageF,
returnF,
presetColor,
changePresetColor,
isCanDownload,
}
}
}).mount('#app');
```