The basic pattern of canvas programming

Basic Introduction to Canvas

I have developed QT based client programs, C# based WinForm client, Java backend services, in addition, front-end VUE and React I also developed quite a few. For all the things I’ve developed, I’m more interested in things that are intuitive and visual than lines of cold code. I remember when I was developing C#, I came into contact with a C# WinForm library called NetronGraphLib, which allows us to easily build our own flowchart drawing software. Let’s build the graph by dragging and dropping (Cobalt is the official example application of the NetronGraphLib library below) :

When I saw this library, I was shocked as a new developer (and still am). Also, the library is open source and free, and there is a lightweight Light version that is also open source. Fascinated by this UI, I started with the Light version and delved into how it worked. Although it is a C# library, its underlying implementation principles and ideas are really universal and revolutionary to me, so much so that I have been thinking back to this library for many years.

This library principle is not complicated, is through C# GDI+ to draw the image. You may not have developed C# and know what GDI+ is. In short, many development languages provide so-called canvas and drawing capabilities (such as canvas tags in html5, Graphics objects in C#, etc.). On canvas, you can draw a wide variety of graphics through the related drawing API. The rectangles, lines, and so on you see in the flow chart above are all rendered by the drawing capabilities provided by the canvas.

A simple drawing

Draw a red rectangle on a blank form:

/// <summary>
///Form draw event, called by the WinForm Form Message Event framework
/// </summary>
private void Form1_Paint(object sender, PaintEventArgs e)
{
    // Gets the graphics canvas object in the Draw event
    Graphics g = e.Graphics;
    // Call API to draw one at x = 10, y = 10 of the current form
    // width = 200, height = 150
    g.DrawRectangle(new Pen(Color.Red), 10.10.200.150);
}
Copy the code

The display looks like this:

The following code is to get the Context object on the HTML5 Canvas and draw a rectangle using the Context API:

<body>
    <canvas id="myCanvas" 
            style="border: 1px solid black;"
            width="200" 
            height="200" />
    <script>
        // Get the context of the canvas
        let ctx = 
            document.getElementById('myCanvas').getContext('2d');
        // Set the color of the brush to draw
        ctx.strokeStyle = '#FF0000';
        // Stroke a rectangle
        ctx.strokeRect(10.10.100.80);
    </script>
</body>
Copy the code

It looks like this (the black border is added to make it easier to see the edges of the canvas) :

In order to facilitate the subsequent implementation and adapt to the current Web front-end, we use THE CANVAS of HTML 5 for code writing and demonstration.

The basic pattern of canvas programming

In order to explain the basic pattern of canvas programming, we will use the example of a scene where the mouse hovers over a rectangle and the rectangle border changes color. For a rectangle, the default is to show a black border, and when the mouse hovers over the rectangle, the border appears red, as shown in the following image:

So how do you implement this feature?

To answer this question, we first need to understand a set of basic concepts: input, update, render, and these operations will be around the ** state ** :

  1. The input triggers the update
  2. Updates modify the status
  3. Render reads the latest state for image mapping

In fact, rendering is decoupled from input and updates, and they are only related by state:

State sorting and refining

To apply the above concepts to the suspension discoloration scenario, we first need to sort out and refine what the states are.

The most straightforward way to sort out the state is to see what UI elements are required for the effect being implemented. For the suspension color-changing scenario, you need something simple:

  1. Position of the rectangle
  2. Size of the rectangle
  3. Rectangle border color

After finishing, we need to refine it. Some readers may say that the above sorting is enough, what else needs to be refined? In fact, the process of refinement is the process of generalization, the process of drawing the boundary between state and rendering. For 1 and 2, there is no need to discuss them too much. They are the basis of core rendering. No matter how simple an image rendering is, position and size are inseparable from these two core elements.

However, whether the rectangle frame color is a state needs to be discussed. In my opinion, it belongs in the category of rendering, not state. Why is that? Because the root cause of color change is mouse suspension, whether the mouse is suspended on the rectangle is the inherent attribute of the rectangle. Under normal circumstances, the interaction between the mouse and the rectangle will inevitably result in whether the mouse is suspended or not. However, the color of suspension is not an inherent attribute. In this scene, red is specified as the color of suspension, but in another scene, blue may be required. “Assembly line color, iron suspension”.

After the above discussion, we get the state of the canvas: a flag that contains position and size, and whether the flag is hovering over. In JS, the code is as follows:

let rect = {
    x: 10.y: 10.width: 80.height: 60.hovered: false
}
Copy the code

Input and Update

Find the update point

After the state is refined, we need to know which parts are updating the state. In this scenario, we change the hover of the rectangle to True as long as the mouse coordinates are within the rectangle area, and false otherwise. Describe with pseudocode:

if(mouse over rectangular area) {rect.hover =true; // Update the status
} else {
    rect.hover = false; // Update the status
}
Copy the code

In other words, we need to consider whether the “mouse is in the rectangular area” condition is true. In the canvas, we need to know the following data: position of the rectangle, size of the rectangle and position of the mouse in the canvas, as shown below:

We assume that the mouse is inside the rectangle as long as the following conditions are met, and a state update occurs:

(x <= xInCanvas && xInCanvas <= x + width) 
&& 
(y <= yInCanvas && yInCanvas <= y + height)
Copy the code

Find the input point

How is the update triggered? We now know that the position and size of the rectangle are existing values. How do you get x and y when the mouse is in canvas? In fact, we can add a mousemove to the Canvas to get the mouse position from the movement event. When the event is triggered, we can get the mouse relative to the viewPort (what is a viewport?) ClientX and event.clienty, which are not directly the position of the mouse in the canvas. At the same time, we can through the canvas. GetBoundingClientRect () to obtain a canvas relative to the coordinates of the viewport (top left), so we can calculate the coordinates of the mouse on the canvas.

Note: Canvas. Left in the figure below may be misleading. Canvas does not have a left.

For further coding, we’ll prepare an index.html:

<! DOCTYPEhtml>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Hover Example</title>
</head>
<body>
<canvas id="myCanvas"
        style="border: 1px solid black"
        width="450"
        height="200"></canvas>
    <! -- index.js -->
<script src="index.js"></script>
</body>
</html>
Copy the code

Index.js in the same directory:

// index.js of the same directory
let canvasEle = document.querySelector('#myCanvas');

canvasEle.addEventListener('mousemove'.ev= > {
  // Move the event object to deconstruct clientX and clientY from it
  let {clientX, clientY} = ev;
  // Deconstruct the left and top in the Canvas boundingClientRect
  let {left, top} = canvasEle.getBoundingClientRect();
  // Calculate the mouse coordinates on the canvas
  let mousePositionInCanvas = {
    x: clientX - left,
    y: clientY - top
  }
  console.log(mousePositionInCanvas);
})

Copy the code

Open index.html in a browser and you can see the coordinate output on the console:

PS: In fact, with different scales and CSS styles on canvas, the calculation of coordinates will be more complicated. This paper simply obtains the coordinates of the mouse in canvas without too much discussion. If you want to have an in-depth understanding, you can read this big man’s article: Get the position of the mouse in the canvas – a broken stick – blogpark (cnblogs.com)

Integrate input and status updates

Based on the above discussion, we integrate the current information and have the following JS code:

// Define the state
let rect = {
  x: 10.y: 10.width: 80.height: 60.hover: false
}

// Get the canvas element
let canvasEle = document.querySelector('#myCanvas');

// Monitor mouse movement
canvasEle.addEventListener('mousemove'.ev= > {
  // Move the event object to deconstruct clientX and clientY from it
  let {clientX, clientY} = ev;
  // Deconstruct the left and top in the Canvas boundingClientRect
  let {left, top} = canvasEle.getBoundingClientRect();
  // Calculate the mouse coordinates on the canvas
  let mousePositionInCanvas = {
    x: clientX - left,
    y: clientY - top
  }

  // console.log(mousePositionInCanvas);
  // Determine the condition to update
  let inRect = 
    (rect.x <= mousePositionInCanvas.x && mousePositionInCanvas.x <= rect.x + rect.width)
    && (rect.y <= mousePositionInCanvas.y && mousePositionInCanvas.y <= rect.y + rect.height)
  console.log('mouse in rect: ' + inRect);
  rect.hover = inRect; // Status changes
})
Copy the code

Apply colours to a drawing

In the last section, we implemented the following effect: Mouse in Rect: As the mouse moves over the canvas, the console outputs text as it moves around the rectangle: False, and once the mouse is inside the rectangle, the console prints: Mouse in rect: true. So how do we convert the Boolean attribute hover from rect to a UI image that we can see? The canvas CanvasRenderingContext2D class instance API is used to draw:

// The source of canvasEle can be seen in the code above
// Get the CanvasRenderingContext2D instance from the Canvas element
let ctx = canvasEle.getContext('2d');
// Set the brush color to black
ctx.strokeStyle = '# 000';
// Draw a black rectangle where the rectangle is
ctx.strokeRect(rect.x, rect.y, rect.width, rect.height);
Copy the code

For strokeStyle, according to our requirements, we need to judge the hover attribute of rect to determine whether the actual color is red or black:

// ctx.strokeStyle = '#000'; To:
ctx.strokeStyle = rect.hover ? '#F00' : '# 000';
Copy the code

For the convenience of subsequent calls, we encapsulate the draw operation as a method:

/** * Canvas render rectangle utility function *@param ctx
 * @param rect* /
function drawRect(ctx, rect) {
  // Hold the current CTX status
  ctx.save();
  // Set the brush color to black
  ctx.strokeStyle = rect.hover ? '#F00' : '# 000';
  // Draw a black rectangle where the rectangle is
  ctx.strokeRect(rect.x, rect.y, rect.width, rect.height);
  // Restore the status of CTX
  ctx.restore();
}
Copy the code

In this method, CTX calls save and restore. For the meanings and usage of these two methods, please refer to:

  • CanvasRenderingContext2D. The save () – Web API interface reference | MDN (mozilla.org)
  • CanvasRenderingContext2D. Restore () – Web API interface reference | MDN (mozilla.org)

Once the method is wrapped, we need the call point of the method, and the most direct way to do this is inside the mouse movement event handler:

// Monitor mouse movement
canvasEle.addEventListener('mousemove'.ev= > {
  // The status update code
  / /...
  // When the move is triggered, render
  drawRect(ctx, rect);
});
Copy the code

After writing the code, the current index.js looks like this:

// Define the state
let rect = {
	// ...
};

// Get the canvas element
let canvasEle = document.querySelector('#myCanvas');

// Get the context from the Canvas element
let ctx = canvasEle.getContext('2d');

/** * Canvas render rectangle tool function */
function drawRect(ctx, rect) {
	// ... 
}

// Monitor mouse movement
canvasEle.addEventListener('mousemove'.ev= > {
	// ...
});
Copy the code

The effect is as follows:

Timing of rendering

Careful readers will notice the problem in this demo: moving the mouse over the outside of the canvas will not display the rectangle in the initial case, but only after the mouse moves into the canvas. It’s easy to explain why: rendering (the drawRect call) starts after the Mousemove event is triggered.

To solve this problem, we need to be clear: ** In general, image rendering should be isolated from any input events, and input events should only be used for updates. That is, the above (drawRect) call should not be associated with the Mousemove event, but should be done in a separate set of loops:

So, in JS, what are the ways we can loop through methods to complete our image rendering? In my understanding, there are mainly the following:

While loops, including for and other loop control statement classes

while(true) {
	render();
}
Copy the code

Disadvantages: easy to cause CPU high occupancy card dead problem

setInterval

let interval = 1000 / 60; // About 60 times per second
setInterval(() = > {
	render();
}, interval);
Copy the code

Cons: Call loss occurs when render() is called beyond the interval; In addition, call rendering takes place whether or not the canvas needs to render.

setTimeout

let interval = 1000 / 60;
function doRendert() {
	setTimeout(() = > {
        doRender(); // recursive call
    }, interval)
}
Copy the code

Disadvantages: Same as above, no matter whether canvas needs rendering or not, it will be called, resulting in a waste of resources.

requestAnimationFrame

For basic usage and how this API works, please refer to requestAnimationFramework-Nuggets (juejin. Cn) as you know it.

In simple terms, requestAnimationFrame(callbackFunc), when this API is called, it just tells the browser that I’m requesting an action that happens when the animation frame is being rendered, and it’s left to the bottom of the browser to render when the animation frame is being rendered, but usually, This value is 60FPS. So, our code looks like this:

(function doRender() {
  requestAnimationFrame(() = > {
    drawRect(ctx, rect);
    doRender(); / / recursion
  })
})();
Copy the code

Necessary canvas emptying

One problem with this code so far is that we’ve been looping over and over again to draw a rectangle at the specified position, but we’ve never emptied the canvas, which means we’re constantly drawing a rectangle at the same position. In this case, the effect of this problem is not obvious, but imagine if we changed the x or y value of the rectangle when we entered the update, we would see multiple rectangular images on the canvas (because the previous rectangle was already “drawn” on the canvas). So, we need to clear it when we start drawing:

(function doRender() {
  requestAnimationFrame(() = > {
    // Clear the canvas first
    ctx.clearRect(0.0, canvasEle.width, canvasEle.height);
    // Draw a rectangle
    drawRect(ctx, rect);
    // recursive call
    doRender(); / / recursion
  })
})();
Copy the code

1px line blur

There’s just one problem with this code so far: by default, our line width is 1px. But in fact, our canvas shows a blurry line that looks much wider than 1px:

The reason for this problem can be searched online. The immediate solution here is that the line’s coordinates need to move 0.5 pixels to the left or right for a line width of 1px, so for the previous drawRect, move x and y by 0.5 pixels:

function drawRect(ctx, rect) {
  // ...
  // Draw a rectangle with a black box and shift it by 0.5 pixels
  ctx.strokeRect(rect.x - 0.5, rect.y - 0.5, rect.width, rect.height);
  // ...
}
Copy the code

After modification, the effect is as follows:

conclusion

Patterns of canvas programming:

Suspension color code

index.html

<! DOCTYPEhtml>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Hover Example</title>
</head>
<body>
<canvas id="myCanvas"
        style="border: 1px solid black"
        width="450"
        height="200"></canvas>
<script src="index.js"></script>
</body>
</html>
Copy the code

index.js

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

// Get the canvas element
let canvasEle = document.querySelector('#myCanvas');
// Get the context from the Canvas element
let ctx = canvasEle.getContext('2d');

/** * Canvas render rectangle utility function *@param ctx
 * @param rect* /
function drawRect(ctx, rect) {
  // Hold the current CTX status
  ctx.save();
  // Set the brush color to black
  ctx.strokeStyle = rect.hover ? '#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 CTX
  ctx.restore();
}

// Monitor mouse movement
canvasEle.addEventListener('mousemove'.ev= > {
  // Move the event object to deconstruct clientX and clientY from it
  let {clientX, clientY} = ev;
  // Deconstruct the left and top in the Canvas boundingClientRect
  let {left, top} = canvasEle.getBoundingClientRect();
  // Calculate the mouse coordinates on the canvas
  let mousePositionInCanvas = {
    x: clientX - left,
    y: clientY - top
  };

  // console.log(mousePositionInCanvas);
  // Determine the condition to update
  let inRect =
    (rect.x <= mousePositionInCanvas.x && mousePositionInCanvas.x <= rect.x + rect.width)
    && (rect.y <= mousePositionInCanvas.y && mousePositionInCanvas.y <= rect.y + rect.height);
  console.log('mouse in rect: ' + inRect);
  rect.hover = inRect;
});


(function doRender() {
  requestAnimationFrame(() = > {
    // Clear the canvas first
    ctx.clearRect(0.0, canvasEle.width, canvasEle.height);
    // Draw a rectangle
    drawRect(ctx, rect);
    // recursive call
    doRender(); / / recursion
  })
})();
Copy the code

GitHub

w4ngzhen/canvas-is-everything (github.com)

01_hover