In “Canvas is Everything (I) — Basics”, we introduced the basic pattern of UI programming using canvas and analyzed how to realize the function of mouse hovering over elements and elements changing color. In this article, we will continue to use the basic pattern of canvas programming, but this time we will make it a little more difficult to achieve the effect of dragging and dropping elements.

Those of you who have used flowcharts or graphing software have seen this: rectangle dragging:

This article will take the above scene as the demand, combined with the basic pattern of canvas programming to reproduce a similar effect. The code for this article has been submitted to the GitHub repository, in the repository root /02_drag directory.

Canvas-is-everything /02_drag at main · w4ngzhen/ Canvas-is-everything (github.com)

state

Let’s first analyze what the states are in this scenario. After the mouse is pressed over the rectangle element, the mouse can drag the rectangle element. After the mouse is released, the rectangle no longer moves with the mouse. For the UI, the most basic thing is the position and size of the rectangle, and we also need a state to indicate whether the rectangle element is selected:

  • Rectangle position
  • Size of rectangle
  • Whether the rectangle is selected

Input and Update

In this scenario, the main update point is that the selected rectangle changes to true when the mouse clicks over the element; When the mouse moves, the position of the rectangle element is changed as long as an element is selected and the left mouse button is clicked. The update is due to mouse input (clicking and moving).

Apply colours to a drawing

In fact, rendering is the easiest part in this scene. According to the introduction in the last article, we just need to draw the rectangle in the Canvas context.

The process to comb

Let’s go through the process again. In the initial case, the mouse moves over the canvas to generate a movement event. We introduce a helper variable lastMousePosition (null by default) that represents the location of the last mouse movement event. In the mouse movement event trigger, we get the mouse position at the moment, and make vector difference with the previous mouse position, and then get the displacement offset. For offset we apply it to the movement of the rectangle. In addition, when the mouse is pressed, we determine whether the rectangle is selected and set the rectangle’s selected to true or false. When the mouse is up, we simply set the selected rectangle to false.

Basic drag code writing and analysis

1) Tool methods

Define common tool methods:

  • Gets the mouse position on the canvas.

  • Checks whether a point is in a rectangle.

// 1 Define common tool methods
const utils = {

  /** * Tool method: Get the cursor position */ on the canvas
  getMousePositionInCanvas: (event, canvasEle) = > {
    // Move the event object to deconstruct clientX and clientY from it
    let {clientX, clientY} = event;
    // Deconstruct the left and top in the Canvas boundingClientRect
    let {left, top} = canvasEle.getBoundingClientRect();
    // Calculate the mouse coordinates on the canvas
    return {
      x: clientX - left,
      y: clientY - top
    };
  },

  /** * tool method: check whether the point is in the rectangle */
  isPointInRect: (rect, point) = > {
    let {x: rectX, y: rectY, width, height} = rect;
    let {x: pX, y: pY} = point;
    return(rectX <= pX && pX <= rectX + width) && (rectY <= pY && pY <= rectY + height); }};Copy the code

2) State definition

// 2 Define the state
let rect = {
  x: 10.y: 10.width: 80.height: 60.selected: false
};
Copy the code

In addition to the position and size of the rectangle’s general properties, we added the selected property to indicate whether the rectangle is selected.

3) Get Canvas element object

// 3 Get canvas element, ready in step
let canvasEle = document.querySelector('#myCanvas');
Copy the code

Call the API to get the Canvas element object for subsequent event listening.

4) Mouse down event

// 4 Mouse press event
canvasEle.addEventListener('mousedown'.event= > {
  // Get the position when the mouse is pressed
  let {x, y} = utils.getMousePositionInCanvas(event, canvasEle);
  // Whether the rectangle is selected depends on whether the mouse is inside the rectangle when clicked
  rect.selected = utils.isPointInRect(rect, {x, y});
});
Copy the code

Gets the current mouse position and determines whether the rectangle needs to be selected (selected set to true/false) using the utility function.

5) Mouse movement processing

// 5 Mouse movement processing
// 5.1 Define auxiliary variables to record the position of each move
let mousePosition = null;
canvasEle.addEventListener('mousemove'.event= > {

  // 5.2 Record the last mouse position
  let lastMousePosition = mousePosition;

  // 5.3 Update the current mouse position
  mousePosition = utils.getMousePositionInCanvas(event, canvasEle);

  5.4 Check whether the left mouse button is clicked and a rectangle is selected
  // https://developer.mozilla.org/en-US/docs/Web/API/MouseEvent/buttons
  let buttons = event.buttons;
  if(! (buttons ===1 && rect.selected)) {
    // If no, no action is taken
    return;
  }

  // 5.5 Get mouse offset
  let offset;
  if (lastMousePosition === null) {
    // First record, offset dx and dy are 0
    offset = {
      dx: 0.dy: 0
    };
  } else {
    // The offset is a vector difference between the current position and the previous position
    offset = {
      dx: mousePosition.x - lastMousePosition.x,
      dy: mousePosition.y - lastMousePosition.y
    };
  }

  // 5.6 Change the recT position
  rect.x = rect.x + offset.dx;
  rect.y = rect.y + offset.dy;

});
Copy the code

This part of the code is a little longer. But the logic is not hard to understand.

**5.1 Define the auxiliary variable mousePosition. ** Use this variable to record the position of the mouse during each movement.

**5.2 Record the temporary variable lastMousePosition. ** Assigns the mousePosition recorded in the last event to this variable for subsequent offset calculations.

The 5.3 updatemousePosition.

5.4 Checking whether a rectangle is selected after the left mouse button is clicked. During mouse movement, the current mouse click (MDN) can be determined by the value of the button or buttons property in the event object. When buttons or buttons are 1, the left mouse button is pressed during movement. Buttons === 1 and rect.selected === True are required to determine whether the current rectangle is being dragged. Buttons === 1 and rect.selected === true are required to determine whether the current rectangle is being dragged.

**5.5 Obtaining mouse offset. ** This section needs to explain what is mouse offset. Every time the mouse moves, there is a position, which we record using mousePosition. Then, using lastMousePosition and mousePosition, we make the difference (vector difference) between the x and y of the current position and the previous position to obtain the offset of a short mouse segment. But note that if this is the first move event and the last position was lastMousePosition was null, then we consider this offset to be 0.

**5.6 Change the position of the rectangle. ** Apply the mouse offset value to the position of the rectangle so that the rectangle also moves by the corresponding distance.

In the process of mouse movement, we completed the mouse movement offset as the input, and changed the position of the rectangle in the dot.

6) Mouse button lifting event

// 6 Mouse lift event
canvasEle.addEventListener('mouseup'.() = > {
  // When the mouse is up, the rectangle is not selected
  rect.selected = false;
});
Copy the code

After the mouse button is lifted, we decide that we no longer need to drag the rectangle, so we set the rectangle’s selected to false.

7) Rendering processing

/ / 7 rendering
Get the context from the Canvas element
let ctx = canvasEle.getContext('2d');
(function doRender() {
  requestAnimationFrame(() = > {

    // 7.2 Handle rendering
    (function render() {
      // Clear the canvas first
      ctx.clearRect(0.0, canvasEle.width, canvasEle.height);
      // Hold the current CTX status
      ctx.save();
      // Set the brush color to black
      ctx.strokeStyle = rect.selected ? '#F00' : '# 000';
      // Draw a black rectangle where the rectangle is
      ctx.strokeRect(rect.x - 0.5, rect.y - 0.5, rect.width, rect.height);
      // Restore the status of CTXctx.restore(); }) ();// 7.3 Recursive invocationdoRender(); }); }) ();Copy the code

The rendering part of the code, in general, there are three points:

  1. Gets the Canvas element’s context object.
  2. userequestAnimationFrameAPI and construct a recursive structure to allow the browser to schedule the rendering flow.
  3. Code the canvas operations (empty, draw) in the render process.

Drag and drop effect demonstration

At this point, we have implemented a sample element drag that looks like this:

The complete code for the current effect is in the project root /02_drag directory, and the corresponding Git commit is 02_drag: 01_ Base effect.

Effect of ascension

For the above effect, it is not perfect. Because there is no UI information when the mouse hovers over the rectangle, the mouse pointer is normal when the rectangle is dragged. So we optimized the code to make the hover effect and drag the mouse pointer effect.

We set the rectangle to change its color to red with 50% transparency when the mouse hovers over it (RGBA (255, 0, 0, 0.5) and change the pointer to pointer. So first we need to add the rectangle to the attribute hover we mentioned in chapter 1:

let rect = {
  x: 10.y: 10.width: 80.height: 60.selected: false./ / hover effect
  hover: false};Copy the code

In rendering, instead of simple processing like in the previous section, we need to consider selected, hover, and general state.

    // 7.2 Handle rendering
    (function render() {
        
	  // ...

      // Select: positive red, pointer to 'move'
      // Hover: 50% transparent positive red with pointer' pointer'
      // Black for normal, pointer to 'default'
      if (rect.selected) {
        ctx.strokeStyle = '#FF0000';
        canvasEle.style.cursor = 'move';
      } else if (rect.hover) {
        ctx.strokeStyle = 'rgba (255, 0, 0, 0.5)';
        canvasEle.style.cursor = 'pointer';
      } else {
        ctx.strokeStyle = '# 000';
        canvasEle.style.cursor = 'default';
      }

	  // ...}) ();Copy the code

Next, in the mouse movement event, modify hover:

canvasEle.addEventListener('mousemove'.event= > {

  // 5.2 Record the last mouse position
  / /... .

  // 5.3 Update the current mouse position
  mousePosition = utils.getMousePositionInCanvas(event, canvasEle);

  // 5.3.1 Check whether the mouse is hovering over the rectangle
  rect.hover = utils.isPointInRect(rect, mousePosition);

  5.4 Check whether the left mouse button is clicked and a rectangle is selected
  / /... .

});
Copy the code

The overall presentation

At this point, we have enriched our drag sample with the following result:

Code repository and specification

The code repository address for this article is:

Canvas-is-everything /02_drag at main · w4ngzhen/ Canvas-is-everything (github.com)

Two submissions:

  1. 02_drag: 01_ Base effect (before optimization)
  2. 02_drag: 02_ Hover and click improvements (after optimization)