抓图巡检-底图支持绘制

Vue3 + fabric实现的图片绘制功能

技术栈

Vue3、fabric@5.2.4

fabric.js官网:https://fabricjs.com/

效果图如下:
功能介绍
预设颜色

支持自选颜色,选择颜色后,绘制图形和画笔的颜色匹配预设颜色
2.

图形绘制

单击矩形后,可在画布中进行绘制;绘制完成点击矩形取消矩形绘制。圆形/箭头/画笔/马赛克/文字 同理。
3.

返回操作

点击返回,取消最后一次的绘制操作(这里没有做没有绘制图形的禁用操作)
4.

下载图片

绘制完成后,点击下载图片按钮,可将图片下载到本地
5.

图片上传和背景设置

点击图片上传,选择一张图片(base64格式展示,后端返回的也是base64格式),点击设置背景,将设置画布的背景为目标图片

操作步骤
  1. 选择图片
  2. 设置背景
  3. 选择"预设颜色"
  4. 根据需求,进行图片绘制
  5. 点击图片下载,结束
注意点
  1. 返回功能(也就是清除功能)参考returnF函数即可,覆盖多数情况,如果使用清除功能可直接复制代码,不要有遗漏,避免无法实现清除
  2. 当前的绘制功能支持的是实时绘制,对于绘制后的图片的旋转,缩放操作还未完全实现。
源码分享
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');

```

相关推荐
来碗盐焗星球2 小时前
yalc,yyds!
前端
熊猫比分站2 小时前
让电竞数据实时跳动:Spring Boot 后端 + Vue 前端的完美融合实践
前端·vue.js·spring boot
eason_fan2 小时前
ESLint报错无具体信息:大型代码合并中的内存与性能问题排查
前端
ConardLi3 小时前
前端程序员原地失业?全面实测 Gemini 3.0,附三个免费使用方法!
前端·人工智能·后端
木子李BLOG3 小时前
Element Plus
前端·javascript·vue.js
Miketutu3 小时前
【大屏优化秘籍】Element UI El-Table 表格透明化与自定义行样式实战
前端·javascript·vue.js
止水编程 water_proof3 小时前
JavaScript基础
开发语言·javascript·ecmascript
rainboy4 小时前
Flutter :自己动手,封装一个小巧精致的气泡弹窗库
前端·flutter·github
合作小小程序员小小店4 小时前
web网页开发,在线%人力资源管理%系统,基于Idea,html,css,jQuery,java,jsp,ssh,mysql。
java·前端·css·数据库·mysql·html·intellij-idea