preface

  1. This paper makes some improvements on the basis of canvas’s function.

  2. You can learn about Canvas through the Canvas_API.

  3. The code uses ES6 syntax directly and works as expected on The Google browser (version 76.0.3809.100).

The implementation process

Add elements

Add action buttons and canvas elements to the HTML.

  <div id="operations">
    <input type="button" id="pencil" value="Pencil"/>
    <input type="button" id="straightLine" value="Linear"/>
    <input type="button" id="rectangle" value="Rectangle"/>
    <input type="button" id="solidRectangle" value="Solid rectangle"/>
    <input type="button" id="circle" value="Circular"/>
    <input type="button" id="solidCircle" value="Solid circle"/>
    <input type="button" id="eraser" value="Eraser"/>
    <input type="button" id="image" value="Import picture"/>
    <input type="button" id="save" value="Save"/>
    <input type="button" id="redo" value="Repeat"/>
    <input type="button" id="undo" value="Cancel"/>
    <input type="button" id="clear" value="Clear"/>
    <label>Color:<input type="color" id="color" /></label>
    <label>Line thickness: 1<input type="range" id="lineWidth" min="1" max="100" value="1"/>100</label>
    <input type="file" id="imageFile" name="image"/>
    <a id="downloadLink"></a>
  </div>
  <div class="canvas-container">
    <canvas id="canvas" width="800" height="800"></canvas>
  </div>
Copy the code

Adding processing Events

A class called Draw is created where the handleOperations property is used to place the button’s handler. The handleMousemove property is used to handle what needs to be done in mousemove if different types (such as lines and pencils) are selected.

class Draw { constructor (elements) { const { canvas, color, lineWidth, operations, imageFile, downloadLink } = elements; This. type = 'pencil'; This. canvas = canvas; // Canvas element this.context = Canvas. GetContext ('2d'); // Get canvas's 2D context object... },... handleOperations () { ... } handleMousemove () { ... }Copy the code

The click event of the binding element

The click events of the first few buttons set the Type property of the Draw instance.

  • clearThe “clear” event fills the canvas with the background color.
  • imageEvent simulationtypeforfiletheinputBox click throughFileReaderObject toinputBox for the selected filebase64Address and usedrawImageI’m going to draw it tocanvasOn.
  • saveEvents throughtoDataURLMethod to get the current canvasbase64Address, and setaOf the labeldownloadProperty of the calling elementclick()Method to simulate a click to download the file.
  • redoEvents andundoThe idea of the event is similar, both are saved by getting inhistoryUrlsIn thebase64Address, callcanvasthedrawImageMethod to draw an image to the Canvas. Only one is to take forward (undo), one is to take back (redo).
handleOperations () {
  return {
    pencil: () = > { this.type = 'pencil'; }, // Pencil button binding event
    straightLine: () = > { this.type = 'straightLine'; }, // The line button is bound to the event
    rectangle: () = > { this.type = 'rectangle'; }, // The rectangle button is bound to the event
    solidRectangle: () = > { this.type = 'solidRectangle'; }, // Solid rectangle button binding event
    eraser: () = > { this.type = 'eraser'; }, // Eraser bound events
    circle: () = > { this.type = 'circle'; }, // Round button binding event
    solidCircle: () = > { this.type = 'solidCircle'; }, // Solid circle button binding event
    clear: () = > { this.clear(); }, // Clear the events bound to the button
    image: () = > { // Import the image button binding event
      this.imageFile.click();
      this.imageFile.onchange = (event) = > {
        let reader = new FileReader();
        reader.readAsDataURL(event.target.files[0]);
        reader.onload = (evt) = > {
          let img = new Image();
          img.src = evt.target.result;
          img.onload = () = > {
            this.context.drawImage(img, 0.0); // Paint the picture on the canvas
            this.addHistory(); }; }}},save: () = > { // Save the event bound to the button
      this.downloadLink.href = this.canvas.toDataURL('image/png');
      this.downloadLink.download = 'drawing.png';
      this.downloadLink.click();
    },
    redo: () = > { // Redo the event bound to the button
      let length = this.historyUrls.length;
      let currentIndex = this.currentHistoryIndex + 1;
      if (currentIndex > length - 1 ) {
        this.currentHistoryIndex = length - 1;
        return;
      };
      this.currentHistoryIndex = currentIndex;
      this.historyImage.src = this.historyUrls[currentIndex];
      this.historyImage.onload = () = > {
        this.context.drawImage(this.historyImage, 0.0); }},undo: () = > { // Undo the event bound to the button
      let currentIndex = this.currentHistoryIndex - 1;
      if (currentIndex < 0) { 
        currentIndex === -1 && this.clear();
        this.currentHistoryIndex = -1;
        return;
      }
      this.currentHistoryIndex = currentIndex;
      this.historyImage.src = this.historyUrls[currentIndex];
      this.historyImage.onload = () = > {
        this.context.drawImage(this.historyImage, 0.0); }}}}Copy the code

The event that was triggered when the mouse was moving

Bind the Mousemove event handler to the canvas.

this.canvas.addEventListener('mousemove'.(event) = > {
  if (this.isDrawing) {
    const { clientX, clientY } = event;
    const x = clientX - offsetLeft;
    const y = clientY - offsetTop;
    let newOriginX = originX, newOriginY = originY;
    let distanceX = Math.abs(x-originX);
    let distanceY = Math.abs(y-originY);

    // Make the coordinates of the upper left corner of the shape always greater than the coordinates of the lower right corner
    if (x < originX) newOriginX = x;
    if (y < originY) newOriginY = y;
    
    (x, y) is the coordinate on the canvas when the mouse moves, (originX, originY) is the coordinate on the canvas when the mouse clicks, (originX, originY) is the coordinate on the canvas when the mouse clicks,
    // (newOriginX, newOriginY) is the coordinate of the top left corner of the shape when drawing it (such as a rectangle)
    const mousePosition = { x, y, originX, originY, newOriginX, newOriginY, distanceX, distanceY };
    let handleMousemove = this.handleMousemove();
    let currentHandleMousemove = handleMousemove[this.type]; // Take different actions depending on the current typecurrentHandleMousemove && currentHandleMousemove(mousePosition); }},false);

Copy the code

The process in Mousemove will be processed according to the value of type.

  • pencil:x.yIs the coordinate in the process of mouse movement, used directlylineToConnect the line to the current(x, y)Coordinates, you can do the pencil thing.
  • eraserThe same way as the pencil, but the eraser sets the line color to the background color of the canvas so that it looks like it has been erased. After erasing, you need to reset the line color to currentcolorThe selected color of the element (this part of the processing is put inmouseupIn, not inmousemoveChinese is better.
  • straightLine: Moves the starting point of the drawing to the point where the mouse clicks(originX, originY), and then the starting point and the mouse move(x, y)Connect, and you’ll have a straight line. Here,this.reDraw();This is to prevent the mousemove process from drawing the “track” as well.
  • Rectangles and circles are drawn similarly to lines, except that the methods are called differently, and the drawing method must ensure that the upper left corner of the drawn shape is higher than the upper right corner, otherwise it will not draw properly.
handleMousemove () {
  return {
    pencil: (mousePosition) = > {
      const { x, y } = mousePosition;
      this.context.lineTo(x, y);
      this.context.stroke();
    },
    eraser: (mousePosition) = > {
      const { x, y } = mousePosition;
      this.context.strokeStyle = this.canvasBackground;;
      this.context.lineTo(x, y);
      this.context.stroke();
      this.context.strokeStyle = this.color.value;
      this.context.fillStyle = this.color.value;
    },
    straightLine: (mousePosition) = > {
      let { x, y, originX, originY } = mousePosition;
      this.reDraw();       

      this.context.moveTo(originX, originY);
      this.context.lineTo(x, y);
      this.context.stroke();

      this.context.closePath();
    },
    rectangle: (mousePosition) = > {
      let {newOriginX, newOriginY, distanceX, distanceY  } = mousePosition;
      this.reDraw();
      this.context.rect(newOriginX, newOriginY, distanceX, distanceY);
      this.context.stroke();

      this.context.closePath();
    },
    solidRectangle: (mousePosition) = > {
      let { newOriginX, newOriginY, distanceX, distanceY } = mousePosition;
      this.reDraw();
      this.context.fillRect(newOriginX, newOriginY, distanceX, distanceY);
      this.context.closePath();
    },
    circle: (mousePosition) = > {
      let { newOriginX, newOriginY, distanceX, distanceY } = mousePosition;
      this.reDraw();

      let r = Math.sqrt(distanceX * distanceX + distanceY * distanceY);
      this.context.arc(distanceX + newOriginX, distanceY + newOriginY , r, 0.2 * Math.PI);
      this.context.stroke();

      this.context.closePath();
    },
    solidCircle: (mousePosition) = > {
      let { newOriginX, newOriginY, distanceX, distanceY } = mousePosition;
      this.reDraw();       

      let r = Math.sqrt(distanceX * distanceX + distanceY * distanceY);
      this.context.arc(distanceX + newOriginX, distanceX + newOriginY , r, 0.2 * Math.PI);
      this.context.fillStyle = this.color.value;
      this.context.fill();

      this.context.closePath();
    },
    clear: () = > {
      this.clear(); }}}Copy the code

Problem solved

  1. The pixel value on the canvas is inconsistent with the pixel value on the page.

I drew a 50px by 50px square on the page and used this.context.strokeRect(0, 0, 50, 50) on the canvas. Draw a square and find that the size of the pixels on the canvas is not the same as the size of the pixels on the page.

The reason is that I didn’t define the width and height in the properties of canvas element at the beginning, but only defined the width and height of canvas element in the CSS style.

.canvas {
  height: 800px;
  width: 800px;
  background-color: #ccc;
}
Copy the code

When width and height are not set, the Canvas initializes a width of 300 pixels and a height of 150 pixels.

Once the width and height are defined in the canvas element’s properties, it is no problem.

 <canvas id="canvas" class="canvas" width="800" height="800"></canvas>
Copy the code
  1. The base64URL of the image was successfully obtained, but nothing was drawn on the canvas.

This is because the image has not been loaded before the start of drawing, wait for the image loading can be drawn.

 img.onload = () = > {
    this.context.drawImage(img, 0.0); // Paint the picture on the canvas
 };
Copy the code
  1. The downloaded image works fine on the computer, but on the phone it goes dark.

I guess it was because there was no background color added. By default, the phone used black as the background color, “hiding” the lines. After using different colors of brushes to draw a painting and save it, you could see the painted part of the colored brushes as expected. So just add a background color to the image.

    this.context.fillStyle = '#ffffff';
    this.context.fillRect(0.0.800.800);
Copy the code

Initially the eraser used the clearRect method to erase the content of the canvas, but after adding the background color, you won’t be able to use this method because it will also erase the background color. Set the eraser to a white brush to simulate erasing.

The previous implementation of the eraser:

let eraserWidth = parseInt(this.lineWidth.value);
if (eraserWidth < 10) {
  eraserWidth = 10;  // Set the minimum width of the eraser to 5px because the eraser doesn't clear very well when the pixel is too small
}
let halfEraserWidth = eraserWidth / 2;
this.context.clearRect(x - halfEraserWidth, y - halfEraserWidth, eraserWidth, eraserWidth);
Copy the code

After dealing with the problem after the implementation of eraser:

this.context.strokeStyle = '#ffffff';
this.context.lineTo(x, y);
this.context.stroke();
this.context.strokeStyle = this.color.value;
Copy the code

other

  1. Source address.
  2. Realized effect: