Canvas is everything - implement element dragging

In "Canvas is everything (1) - Introduction to the basics", we introduce the basic mode of UI programming using canvas and use the basic mode to analyze how to achieve the function of mouse hovering over elements and element discoloration. In this article, we still use the basic mode of canvas programming to program, but this time we will increase the difficulty of dragging elements.

Classmates who have used flowcharts or graphics drawing software have seen such scenes for dragging rectangles:

This article will take the above scenario as a requirement and combine the basic pattern of canvas programming to reproduce a similar effect. The code in this article has been submitted to the GitHub repository at the repository root directory/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. When the mouse is pressed on a rectangular element, the mouse can drag the rectangular element. When the mouse is released, the rectangle no longer follows the mouse. So for 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 or not:

  • Rectangular position
  • Rectangle size
  • Is the rectangle selected

Input and Update

In this scenario, the main update point is that when the mouse clicks on an element, the rectangle selected changes to true. When the mouse moves, the position of the rectangular element is modified as long as an element is selected and the left mouse button is clicked. The reason for the update is the behavior input (click and move) of the mouse.

Rendering

In fact, rendering is the simplest part of this scenario, and as described in the previous article, we just need canvas context s to draw rectangles constantly.

Process Combing

Let's comb the process again. Initially, the mouse moves over the canvas, resulting in a movement event. We introduce an auxiliary variable lastMousePosition (the default is null) to represent the location of the last mouse movement event. In the trigger of the mouse movement event, we get the position of the mouse at this moment and make a vector difference from the last mouse position to get the offset of the displacement difference. For offset we apply it to the movement of rectangles. In addition, when the mouse is pressed, we determine whether a rectangle is selected and then set the selected rectangle to true or false. When the mouse is raised, we can set the rectangle selected to false directly.

Writing and Analysis of Basic Drag Code

1) Tool method

Define common tool methods:

  • Gets the position of the mouse on canvas.

  • Check if a point is in a rectangle.

// 1 Define common tool methods
const utils = {

  /**
   * Tool method: Get the position of the mouse on the canvas
   */
  getMousePositionInCanvas: (event, canvasEle) => {
    // Move event objects from which clientX and clientY are deconstructed
    let {clientX, clientY} = event;
    // Deconstructing left and top in boundingClientRect of canvas
    let {left, top} = canvasEle.getBoundingClientRect();
    // Calculate mouse coordinates on canvas
    return {
      x: clientX - left,
      y: clientY - top
    };
  },

  /**
   * Tool method: Check whether the point is inside a 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);
  },

};

2) State Definition

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

In the previous section, we also added the attribute selected to indicate whether the rectangle is selected or not, based on the position and size of the rectangle's general attributes.

3) Get Canvas element object

// 3 Get the canvas element and prepare it for the next step
let canvasEle = document.querySelector('#myCanvas');

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

4) Mouse press 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 clicks inside the rectangle
  rect.selected = utils.isPointInRect(rect, {x, y});
});

Gets the current mouse-down position and uses the tool function to determine if a rectangle needs to be selected (selected set to true/false).

5) Mouse Move Processing

// 5 Mouse Move Processing
// 5.1 Defines an auxiliary variable that records the location of each movement
let mousePosition = null;
canvasEle.addEventListener('mousemove', event => {

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

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

  // 5.4 Determine if 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)) {
    // Do not process if not satisfied
    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 {
    // Where the position has been recorded, 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 rect position
  rect.x = rect.x + offset.dx;
  rect.y = rect.y + offset.dy;

});

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

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

5.2 Records the temporary variable lastMousePosition. Assign the mousePosition of the last event record to this variable for subsequent offset offset calculations.

5.3 Update mousePosition.

5.4 Determine if the left mouse button clicks and a rectangle is selected. During the mouse movement, we can judge the current mouse click by the numeric value of the button or buttons attribute in the event object. MDN ). When buttons or buttons are 1, the left mouse button is pressed during the move. By determining whether the left mouse button is pressed down, you are dragging, but dragging does not mean that the rectangle is selected. You also need to determine whether the current rectangle is selected, so you need (buttons == 1 and rect.selected == true) to decide together.

5.5 Get the mouse offset. This section needs to explain what offset is. There is a location every time the mouse moves, and we recorded it using mousePosition. Then, using lastMousePosition and mousePosition, we make a difference (vector difference) between the x and y of the current position and the last position to get the offset of a small segment of the mouse. However, it is important to note that if this is the first movement event, then the last location is lastMousePosition is null, then we think this offset is 0.

5.6 Change rectangle position. Apply the mouse offset value to the position of the rectangle so that it also shifts the corresponding distance.

In the processing of mouse movement, we completed the offset mouse movement as input, modifying the position of the rectangle in the point.

6) Mouse button lift event

// 6 Mouse Up Event
canvasEle.addEventListener('mouseup', () => {
  // When the mouse is raised, the rectangle is not selected
  rect.selected = false;
});

After the mouse button is raised, we don't think we need to push and pull the rectangle anymore, so we set the selected rectangle to false.

7) Rendering

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

    // 7.2 Handling Rendering
    (function render() {
      // Empty the canvas first
      ctx.clearRect(0, 0, canvasEle.width, canvasEle.height);
      // Staging the status of the current ctx
      ctx.save();
      // Set Brush Color: Black
      ctx.strokeStyle = rect.selected ? '#F00' : '#000';
      // Draw a black box rectangle where the rectangle is located
      ctx.strokeRect(rect.x - 0.5, rect.y - 0.5, rect.width, rect.height);
      // Restore the state of ctx
      ctx.restore();
    })();

    // 7.3 Recursive Calls
    doRender();

  });
})();

The code for the rendering section is generally three main points:

  1. Gets the context object of the Canvas element.
  2. Use the requestAnimationFrame API and construct a recursive structure to have the browser schedule the rendering process.
  3. Code canvas operations (empty, draw) during the rendering process.

Drag effect demonstration

So far, we have implemented an example of element dragging as follows:

Full code for current effect at project root/02_ In the drag directory, the corresponding git is submitted as: 02_drag: 01_ Basic effects.

Effect Enhancement

For the above effect, it is actually not perfect. Because there is no information on the UI when the mouse hovers over a rectangle, the mouse pointer is also normal when the rectangle is clicked and dragged. So we optimize the code to make the mouse hover effect and the mouse pointer effect when dragging.

We set that when the mouse hovers over a rectangle, the rectangle will change its corresponding color to a red with 50% transparency (rgba(255, 0, 0, 0.5), and the mouse pointer will be changed to a pointer. Then you first need to add the attribute hover we mentioned in Chapter 1 to the rectangle:

let rect = {
  x: 10,
  y: 10,
  width: 80,
  height: 60,
  selected: false,
  // hover effect
  hover: false,
};

In rendering, we are no longer dealing with it as we did in the previous section, but need to consider selected, hover, and general state.

    // 7.2 Handling Rendering
    (function render() {
        
	  // ...

      // Selected by click: Positive red, pointer is'move'
      // Suspend: Positive red with 50% transparency, pointer'pointer'
      // Normally black, pointer is'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';
      }

	  // ...
        
    })();

Next, in the mouse movement event, modify the hover:

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

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

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

  // 5.3.1 Judging whether the mouse is floating in a rectangle
  rect.hover = utils.isPointInRect(rect, mousePosition);

  // 5.4 Determine if the left mouse button is clicked and a rectangle is selected
  // ... ...

});

Overall presentation

So far, we have enriched our drag samples with the following results:

Code repository and description

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_ Suspend and Click Effect Enhancement (Optimized)

Posted by Push Eject on Sun, 28 Nov 2021 09:52:09 -0800