Create signature component of canvas

Time:2021-3-8

This article first appeared on CSDN website, and the following version has been further revised.
Original text:Create signature component of canvas

Reading guide

June is another time when the project is tight, a big wave of demand hit, caught off guard.

After a long and painful June, it’s time to sum up a wave. Recently, a mobile product was originally planned to introduce a third-party signature plug-in. The plug-in is complex and can be used by several JSdocument.writeSequential loading, plug-in source code is Es5, even Es3 is not too much. In order to successfully embed our Vue project, I read the source code of the plug-in for two days (demo and incomplete documentation, embarrassing), and then spent more than a day using ES6 to reference it. In view of the guiding principle that any non global resources should not be loaded in advance in single page applications, in order to achieve dynamic loading, I even wrote a simple Vue componentiload.jsTo load these resources in order and execute callbacks. Everything seems perfect. It turns out that the ID and style of the DOM node related to the plug-in are written dead in a compressed JS referenced by demo. At the moment, my heart is almost broken. I’m afraid I can’t introduce such a plug-in.

Create signature component of canvas

Although I said so, my body is very honest, and I put this plug-in into the project after a lot of hard work. With the progress of the project, after many times of business communication, we cut off the digital certificate verification part of the signature plug-in. In other words, such a large plug-in only has the function of user signature. I can do it myself. So I quietly removed this plug-in, marking a perfect end to the research and coding process in recent days.

Signature is a collection of several operations, starting from the user’s handwritten name and finally uploading the signed image, which also includes image processing, such as reducing jagged, rotating, shrinking, previewing, etc. Canvas is almost the most suitable solution.

Handwriting

From the interactive point of view, the process of user signature, only the beginning of the handwritten part is interactive, followed by automatic processing. In order to complete handwriting, you need to listen to two events on the canvas: touchstart and touchmove (the mobile terminal touchend is not triggered after touchmove). The former defines the starting point, while the latter keeps drawing lines.

const canvas = document.getElementById('canvas');
const touchstart = (e) => {
  /*Todo definition starting point*/
};
const touchmove = (e) => {
  /*Todo connects points into lines and fills them with colors*/
};
canvas.addEventListener('touchstart', touchstart);
canvas.addEventListener('touchmove', touchmove);

Note: the following default canvas and context objects already exist.

You can first poke here to experience the signature components that will be mentioned latercanvas-draw

Line drawing

Since you want to connect the points into a line, you naturally need a variable to store these points.

const point = {};

Next is the line drawing part. Canvas only needs 4 lines of code to draw lines:

  1. Beginpath

  2. Positioning starting point (moveto)

  3. Mobile brush (lineto)

  4. Draw path (stroke)

Considering the two actions of start and move, a line drawing method is ready to come out, as follows:

const paint = (signal) => {
  switch (signal) {
    Case 1: // start path
      context.beginPath();
      context.moveTo(point.x, point.y);
    Case 2: // the reason why there is no break statement is that you can draw a point when you click
      context.lineTo(point.x, point.y);
      context.stroke();
      break;
  }
};

Binding events

In order to meet the similar requirements of PC, it is necessary to distinguish the platforms. On the mobile side, touch start and touch move need to be bound for finger operation; on the PC side, MouseDown and MouseMove need to be bound for mouse operation. The following line of code can be used to determine whether the mobile terminal is mobile or not:

const isMobile = /(phone|pad|pod|iPhone|iPod|ios|iPad|Android|Mobile|BlackBerry|IEMobile|MQQBrowser|JUC|Fennec|wOSBrowser|BrowserNG|WebOS|Symbian|Windows Phone)/i.test(navigator.userAgent);

After the line drawing method is ready, the rest is to record the current crossing point at an appropriate time, and call the paint method to draw. An event generator can be abstracted here

Let pressed = false; // indicates whether the mouse down or finger down event occurs
const create = signal => (e) => {
  if (signal === 1) {
    pressed = true;
  }
  if (signal === 1 || pressed) {
    e = isMobile ? e.touches[0] : e;
    Point. X = e.clientx - left + 0.5; // if you do not add 0.5, draw a straight line at the integer coordinates, and the width of the straight line will be increased by 1px
    point.y = e.clientY - top + 0.5;
    paint(signal);
  }
};

The left and top in the above code are not built-in variables. They represent the pixel distances between the canvas and the left and top of the screen, respectively. They are mainly used to convert screen coordinate points into canvas coordinate points. Here’s a way to get it:

const { left, top } = canvas.getBoundingClientRect();

Obviously, the above event generator is a high-order function to solidify the signal parameter and return a new function. Based on this, the start and move callbacks are presented.

const start = create(1);
const move = create(2);

In order to avoid excessive UI rendering and make the move operation run more smoothly, the optimization of request animation frame is inevitable.

const requestAnimationFrame = window.requestAnimationFrame;
const optimizedMove = requestAnimationFrame ? (e) => {
  requestAnimationFrame(() => {
    move(e);
  });
} : move;

The rest is also a key step in binding events. On the PC side, MouseDown and MouseMove have no sequence. Not every mouse movement on the canvas is an effective operation. Therefore, we use the pressed variable to ensure that the MouseMove event callback is only executed after the MouseDown event. In fact, the set pressed variable always needs to be restored. The opportunity of restoration is mouseup and mouseleave callback. Because mouseup events are not always triggered (for example, mouseup events of other nodes are triggered when the mouse moves to other nodes), mouseleave is the underlying logic when the mouse moves out of the canvas. The natural continuity of touch events on the mobile terminal ensures that touchmove will only be triggered after touchstart, so there is no need to set the pressed variable or restore it. The code is as follows:

if (isMobile) {
  canvas.addEventListener('touchstart', start);
  canvas.addEventListener('touchmove', optimizedMove);
} else {
  canvas.addEventListener('mousedown', start);
  canvas.addEventListener('mousemove', optimizedMove);
  ['mouseup', 'mouseleave'].forEach((event) => {
    canvas.addEventListener(event, () => {
      pressed = false;
    });
  });
}

rotate

If you want to sign on the mobile terminal, you often face the embarrassment of insufficient screen width. I can’t write a few Chinese characters on the vertical screen, even three. If the app WebView or browser does not support horizontal screen display, it does not mean that there is no way. At least we can rotate the whole web page 90 degrees.

Scheme 1: at first, my idea was to rotate the canvas 90 ° together. Later, it was found that it was difficult to deal with the corresponding relationship between the rotated coordinate system and the screen coordinate system. Therefore, I adopted the scheme of rotating the canvas 90 ° to draw the page, but laying out the canvas normally, so as to ensure the consistency of the coordinate system (in this way, there is no need to correct the coordinate system of the canvas again, and there is no need to correct the coordinate system later There is plan 2, please read it patiently.

Because the user operates the canvas horizontally, the image needs to be rotated 90 ° counterclockwise to upload to the server after signing. So there is still a way to rotate. In fact, the rotate method can rotate the canvas, and the DrawImage method can draw an image or an old canvas in a new canvas, which is highly customized.

rotate

Rotate is used to rotate the current canvas.

Grammar:rotate(angle)Angle represents the radian of rotation. Here, the angle needs to be converted into radian calculation. For example, rotate 90 ° clockwise, and the value of angle is equal to-90 * Math.PI / 180. When ratate rotates, the center is the upper left corner of the canvas by default. If you need to center on the center of the canvas, you need to move the coordinate origin of the canvas to the center before executing the rotate method, and then move it back after the rotation. As follows:

const { width, height } = canvas;
context.translate (width / 2, height / 2); // the coordinate origin is moved to the center of the canvas
context.rotate (90 *  Math.PI  /180); // rotate 90 ° clockwise
context.translate (- width / 2, - height / 2); // the coordinate origin is restored to the starting position

In fact, this transformation process usestransform(Math.cos(90 * Math.PI / 180), 1, -1, Math.cos(90 * Math.PI / 180), 0, 0)It can also be rotated 90 ° clockwise.

drawImage

DrawImage is used to draw pictures, canvases or videos. You can customize the width, height, position and even local clipping. It has three forms of API:

  • drawImage(img,x,y), x, y are the coordinates in the canvas. Img can be an image, canvas or video resource, indicating that it is drawn at the specified coordinates of the canvas.

  • drawImage(img,x,y,width,height), width, height indicates the width and height of the specified picture after drawing (you can zoom or adjust the width and height ratio arbitrarily).

  • context.drawImage(img,sx,sy,swidth,sheight,x,y,width,height), SX, sy means that the original image is cropped from the specified coordinate position, and the width of swidth and the height of height are cropped.

In general, we may need to rotate a picture 90 degrees, 180 degrees or – 90 degrees. The code is as follows:

const rotate = (degree, image) => {
  degree = ~~degree;
  if (degree !== 0) {
    const maxDegree = 180;
    const minDegree = -90;
    if (degree > maxDegree) {
      degree = maxDegree;
    } else if (degree < minDegree) {
      degree = minDegree;
    }

    const canvas = document.createElement('canvas');
    const context = canvas.getContext('2d');
    const height = image.height;
    const width = image.width;
    const angle = (degree * Math.PI) / 180;

    switch (degree) {
      //Rotate 90 ° counterclockwise
      case -90:
        canvas.width = height;
        canvas.height = width;
        context.rotate(angle);
        context.drawImage(image, -width, 0);
        break;
      //Rotate 90 ° clockwise
      case 90:
        canvas.width = height;
        canvas.height = width;
        context.rotate(angle);
        context.drawImage(image, 0, -height);
        break;
      //Rotate 180 ° clockwise
      case 180:
        canvas.width = width;
        canvas.height = height;
        context.rotate(angle);
        context.drawImage(image, -width, -height);
        break;
    }
    image = canvas;
  }
  return image;
};

zoom

The rotated canvas usually needs to further format its width and height to upload. Here, we use DrawImage to change the width and height of the canvas to achieve the purpose of reducing and enlarging. As follows:

const scale = (width, height) => {
  const w = canvas.width;
  const h = canvas.height;
  width = width || w;
  height = height || h;
  if (width !== w || height !== h) {
    const tmpCanvas = document.createElement('canvas');
    const tmpContext = tmpCanvas.getContext('2d');
    tmpCanvas.width = width;
    tmpCanvas.height = height;
    tmpContext.drawImage(canvas, 0, 0, w, h, 0, 0, width, height);
    canvas = tmpCanvas;
  }
  return canvas;
};

upload

We have done so many operations and transformations, and the ultimate goal is to upload pictures.

First, get the image in the canvas

const getPNGImage = () => {
  return canvas.toDataURL('image/png');
};

The getpngimage method returns the dataurl, which needs to be converted to a blob object to upload. As follows:

const dataURLtoBlob = (dataURL) => {
  const arr = dataURL.split(',');
  const mime = arr[0].match(/:(.*?);/)[1];
  const bStr = atob(arr[1]);
  let n = bStr.length;
  const u8arr = new Uint8Array(n);
  while (n--) {
    u8arr[n] = bStr.charCodeAt(n);
  }
  return new Blob([u8arr], { type: mime });
};

After completing the above, a wave of Ajax requests (XHR, fetch, Axios) can take the signature image away.

const upload = (blob, url, callback) => {
  const formData = new FormData();
  const xhr = new XMLHttpRequest();
  xhr.withCredentials = true;
  formData.append('image', blob, 'sign');

  xhr.open('POST', url, true);
  xhr.onload = () => {
    if ((xhr.status >= 200 && xhr.status < 300) || xhr.status === 304) {
      callback(xhr.responseText);
    }
  };
  xhr.onerror = (e) => {
    console.log(`upload img error: ${e}`);
  };
  xhr.send(formData);
};

set up

After completing the above functions, a signature plug-in has been formed. Unless you can’t wait to release, I don’t recommend taking out such code. Some necessary settings usually cannot be ignored.

Usually, the line in the canvas is 1px in size. Such a thin line can’t simulate strokes. However, if you want to zoom in to 10px, you will find that the drawn line is actually a rectangle. This is also inappropriate in the signature process. We expect smooth strokes, so we need to simulate handwriting as much as possible. In fact, linecap can specify the smooth end of the line, and linejoin can specify the smooth corners when the lines meet. Here is a simple setting:

context.lineWidth  =10; // line width
context.strokeStyle  ='Black'; // the color of the path
context.lineCap  ='round'; // the end of the line is smooth
context.lineJoin  ='round'; // when two lines meet, create a circular corner
context.shadowBlur  =1; // the edge is blurred to prevent the straight line edge from serration
context.shadowColor  ='Black'; // edge color

optimization

Everything seemed perfect until I met the retina screen. Retina screen is a canvas with four physical pixels to draw a virtual pixel and the same screen width. Each pixel of retina screen will be drawn by four times the physical pixel. With the increase of the distance between the midpoint and the point of the canvas, obvious sawtooth will appear. This problem can be solved by enlarging the canvas and then compressing the display.

let { width, height } = window.getComputedStyle(canvas, null);
width = width.replace('px', '');
height = height.replace('px', '');

//Optimizing canvas drawing based on device pixel ratio
const devicePixelRatio = window.devicePixelRatio;
if (devicePixelRatio) {
  canvas.style.width = `${width}px`;
  canvas.style.height = `${height}px`;
  canvas.height  =Height * devicepixelratio; // enlarge the width and height of the canvas
  canvas.width = width * devicePixelRatio;
  context.scale (devicepixelratio, devicepixelratio); // enlarge the canvas content by the same magnification
} else {
  canvas.width = width;
  canvas.height = height;
}

Reset coordinate system

Due to the adoption of scheme 1, the work flow of signature is as follows: “the page is rotated 90 ° clockwise to draw, and the canvas is drawn vertically” > “handwritten signature” > “the canvas is rotated 90 ° counterclockwise” > “reasonable scaling of the canvas to the screen width” > “export image and upload”. It can be seen that the process of scheme 1 is complex, and it is troublesome to deal with.

From another perspective, since the canvas can be rotated, I can just use the reverse rotation of the coordinate system to offset the positive rotation of the page, so that the coordinates of the points on the page can be mapped to the coordinates of the canvas itself. So there is the second plan.

Scheme 2: the page rotates 90 ° clockwise and the canvas rotates with it (the coordinate system of the canvas also rotates 90 °); then the canvas rotates 90 ° reversely to reset the coordinate system of the canvas and map it to the page coordinate system.

The page rotated 90 ° clockwise is as follows:

Create signature component of canvas

At this time, the canvas rotates 90 ° clockwise with the page. If you want to reset the canvas coordinate system, you can rotate 90 ° backward by rotate, and then translate the coordinate system. The following code contains the processing of rotating 90 ° and 180 ° clockwise (for the sake of description, assume that the canvas is full of screen)

context.rotate((degree * Math.PI) / 180);
switch (degree) {
  //When the page is rotated 90 ° clockwise, the origin position of the upper left corner of the canvas falls to the upper right corner of the screen (at this time, the width and height are exchanged). When the page is rotated 90 ° counterclockwise around the origin, the canvas is vertical to the original position and on the right side of the screen, so you need to move the canvas to the left by the same distance as the current height.
  case -90:
    context.translate(-height, 0);
    break;
  //When the page is rotated 90 ° counterclockwise, the origin position of the upper left corner of the canvas falls to the lower left corner of the screen (at this time, the width and height are exchanged). When the page is rotated 90 ° clockwise around the origin, the canvas is vertical to the original position and is on the lower side of the screen, so it is necessary to move up the same distance as the current width of the canvas.
  case 90:
    context.translate(0, -width);
    break;
  //The page rotates 180 ° clockwise to return to the same position (i.e. page Handstand), and the origin position of the upper left corner of the canvas falls to the lower right corner of the screen (at this time, the width and height remain unchanged). After rotating 180 ° in the opposite direction around the origin, the canvas is parallel to the original position, and is located on the lower side of the right side of the screen. You need to move the same distance of the canvas width to the left and the same distance of the canvas height to the right.
  case -180:
  case 180:
    context.translate(-width, -height);
}

With the ability to reset the canvas coordinate system, it is feasible to rotate the canvas 90 ° or even 180 ° counterclockwise. As follows:

Create signature component of canvas

Create signature component of canvas

Of course, after resetting the canvas coordinate system, you need to pay attention to that when clearing the screen, the range of clearing the screen may also change. You need to do the following.

const clear = () => {
  let width;
  let height;
  switch ( this.degree ) { //  this.degree Is the degree of rotation of the canvas coordinate system
    case -90:
    case 90:
      width =  this.height ; // the height of the canvas before rotation
      height =  this.width ; // width before canvas selection
      break;
    default:
      width = this.width;
      height = this.height;
  }
  this.context.clearRect(0, 0, width, height);
};

Scheme 1 is simple and crude. In terms of layout, canvas does not need to rotate, but it needs to position the layout absolutely, which brings inconvenience to the visual display of the page. At the same time, before uploading the image, it needs to rotate and zoom the image, which makes the process complex.

The second scheme uses the method of correcting the canvas coordinate system, which saves the special processing on the layout and pictures, so the second scheme is better.

The above code can be found here:canvas-draw, this is a shell built with the help of Vue cli, mainly for the convenience of debugging, see the core codecanvas-draw/draw.jsIf you like, you may as well lighten star.


This question discusses so many contents. If you have any questions or good ideas, please leave comments below

The author of this paper:louis

Link to this article:http://louiszhai.github.io/20…

Reference article:

Recommended Today

What is “hybrid cloud”?

In this paper, we define the concept of “hybrid cloud”, explain four different cloud deployment models of hybrid cloud, and deeply analyze the industrial trend of hybrid cloud through a series of data and charts. 01 introduction Hybrid cloud is a computing environment that integrates multiple platforms and data centers. Generally speaking, hybrid cloud is […]