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:
- Gets the context object of the Canvas element.
- Use the requestAnimationFrame API and construct a recursive structure to have the browser schedule the rendering process.
- 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:
- 02_drag: 01_Base effect (before optimization)
- 02_drag: 02_ Suspend and Click Effect Enhancement (Optimized)