preface

This article will begin to introduce the development process of image annotation tool in detail. It mainly includes layout, event type, graphic design and so on.

layout

The layout of the tool is relatively simple. Divided into upper, middle and lower parts:

  • Top toolbar: Includes zoom in, zoom out, rotate, pan, full screen, clear, configure, and other operations.
  • Middle drawing area: drawing area for the main picture display area and data graphics.
  • Bottom custom: Essentially a slot for users to add their own logic. Such as: loading pictures, annotation data processing, etc.

Obviously, the middle drawing area is the most important part, and it is divided into two parts:

  • Left graphics selection area: you can select different graphics, so as to redraw the area for drawing.
  • Graphics display and rendering area: used to display graphics, and graphics on the image.

The resulting layout is shown below:

There is a special point to note here: if we display the image on the canvas and draw the image directly on the canvas, it will affect the original image display when we need to undo the edit operation. Therefore, during the tool design, two canvases are set up, one is used to present the image and the other is used to draw the graph. Both of them achieve overlapping layout through absolute positioning with the parent element.

The graphics class

The core of icon annotation tool lies in the drawing of different graphics. What I did was to design a superclass, a graphics class, from which all other concrete shapes (rectangles, polygons, etc.) would be inherited. The design of the graphics class is as follows:

const config = {
  PATH_LINEWIDTH:1.PATH_STROKESTYLE:"#f00".POINT_LINEWIDTH:2.POINT_STROKESTYLE:"# 999".POINT_RADIS:5
}

class Graph {
  // Constructor: contains the basic information of the graph
  constructor(point,options={}){
    this.x = Math.round(point.x)
    this.y = Math.round(point.y)
    this.points = []
    this.points.push(point)
    this.options = options
    this.path_lineWidth = options.path_lineWidth || config.PATH_LINEWIDTH
    this.path_strokeStyle = options.path_strokeStyle || config.PATH_STROKESTYLE
    this.point_radis = options.point_radis|| config.POINT_RADIS
    this.point_lineWidth = options.point_lineWidth || config.POINT_LINEWIDTH
    this.point_strokeStyle = options.point_strokeStyle || config.POINT_STROKESTYLE
  }
  // Calculate the center point of the graph: this is used when translating the graph
  computedCenter() {
    let x_sum = 0,y_sum = 0;
    this.points.forEach(p= > {
      x_sum += p.x;
      y_sum += p.y;
    });
    this.x = Math.round(x_sum / this.points.length);
    this.y = Math.round(y_sum / this.points.length);
  }
  // Move the method
  move(startPoint,endPoint) {
    let x1 = endPoint.x - startPoint.x;
    let y1 = endPoint.y - startPoint.y;
    this.points = this.points.map(item= > {
      return {
        x: item.x + x1,
        y: item.y + y1,
      }
    })
    this.computedCenter()
  }
  // Click update method
  update(i,point){
    this.points[i] = point
    this.computedCenter()
  }
  // Create a graph based on the basic elements of the graph
  createPath(ctx) {
    ctx.beginPath();
    ctx.lineWidth = this.path_lineWidth;
    ctx.strokeStyle = this.path_strokeStyle;
    this.points.forEach((p, i) = > {
      ctx[i == 0 ? "moveTo" : "lineTo"](p.x, p.y);
    });
    ctx.closePath();
  }
  // Whether in path: used to update and migrate graphics
  isInPath(ctx, point) {
    // in the 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)) {
        returni; }}// in the figure
    this.createPath(ctx);
    if (ctx.isPointInPath(point.x, point.y)) {
      return 999;
    }
    return -1;
  }
  // Draw control points to display when the graph is selected
  drawPoints(ctx) {
    ctx.save();
    ctx.lineWidth = this.point_lineWidth;
    ctx.strokeStyle = this.point_strokeStyle;
    ctx.fillStyle = this.point_strokeStyle;
    this.points.forEach(p= > {
      ctx.beginPath();
      ctx.moveTo(p.x - this.point_radis, p.y - this.point_radis);
      ctx.lineTo(p.x - this.point_radis, p.y + this.point_radis);
      ctx.lineTo(p.x + this.point_radis, p.y + this.point_radis);
      ctx.lineTo(p.x + this.point_radis, p.y - this.point_radis);
      ctx.closePath();
      ctx.fill();
    });
    ctx.restore();
  }
  // Draw a graph
  draw(ctx) {
    ctx.save();
    this.createPath(ctx); ctx.stroke(); ctx.restore(); }}Copy the code

Other graphics, if there is a different method from the parent class can be overwritten in their own, the specific rewriting method can see the source code.

Events and States

There are three main events in image annotation tools:

  • mousedown
  • mousemove
  • mouseup

There are four main states of image annotation tools:

  • DRAWING
  • UPDATING.
  • MOVING
  • DEFAULT

According to the above three events and four states, different actions are performed in different events and states. This corresponds to canvasMousedown, canvasMousemove and canvasMouseup in the source code, which is the core code of the image annotation tool.

Coordinate system transformation

Of the pit

  • Hand in hand, take you to make an image annotation tool with Canvas (I)
  • Hand in hand, take you to make an image annotation tool with Canvas (II)