使用Vue + fabric.js构建标注工具

871 阅读1分钟

一、案例截图

image.png

文章记录了工作中开发的标注功能,上图是写的一个demo,不是实际项目中的代码和页面,所以页面和代码写的很潦草只是为了记录下,忽略页面样式和一些细节。代码写的很烂,别怼我!!!

二、简介

在做项目时会遇到手动绘图等标注的需求,此时多会想到使用canvas,但如果自己用原生canvas去画图、尤其是拖拽交互会复杂很多;这时候可以考虑fabric.js库来实现。

自己理解Fabric的话,就是在原生canvas上封装了一次,代码语义上更加简洁易懂,暴露的api可供我们直接调用,可以调用不同的api直接创建出矩形、圆形、直线、文本等不同的图形,并且有鼠标的一些事件可供使用;

本工具中包含了所有的常用图形,所以直接复制代码到你的项目中即可看到效果。

另外fabric是外国的框架,官网是纯英文的,你**🐴😡,我是看不懂,太欺负人了,所以就找到了一个其他博主简单翻意的算是一个中文文档吧,参考下,直接上链接;

官网网址:fabricjs.com/

官网提供的demo例子:fabricjs.com/demos/

引用 中文文档:funcion_woqu.gitee.io/fabric-doc/…

npm地址:www.npmjs.com/package/fab…

三、相关依赖

文章代码中使用到了几个依赖库,需要下载

1、element-ui的取色器、input、Select组件

2、fabric.js (从上面👆🏻的npm官网下载)

四、源码

<template>
  <div class="fabric">
    
    <div class="controlPanel">
      <div
        :class="[currentTool === item.name ? 'contro-item active' : 'contro-item']"
        v-for="(item, idx) in toolsArr"
        :key="idx"
        @click="handleTools(item, idx)"
      >
        <div v-if="!item.custom">
          <span class="con_title">{{ item.title }}</span>
        </div>
        <div v-if="item.name == 'ColorPicker'">
          <span class="con_title">{{ item.title }}:</span>
          <el-color-picker
            v-model="stroke"
            show-alpha
            :predefine="predefineColors">
          </el-color-picker>
          </div>
        <div v-if="item.name == 'lineWidth'" class="_inp">
          <span class="con_title">{{ item.title }}:</span>
          <el-input
            size="mini"
            type="number"
            v-model="strokeWidth"
            @blur="blurChang"
            @input="inpChang"
            placeholder="请输入线宽"
          ></el-input>
        </div>
        <div v-if="item.name == 'fontSize'">
          <span class="con_title">{{ item.title }}:</span>
          <el-select v-model="textFontSize" placeholder="请选择" size="mini">
            <el-option
              v-for="(size_item,size_index) in [15,18,21,24]"
              :key="size_item"
              :label="size_item"
              :value="size_item">
            </el-option>
          </el-select>
        </div>
        <div v-if="item.name == 'Stamp'" class="aa">
          <el-upload
            class="upload-demo"
            ref="upload"
            action="fakeaction"
            :http-request="uploadSectionFile"
            accept=".jpg,.png"
            :show-file-list="false"
            :multiple="true"
            >
            <span class="con_title">{{ item.title }}</span>
          </el-upload>
        </div>
      </div>
    </div>

    <div class="canvas-wraper">
      <p class="title">绘图区</p>
      <canvas width="600" height="600" id="canvas" style="border: 1px solid #ccc;"></canvas>
    </div>

    <div class="imgbase">
      <p class="title">图片展示区</p>
      <img :src="imageBase64" v-show="imageBase64!=''" alt="">
    </div>
    
  </div>
</template>
  
<script>
//创建完实例后,fabric.js会构建两层 canvas 元素:lower-canvas 和 upper-canvas
// lower-canvas: 只负责渲染元素
// upper-canvas: 负责所有的事件处理
import { fabric } from "fabric";
export default {
  name: "Fabric",
  data() {
    return {
      fabricCanvas: null, // 当前画布,如果需求是多个页可以循环
      drawingObject: null, // 当前fabric对象
      currentTool: "juxing",
      toolsArr: [
        { name: "ColorPicker", title: "取色器", icon: "", custom: true },
        { name: "lineWidth", title: "线宽", icon: "", custom: true },
        { name: "fontSize", title: "字号", icon: "", custom: true },
        { name: "bj", title: "编辑", icon: "", custom: false },
        { name: "pencil", title: "自由画笔", icon: "", custom: false },
        { name: "line", title: "直线", icon: "", custom: false },
        { name: "arrow", title: "箭头", icon: "", custom: false },
        { name: "xuxian", title: "虚线", icon: "", custom: false },
        { name: "wavyLine", title: "波浪线", icon: "", custom: false },
        { name: "juxing", title: "矩形", icon: "", custom: false },
        { name: "Highlight", title: "高亮", icon: "", custom: false },
        { name: "Stamp", title: "图章", icon: "", custom: true },
        { name: "text", title: "文字", icon: "", custom: false },
        { name: "cricle", title: "圆形", icon: "", custom: false },
        { name: "ellipse", title: "椭圆", icon: "", custom: false },
        { name: "equilateral", title: "三角形", icon: "", custom: false },
        { name: "undo", title: "上一步", icon: "", custom: false },
        { name: "redo", title: "下一步", icon: "", custom: false },
        { name: "reset", title: "重置", icon: "", custom: false },
        { name: "remove", title: "删除", icon: "", custom: false },
        { name: "bc", title: "获取所有图形数据", icon: "", custom: false },
        { name: "imageBase64", title: "保存成图片", icon: "", custom: false },
      ],
      mouseFrom: {},
      mouseTo: {},
      moveCount: 1,
      doDrawing: false,
      fabricHistoryJson: [],
      mods: 0,
      predefineColors: [
        '#ff4500',
        '#ff8c00',
        '#ffd700',
        '#90ee90',
        '#00ced1',
        '#1e90ff',
        '#c71585',
        'rgba(255, 69, 0, 0.68)',
        'rgb(255, 120, 0)',
        'hsv(51, 100, 98)',
        'hsva(120, 40, 94, 0.5)',
        'hsl(181, 100%, 37%)',
        'hsla(209, 100%, 56%, 0.73)',
        '#c7158577',
      ],
      stroke: "#00ced1", // 组件默认颜色 rgba(227, 79, 81) 或 #E34F51
      strokeWidth: 1, // 组件画笔的宽度
      textFontSize: 18, // 文本字体大小
      imageBase64: '',
    };
  },
  computed: {
  },
  mounted() {
    this.initCanvas()
  },
  watch: {
    stroke(newvalu,oldvalue){
      if (this.drawingObject) {
        var drawingObject = this.drawingObject
        this.fabricCanvas.remove(this.drawingObject);
        this.drawingObject = drawingObject
        this.drawingObject.set({
          stroke: this.stroke,
        })
        if (this.drawingObject.customType == 'Highlight') {
          this.drawingObject.set({
            fill: this.stroke,
          })
        }
        this.fabricCanvas.add(this.drawingObject);
        this.fabricCanvas.setActiveObject(this.drawingObject)
        this.fabricCanvas.requestRenderAll()
      }
    },
    strokeWidth(newvalu,oldvalue) {
      if (this.strokeWidth != 0 && this.drawingObject && this.drawingObject.customType != 'Highlight') {
        var drawingObject = this.drawingObject
        this.fabricCanvas.remove(this.drawingObject);
        this.drawingObject = drawingObject
        this.drawingObject.set({
          strokeWidth: this.strokeWidth,
        })
        this.fabricCanvas.add(this.drawingObject);
        this.fabricCanvas.setActiveObject(this.drawingObject)
        this.fabricCanvas.requestRenderAll()
      }
    },
  },
  methods: {
    // 重置画布
    resetCon() {
      this.fabricCanvas.clear(); // 清空画布
      this.fabricHistoryJson = []; // 清空历史记录
    },
    blurChang(vlaue){
      this.strokeWidth = this.strokeWidth == 0 ? 1 : this.strokeWidth
    },
    inpChang(value) {
      this.strokeWidth = Number(value);
      if (this.strokeWidth < 0) {
        this.strokeWidth = 1;
      }
    },
    // 选取图章文件
    uploadSectionFile(params){
      console.log('选取文件',params)
      const file = params
      const _this = this
      const reader = new FileReader()
      reader.readAsDataURL(file.file)
      reader.onload = () => {
        // reader.result 是 base64格式
        fabric.Image.fromURL(reader.result, oImg => {
          console.log('oImg>>>:',oImg)
          oImg.set({
            customType: 'Stamp',
            type: 'Stamp',
            width: oImg.width, 
            height: oImg.height,
            left: 50, 
            top: 50,
            stroke: this.stroke,
            strokeWidth: 0,
            fill: "rgba(255,255,255,0)",
            img: reader.result,
            imgName: file.file.name,
            hasControls: false, // 不显示控制器;如果图片需要手动调整大小,注释掉这行
          })
          _this.drawingObject = oImg
          _this.fabricCanvas.add(oImg);
          _this.fabricCanvas.discardActiveObject()
          _this.fabricCanvas.setActiveObject(oImg)
          _this.fabricCanvas.requestRenderAll()
        })
      }
    },
    // 初始化fabric
    initCanvas() {
        this.fabricCanvas = new fabric.Canvas("canvas", {
          isDrawingMode: false, //设置是否可以自由绘制
          selection: false, // 是否允许框选 默认true  true允许  false不允许
          skipTargetFind: false, // 是否选中 默认false  true禁止  false允许
          width: 1100, //设置画布的宽度
          height: 300, //设置画布的高度
        });
        
        //另一种设置宽高的方式
        // this.fabricCanvas.setWidth(100); //设置画布的宽度
        // this.fabricCanvas.setHeight(100); //设置画布的高度
        this.fabricObjAddEvent(); //绑定画板事件
        this.LineWithArrow()
        this.keyDown();
    },
    //事件监听
    fabricObjAddEvent() {
      var _this = this;
      this.fabricCanvas.on({
        // 鼠标按下
        "mouse:down": (e) => {
          console.log("鼠标按下", e);
          _this.drawingObject = null;
          _this.mouseFrom.x = e.pointer.x;
          _this.mouseFrom.y = e.pointer.y;
          _this.doDrawing = true;
           // 创建文字是在鼠标按下时创建,不可与绘制矩形箭头等图像一样放在鼠标移动时
           // 鼠标按下获取处于活动状态的对象 (此行代码的作用:防止鼠标在拖动时,调用drawing方法)
          if (_this.currentTool == "text" && !_this.fabricCanvas.getActiveObject()) {
            _this.drawText();
          }
        },
        // 鼠标移动
        "mouse:move": (e) => {
          // console.log('鼠标移动',e)
          if (_this.moveCount % 2 && !_this.doDrawing) {
            //减少绘制频率
            return;
          }
          _this.moveCount++;
          _this.mouseTo.x = e.pointer.x;
          _this.mouseTo.y = e.pointer.y;

          // 鼠标按下获取处于活动状态的对象 (此行代码的作用:防止鼠标在拖动时,调用drawing方法)
          if (!_this.fabricCanvas.getActiveObject()) {
            _this.drawing();
          }
        },
        // 鼠标抬起
        "mouse:up": (e) => {
          console.log("鼠标抬起", e);
          _this.mouseTo.x = e.pointer.x;
          _this.mouseTo.y = e.pointer.y;
          _this.moveCount = 1;
          _this.doDrawing = false;
          if (e.target || _this.drawingObject) {
            _this.fabricCanvas.discardActiveObject()
            _this.fabricCanvas.setActiveObject(e.target || _this.drawingObject)
            _this.fabricCanvas.requestRenderAll()
          } 
          _this.updateModifications(true);
        },
        // 选中画布上的对象
        "selection:created": (e) => {
          console.log('选中画布上的对象:',e)

          // 设置Object控制器样式
          e.target.set({
            transparentCorners: false,
            cornerColor: this.stroke,
            cornerSize: 12,
            cornerStyle: 'circle', // 设置Object控制器四角的样式为circle圆角
            padding: 10,
            borderColor: this.stroke, // 设置Object为激活状态时,控制器的边框颜色。默认值为 //rgba(102,153,255,0.75)。
            borderDashArray: [3, 3],
            // cornerStrokeColor: '#ff7a55',
          });

          _this.drawingObject = e.target;
          _this.strokeWidth = e.target.strokeWidth
          _this.stroke = e.target.stroke
          if (e.target.fontSize) {
            _this.textFontSize = e.target.fontSize
          }

          _this.removeFabricObj(e.target);
          // _this.fabricCanvas.discardActiveObject(); //清楚选中框
          // _this.updateModifications(true);
        },
        // 取消选中
        "selection:cleared":(e) => {
            console.log('取消选中',e)
            _this.drawingObject = null;
            _this.strokeWidth = _this.strokeWidth == 0 ? 1 : _this.strokeWidth
        },
        //对象移动
        "object:moving": (e) => {
          console.log("对象移动", e);
          e.target.opacity = 0.5;
          _this.noOutgoingLine(e,'moving'); // 禁止元素超出画布
        },
        // 对象缩放
        "object:scaling": (e) => {
          console.log('对象缩放',e)
        },
        "object:modified": (e) => {
          console.log("object:modified:", e);
          e.target.opacity = 1;
          _this.updateModifications(true);
        },
        //选中对象更新
        "selection:updated": (e) => {
            console.log("选中对象更新:", e);
        },
        //增加对象
        "object:added": (e) => {
          // console.log('增加对象:',e)
          // debugger
        },
      });
    },
    // 禁止元素超出画布
    noOutgoingLine(e,strType){
      if (strType == 'moving') {
        const padding = -10; // 内容距离画布的空白宽度,主动设置
        const obj = e.target;
        if(obj.currentHeight > obj.canvas.height || obj.currentWidth > obj.canvas.width){
            return;
        }        
        obj.setCoords();        
        // 处理条件, 如果元素距离听小于 padding 或者距离 左侧小于 padding  top-left  corner
        if(obj.getBoundingRect().top < padding || obj.getBoundingRect().left < padding){
          const toTop = (obj.top-obj.getBoundingRect().top) + padding
          const toLeft = (obj.left-obj.getBoundingRect().left) + padding
          // 如果元素只有一个条件在阀值,则另外一个条件正常处理
          obj.top = Math.max(obj.top, toTop);
          obj.left = Math.max(obj.left, toLeft);
        }

        // 元素距离上侧的距离 + 元素的高度, 计算元素距离底部的距离
        const toHeight = obj.getBoundingRect().top + obj.getBoundingRect().height;
        // 元素距离左侧的距离 + 元素的宽度 , 计算元素距离右边的距离
        const toWidth = obj.getBoundingRect().left + obj.getBoundingRect().width;
        // 处理条件  如果元素距离底部小于 padding, 或者距离右边小于padding  bottom-right corner
        if(toHeight > obj.canvas.height - padding || toWidth  > obj.canvas.width - padding){
          const toTop = obj.top - (toHeight - (obj.canvas.height - padding));
          const toLeft = obj.left - (toWidth - (obj.canvas.width - padding));
          obj.top = Math.min(obj.top, toTop);
          obj.left = Math.min(obj.left, toLeft);
        }
      }
    },
    // 注册键盘退格键删除
    keyDown() {
      document.onkeydown = (e) => {
        console.log(e)
        if (e && e.keyCode == 46 && this.drawingObject) {
            this.removeFabricObj(this.drawingObject,'backspaceKey')
        }
      };
    },
    // 删除选中的对象
    removeFabricObj(e_target,type) {
      console.log('删除选中的对象:',e_target)
        if (!e_target) {
            return
        }
      if (this.currentTool == "remove" || type == 'backspaceKey') {
        if (e_target._objects) {
          //多选删除
          var etCount = e_target._objects.length;
          for (var etindex = 0; etindex < etCount; etindex++) {
            this.fabricCanvas.remove(e_target._objects[etindex]);
          }
        } else {
          //单选删除
          this.fabricCanvas.remove(e_target);
        }
      }
    },
    //储存历史记录
    updateModifications(savehistory) {
      if (savehistory == true) {
        this.fabricHistoryJson.push(JSON.stringify(this.fabricCanvas));
      }
    },
    // 上一步
    undo() {
      let state = this.fabricHistoryJson;
      if (this.mods < state.length) {
        this.fabricCanvas.clear().renderAll();
        this.fabricCanvas.loadFromJSON(
          state[state.length - 1 - this.mods - 1]
        );
        this.fabricCanvas.renderAll();
        this.mods += 1;
      }
    },
    // 下一步
    redo() {
      let state = this.fabricHistoryJson;
      if (this.mods > 0) {
        this.fabricCanvas.clear().renderAll();
        this.fabricCanvas.loadFromJSON(
          state[state.length - 1 - this.mods + 1]
        );
        this.fabricCanvas.renderAll();
        this.mods -= 1;
      }
    },
    handleTools(tools, idx) {
      this.fabricCanvas.isDrawingMode = false
      this.fabricCanvas.selection = false
      this.fabricCanvas.skipTargetFind = false
      this.currentTool = tools.name;
      switch (tools.name) {
        case "pencil":
          this.fabricCanvas.isDrawingMode = true
          this.fabricCanvas.freeDrawingBrush.color = this.stroke; // 设置绘画笔的颜色
          this.fabricCanvas.freeDrawingBrush.width = this.strokeWidth; // 设置绘画笔的宽度
          break;
        case "remove":
        case "bj":
          this.fabricCanvas.selection = true
          break;
        case "redo":
          this.redo();
          break;
        case "undo":
          this.undo();
          break;
        case "reset":
          this.resetCon();
        case "bc":
          this.addAnnot();
        case "imageBase64":
          this.downLoadImage()
          break;
        default:
          break;
      }
    },
    drawing() {
      if (this.drawingObject) {
        this.fabricCanvas.remove(this.drawingObject);
      }
      let _this = this;
      let fabricObject = null;
      switch (this.currentTool) {
        case "line": // 直线
          fabricObject = new fabric.Line([
            this.mouseFrom.x,
            this.mouseFrom.y,
            this.mouseTo.x,
            this.mouseTo.y,
          ]);
          break;
        case "arrow": // 箭头
          fabricObject = new fabric.Path(
            this.drawArrow(
              this.mouseFrom.x,
              this.mouseFrom.y,
              this.mouseTo.x,
              this.mouseTo.y,
              17.5,
              17.5
          ));
          break;
        case "xuxian": //虚线
          fabricObject = new fabric.Line(
            [
              this.mouseFrom.x,
              this.mouseFrom.y,
              this.mouseTo.x,
              this.mouseTo.y,
            ],
            {
              strokeDashArray: [10, 3],
            }
          );
          break;
        case "wavyLine": // 波浪线
          // 方式一:绘制任意方向的波浪线
          fabricObject = new fabric.LineWithArrow([
            this.mouseFrom.x,
              this.mouseFrom.y,
              this.mouseTo.x,
              this.mouseTo.y,
          ])
          // 方式二:只可绘制水平波浪线
          // fabricObject = new fabric.Path(
          //   this.drawWavyLine(
          //     this.mouseFrom.x,
          //     this.mouseFrom.y,
          //     this.mouseTo.x,
          //     this.mouseTo.y,
          //     5,
          //     5
          //   )
          // );
          break;
        case "Highlight": // 高亮
        case "juxing": //矩形
          fabricObject = new fabric.Rect({
            top: Math.min(this.mouseFrom.y, this.mouseTo.y),
            left: Math.min(this.mouseFrom.x, this.mouseTo.x),
            width: Math.abs(this.mouseFrom.x - this.mouseTo.x),
            height: Math.abs(this.mouseFrom.y - this.mouseTo.y),
          });
          break;
        case "cricle": //正圆
          let radius = Math.sqrt(
            (this.mouseTo.x - this.mouseFrom.x) *
            (this.mouseTo.x - this.mouseFrom.x) +
            (this.mouseTo.y - this.mouseFrom.y) *
            (this.mouseTo.y - this.mouseFrom.y)
          ) / 2;
          fabricObject = new fabric.Circle({
            left: this.mouseFrom.x,
            top: this.mouseFrom.y,
            radius: radius,
          });
          break;
        case "ellipse": //椭圆
          let left = this.mouseFrom.x;
          let top = this.mouseFrom.y;
          fabricObject = new fabric.Ellipse({
            left: left,
            top: top,
            originX: "center",
            originY: "center",
            rx: Math.abs(left - this.mouseTo.x),
            ry: Math.abs(top - this.mouseTo.y),
          });
          break;
        case "equilateral": //等边三角形
          let height = this.mouseTo.y - this.mouseFrom.y;
          fabricObject = new fabric.Triangle({
            top: this.mouseFrom.y,
            left: this.mouseFrom.x,
            width: Math.sqrt(Math.pow(height, 2) + Math.pow(height / 2.0, 2)),
            height: height,
          });
          break;
        case "remove":
          break;
        default:
          break;
      }
      
      if (fabricObject) {
        // 设置Object其它属性
        fabricObject.set({
          customType: this.currentTool, // 自定义的类型,如果不需要可以删掉
          stroke: this.stroke,
          strokeWidth: this.strokeWidth,
          fill: "rgba(255,255,255,0)", // 绘制空心图形时fill为透明,绘制实心图形时fill需要设置颜色
          // hasControls: false, // 是否显示控制器  false不显示
        });
        if (this.currentTool == 'Highlight') {
            fabricObject.set('fill', this.stroke); // 本例中高亮其实就是一个实心的矩形图
            fabricObject.set('strokeWidth', 0); // 高亮的图形没有线宽
        }
        this.fabricCanvas.add(fabricObject);
        this.drawingObject = fabricObject;
      }
    },
    // 自定义波浪线
    LineWithArrow(){
      if (fabric.LineWithArrow) {
          fabric.warn('fabric.LineWithArrow is already defined.');
          return;
        }
        fabric.LineWithArrow = fabric.util.createClass(fabric.Line, {
          type: 'line_with_arrow',

          initialize: function(element, options) {
            options || (options = {});
            this.callSuper('initialize', element, options);

            // Set default options
            this.set({
              hasBorders: false,
              hasControls: false,
            });
          },

          _render: function(ctx) {
            // this.callSuper('_render', ctx);
            ctx.save();
            const xDiff = this.x2 - this.x1;
            const yDiff = this.y2 - this.y1;
            const angle = Math.atan2(yDiff, xDiff);
            ctx.translate(xDiff / 2, yDiff / 2);
            ctx.rotate(angle);
            ctx.beginPath();
            // Move 5px in front of line to start the arrow so it does not have the square line end showing in front (0,0)
            ctx.moveTo(5, 0);
            ctx.lineTo(-5, 5);
            ctx.lineTo(-5, -5);
            ctx.closePath();
            ctx.fillStyle = this.stroke;
            ctx.fill();
            ctx.restore();
            var p = this.calcLinePoints();
            var point = this.pointOnLine(this.point(p.x2, p.y2), this.point(p.x1, p.y1), 10)
            this.wavy(this.point(p.x1, p.y1), point, this.point(p.x2, p.y2), ctx);
            ctx.stroke();
          },

          point: function(x, y) {
            return {
              x: x,
              y: y
            };
          },

          wavy: function(from, to, endPoint, ctx) {
            var cx = 0,
              cy = 0,
              fx = from.x,
              fy = from.y,
              tx = to.x,
              ty = to.y,
              i = 0,
              step = 4,
              waveOffsetLength = 0,

              ang = Math.atan2(ty - fy, tx - fx),
              distance = Math.sqrt((fx - tx) * (fx - tx) + (fy - ty) * (fy - ty)),
              amplitude = -10,
              f = Math.PI * distance / 30;

            for (i; i <= distance; i += step) {
              waveOffsetLength = Math.sin((i / distance) * f) * amplitude;
              cx = from.x + Math.cos(ang) * i + Math.cos(ang - Math.PI / 2) * waveOffsetLength;
              cy = from.y + Math.sin(ang) * i + Math.sin(ang - Math.PI / 2) * waveOffsetLength;
              i > 0 ? ctx.lineTo(cx, cy) : ctx.moveTo(cx, cy);
            }
            ctx.lineTo(to.x, to.y);
            ctx.lineTo(endPoint.x, endPoint.y);
          },

          pointOnLine: function(point1, point2, dist) {
            var len = Math.sqrt(((point2.x - point1.x) * (point2.x - point1.x)) + ((point2.y - point1.y) * (point2.y - point1.y)));
            var t = (dist) / len;
            var x3 = ((1 - t) * point1.x) + (t * point2.x),
              y3 = ((1 - t) * point1.y) + (t * point2.y);
            return new fabric.Point(x3, y3);
          },

          toObject: function() {
            return fabric.util.object.extend(this.callSuper('toObject'), {
              customProps: this.customProps,
            });
          },
        })
    },
    // 绘制文字对象
    drawText() {
      this.drawingObject = new fabric.Textbox("", {
        left: this.mouseFrom.x,
        top: this.mouseFrom.y,
        width: 150,
        fontSize: this.textFontSize,
        fill: this.stroke,
        hasControls: false,
        type: "Watermark",
        customType: this.currentTool,
        stroke: this.stroke,
        lineHeight: 1,
        // fontWeight: "bold",// 设置文本的粗细
        textAlign: "left", // 文字对齐
        splitByGrapheme: true, // 拆分中文,可以实现自动换行
        objectCaching: false,
      });
      this.fabricCanvas.add(this.drawingObject);
      this.drawingObject.enterEditing();
      this.drawingObject.hiddenTextarea.focus();
      this.updateModifications(true);
    },
    // 绘制波浪线 (只可水平绘制)
    drawWavyLine(x1, y1, x2, y2, xOff, yOff) {
      let a = new Array();
      let xLen = (x2 - x1) / xOff;
      for(let i = 0; i < xLen; i++){
        a.push({x : x1 + xOff * i, y:i % 2 == 0 ? y1 + yOff : y1})
      }
      var path = ''
      for(let i = 1; i < a.length; i++){
        path += ` L ${a[i].x} ${a[i].y}`
      }
      if (a.length > 0) {
        path = `M ${a[0].x} ${a[0].y} ${path}`
      }
      return path;
    },
    // 绘制箭头
    drawArrow(fromX, fromY, toX, toY, theta, headlen, _type) {
      theta = typeof theta != "undefined" ? theta : 30;
      headlen = typeof theta != "undefined" ? headlen : 10;
      // 计算各角度和对应的P2,P3坐标
      let angle = (Math.atan2(fromY - toY, fromX - toX) * 180) / Math.PI,
        angle1 = ((angle + theta) * Math.PI) / 180,
        angle2 = ((angle - theta) * Math.PI) / 180,
        topX = headlen * Math.cos(angle1),
        topY = headlen * Math.sin(angle1),
        botX = headlen * Math.cos(angle2),
        botY = headlen * Math.sin(angle2);
      let arrowX = fromX - topX,
        arrowY = fromY - topY;
      let path = "";
      if (_type == "arrowAdd") {
        var pathObj = {
          M1: { x: fromX, y: fromY },
          L2: { x: toX, y: toY },
          M3: { x: toX + topX, y: toY + topY },
          L4: { x: toX, y: toY },
          L5: { x: toX + botX, y: toY + botY },
        };
        return pathObj;
      }
      path = "M " + fromX + " " + fromY;
      path += " L " + toX + " " + toY;
      arrowX = toX + topX;
      arrowY = toY + topY;
      path += " M " + arrowX + " " + arrowY;
      path += " L " + toX + " " + toY;
      arrowX = toX + botX;
      arrowY = toY + botY;
      path += " L " + arrowX + " " + arrowY;
      console.log("path:", path);
      return path;
    },
    // 获取绘制好的所有对象数据
    addAnnot() {
      console.log(this.fabricCanvas.getObjects()) // 获取画布容器中的所有对象
    },
    downLoadImage() {
      //生成双倍像素比的图片
      let base64URl = this.fabricCanvas.toDataURL({
          formart: 'png',
          multiplier: 1, // 倍数
      })
      this.imageBase64 = base64URl
      console.log(base64URl)
    },
  },
  components: {},
};
</script>
 
<style scoped>
/* 去除 el-input type为number时,输入框中的上下滚动按钮 */
::v-deep ._inp input::-webkit-outer-spin-button,
::v-deep ._inp input::-webkit-inner-spin-button {
  -webkit-appearance: none;
}

/* 工具栏 start */
.controlPanel {
  width: 100%;
  display: flex;
  align-items: center;
  white-space: nowrap;
  flex-wrap: wrap;
}
.controlPanel ._inp .el-input{
  width: 70%;
}
.controlPanel .contro-item {
  margin-left: 1.5rem;
  text-align: center;
  cursor: pointer;
}
.controlPanel .active {
  color: rgba(30, 144, 255, 1);
}
.controlPanel .contro-item .picker{
  width: 25px;
  height: 25px;
  border: 2.5px solid transparent;
  border-radius: 50%;
  background-clip: padding-box, border-box;
  background-origin: padding-box, border-box;
  background-image: linear-gradient(to right, rgba(30, 144, 255, 1), rgba(30, 144, 255, 1), linear-gradient(90deg, #8F41E9, #578AEF));
}
/* 工具栏 end */


/* 绘图区 start */
.canvas-wraper {
  border: 1px solid;
  height: 100%;
  width: 100%;
  overflow: hidden;
  margin: 0 auto;
  margin-top: 4px;
}
.canvas-wraper .title{
  text-align: center;
}
/* 绘图区 end */


/* 图片展示区 start */
.imgbase{
  border: 1px solid;
  margin-top: 1rem;
}
.imgbase .title{
  text-align: center;
}
/* 图片展示区 end */
</style>