如何基于canvas做出一个绘制板工具

274 阅读4分钟

点这里:本文仓库地址

界面:


这是一个基于vue2.x的绘制板工具。通过该工具,您可以在图片上标记您想要的信息,并获得相应的数据。此外,您还可以将其作为一个普通的画板来使用,您可以在上面自由地绘制图形。当前支持矩形画图。可以支持放大、缩小、旋转、平移等操作。此外,您可以灵活地配置您的绘图信息。

结构

D:.
├─lib
│  └─fonts
├─packages
│  └─DrawBoard
│      ├─assets
│      ├─components
│      ├─draw
│      ├─styles
│      └─utils
└─public

其中,packages\index.js为入口文件

参数选项

参数类型备注
locationDetileObject根据输入数据渲染图形
urlString要编辑的图像的URL
isFocusBoolean是否传入了当前修改框的坐标对象
pointObject当前修改框的坐标对象
loadingDataBoolean控制是否显示图像加载动画

数据格式特殊说明

locationDetile

根据输入数据渲染图形。数据格式如下:

{
    example1:[
        110,
        220,
        330,
        140,
        150,
        160,
        180,
        300
    ],
    example2:[
        110,
        220,
        330,
        140,
        150,
        160,
        180,
        300
    ]
}

point

point是根据实际业务要求,从父组件传入的,当前应修改框的位置信息对象,并在图上对应显示当前修改框的位置。你也可以根据自己的要求灵活更改。数据格式如下:

{
    location:[
    	110,
        220,
        330,
        140,
        150,
        160,
        180,
        300
    ]
}

关键流程图

url图片链接

locationDetile位置信息

功能详解

packages\DrawBoard\main.vue

初次渲染

mounted() {
    this.initSize();
    this.observerView();
    this.canvas.addEventListener(
      "mousemove",
      this.drawNavigationLineEvent,
      false
    );
    var that = this;
    window.onresize = () => {
      return (() => {
        if (this.url) {
          this.clearAll1();
          this.loading = true;
          this.loadImage(this.url);
          that.drawOnePoint();
        }
        this.image.style.transform =
          "scale(1, 1) translate(0px, 0px) rotateZ(3600deg)";
        this.clearAll1();
        setTimeout(function () {
          that.drawAll();
          that.drawOnePoint();
        }, 800);
      })();
    };
    if (!this.isMounted) {
      setTimeout(function () {
        that.drawOnePoint();
      }, 900);
    }
  },
  1. initSize()确定canvas的宽高等信息
  2. 监听画布上鼠标移动事件
  3. 因为canvas有个特性,画布宽高改变的时候会清空画布,那么原本画在画布上的图片会消失,所以这里加了一个监听浏览器大小的事件window.onresize,在其中,分别进行了清空画布、开启懒加载、加载url图片、根据数据源画框、选取第一个画框的操作
  4. 需要注意的:
    • 因为业务需求里,需要动态传入url和locationDetile,并且要求一切(针对图片的缩放平移旋转)都恢复初始态,所以每次都需要给canvas加上this.image.style.transform = "scale(1, 1) translate(0px, 0px) rotateZ(3600deg)";。这个根据自己的业务要求修改。
    • 因为每次根据url在画布上画出图片(loadImage())需要时间,所以根据数据源画框drawAll()这一动作需要在图片加载成功之后。有很多种方式,我这里使用了最笨的定时器。这个根据自己的业务要求修改。

新增功能

  1. 传入一个坐标点对象,通过计算,根据输入数据渲染图形

    • 通过watch监听器监听locationDetile属性:

      watch:{
      	locationDetile: {
            handler() {
              if (Object.keys(this.locationDetile).length !== 0) {
                var that = this;
                this.clearAll1();
                setTimeout(function () {
                  that.drawAll();
                  that.drawOnePoint();
                }, 800);
                this.lastPoint = [];
                this.point = [];
              } else {
              }
            },
            deep: true,
            immediate: true,
          },
      }
      

    注意:为了避免监听器反复监听,在图片上进行多次标注,所以每次画框都需要清空一次画布

    • 通过drawAll()按照原始图片比例,换算成当前画布比例,这样就保证了无论画布显示多大,标注框都能在正确位置:

          //统一画框
          drawAll() {
            this.image.style.transform =
              "scale(1, 1) translate(0px, 0px) rotateZ(3600deg)";
            this.clearAll1();
            const canvas = document.getElementById("canvas");
            const ctx = canvas.getContext("2d");
            const img = document.getElementById("image");
            const ctx1 = img.getContext("2d");
            for (let key in this.locationDetile) {
              if (key !== "imgSize" && this.locationDetile[key] !== "Null") {
                const firstPoint = {
                  x: this.locationDetile[key][0] * this.imageScale + this.imagePosX,
                  y: this.locationDetile[key][1] * this.imageScale + this.imagePosY,
                };
                const rectSize = {
                  w:
                    (this.locationDetile[key][2] - this.locationDetile[key][0]) *
                    this.imageScale,
                  h:
                    (this.locationDetile[key][5] - this.locationDetile[key][1]) *
                    this.imageScale,
                };
                this.activeGraphic = figureFactory(
                  this.currentTool,
                  { x: firstPoint.x, y: firstPoint.y },
                  this.options
                );
                this.activeGraphic.points = [
                  { x: firstPoint.x, y: firstPoint.y },
                  { x: firstPoint.x + rectSize.w, y: firstPoint.y },
                  { x: firstPoint.x + rectSize.w, y: firstPoint.y + rectSize.h },
                  { x: firstPoint.x, y: firstPoint.y + rectSize.h },
                ];
                this.graphics.push(this.activeGraphic);
                this.activeIndex = this.graphics.length - 1;
                this.activeGraphic.drawMyPoint(ctx, firstPoint, rectSize);
              }
            }
            this.drawBG();
            this.drawGraphics();
            this.loading = false;
          },
      

      imageScale:图片实际大小/图片真实大小

      imagePosXimagePosY:图片偏移量大小;以上三者都是初次渲染时,在加载图片的函数中计算得出。

      figureFactory():新建一个标注框对象,具体对象信息在packages\DrawBoard\draw\figureFactory.js,这里需要将四个坐标点以及配置信息注入该对象。并且调用对象中的drawMyPoint()方法去渲染出该标注框。

  2. 默认在图上画出标注框后,操作状态为修改,并选中第一个标注框

    drawOnePoint() {
          for (var i = 0; i < this.graphics.length; i++) {
            if (this.graphics[i].x + "" != "NaN") {
              this.graphics[i].drawPoints(this.canvasCtx);
              this.activeIndex = i;
              this.currentStatus = status.UPDATING;
              this.drawBG();
              this.drawGraphics();
              break;
            }
          }
        },
    

  3. 添加一个新增标注标识isFocus,当isFocus为true时,画布自动切换为新增标注状态,并且在上方显示“标注”图标

    watch:{
        isFocus: {
          handler() {
            if (this.isFocus) {
              this.activeIndex = -1;
              this.readyForNewEvent("draw");
            } else {
            }
          },
          immediate: true,
        },
    }
    
  4. 添加一个point属性,当point有值传入的时候,可以判断出该位置是否属于已经标注好的框中,并且默认选中修改。

    • 监听器
    watch:{
        point: {
          handler() {
            if (Object.keys(this.point).length !== 0) {
              if (this.point !== this.lastPoint) {
              } else {
              }
              this.lastPoint = this.point;//记录上一个点
              this.drawAll();
              this.updatePoint();
              this.drawGraphics();
            } else {
              this.clearAll1()
              this.drawAll();
              this.drawBG();
              this.currentStatus = status.MOVING;
            }
          },
          deep: true,
          immediate: true,
        },
    }
    
    • 判断是否在框内
        updatePoint() {
          for (let i = 0; i < this.graphics.length; i++) {
            if (
              this.graphics[i].isInPath(
                this.canvasCtx,
                this.transPoint(this.point.location[0], this.point.location[1])
              ) > -1
            ) {
              this.canvas.style.cursor = "crosshair";
              this.activeGraphic = this.graphics[i];
              console.log("activeIndex", this.activeIndex);
              this.activeIndex = i;
              this.currentStatus = status.UPDATING;
              this.drawBG();
              break;
            }
          }
        },
    
    • 关键函数isInPath()
    //packages\DrawBoard\draw\figureFactory.js
    isInPath(ctx, point) {
        for (let i = 0; i < this.points.length; i++) {
          // 通过清空子路径列表开始一个新路径的方法。 当你想创建一个新的路径时,调用此方法。
          ctx.beginPath();
          //绘制圆弧路径的方法
          ctx.arc(this.points[i].x, this.points[i].y, this.point_radis, 0, Math.PI * 2, false);
          if (ctx.isPointInPath(point.x, point.y)) {
            return i;
          }
        }
        // in the figure
        this.createPath(ctx);
        if (ctx.isPointInPath(point.x, point.y)) {
          return 999;
        }
        return -1;
      }