Recently, I have been working on some projects related to web image processing, which is my first step into the canvas pit. One of the project requirements is the ability to add a watermark to an image. As we know, the usual way to add watermarks to pictures in the browser is to use the drawImage method of canvas. For common composition (such as a base image and a PNG watermark image composition), the approximate implementation principle is as follows:

var canvas = document.getElementById("canvas");
var ctx = canvas.getContext('2d');

/ / img: reproduction
// watermarkImg: watermark image
// x, y are the coordinates on the canvas where img is placed
ctx.drawImage(img, x, y);
ctx.drawImage(watermarkImg, x, y);
Copy the code

Use drawImage() continuously to draw the corresponding image onto the canvas.

So that’s the background. However, it is slightly troublesome that the user can switch the location of the watermark in the requirement of adding a watermark. Naturally, we will think of whether the undo function of Canvas can be realized. When the user switches the watermark position, the previous drawImage operation should be cancelled first, and then the watermark image position should be redrawn.

restore/save ?

The most efficient and convenient way to do this is to check the Canvas 2D native API. After a search, the restore/ Save API pair comes into view. Let’s take a look at the descriptions of the two apis:

CanvasRenderingContext2D. Restore () is the Canvas by 2 d API in the drawing of stack shot ChuDing end state and the restoration of Canvas to the nearest save state method. This method does nothing if the state is not saved.

CanvasRenderingContext2D. The save () is a 2 d Canvas API through the current state in the stack, all save the Canvas state method.

At first glance, this seems to meet the requirements. Take a look at the official sample code:

var canvas = document.getElementById("canvas");
var ctx = canvas.getContext("2d");

ctx.save(); // Save the default state
ctx.fillStyle = "green";
ctx.fillRect(10.10.100.100);

ctx.restore(); // Restore to the default state saved last time
ctx.fillRect(150.75.100.100);
Copy the code

The results are shown below:

That’s weird. It doesn’t seem to be quite what we were expecting. What we want is for the save method call to save a snapshot of the current canvas, and the resolve method call to return exactly to the state of the last saved snapshot.

Take a closer look at the API. It turns out we left out an important concept: drawing state. The drawing state saved to the stack consists of the following parts:

  • The current transformation matrix
  • The current clipping region
  • The current dashed line list
  • Current values of the following attributes: strokeStyle, fillStyle, globalAlpha, lineWidth, lineCap, lineJoin, miterLimit, lineDashOffset, shadowOffsetX, shadowOffsetY, shadowBlur, shadowColor, globalCompositeOperation, font, textAlign, textBaseline, direction, imageSmoothingEnabled.

Well, the changes made to the canvas after the drawImage operation do not exist in the drawing state at all. So using resolve/ Save won’t do the undo functionality we need.

Analog stack implementation

Since the native API’s stack for storing draw state was not sufficient, it was natural to think of emulating a stack for storing operations ourselves. The question then becomes: what data should be stored on the stack after each draw operation? As mentioned earlier, we want to save a snapshot of the current canvas after each drawing operation. If we can take the snapshot data and use the snapshot data to restore the canvas, the problem will be solved.

Fortunately canvas 2 d native snapshot provides the snapshots and through the canvas API – getImageData/putImageData. Here is the API description:

/* * @param {Number} sx | @param {Number} sy | @param {Number} sw | @param {Number} sw The width of the rectangle of ImageData to be extracted * @param {Number} sh the height of the rectangle of ImageData to be extracted * @return {Object} ImageData contains the rectangle of ImageData given by canvas */
 ImageData ctx.getImageData(sx, sy, sw, sh);
 
 /* * @param {Object} imagedata the Object that contains pixel values * @param {Number} dx the offset of the source imagedata in the target canvas (the offset in the X-axis) * @param {Number} dy The offset of the position of the source image data in the target canvas (the offset in the y-direction) */
 void ctx.putImageData(imagedata, dx, dy);
Copy the code

Let’s look at a simple application:

class WrappedCanvas {
    constructor (canvas) {
        this.ctx = canvas.getContext('2d');
        this.width = this.ctx.canvas.width;
        this.height = this.ctx.canvas.height;
        this.imgStack = []; } drawImage (... params) {const imgData = this.ctx.getImageData(0.0.this.width, this.height);
        this.imgStack.push(imgData);
		this.ctx.drawImage(... params); } undo () {if (this.imgStack.length > 0) {
            const imgData = this.imgStack.pop();
            this.ctx.putImageData(imgData, 0.0); }}}Copy the code

We encapsulate the canvas drawImage method, which saves a snapshot of the previous state to the simulated stack before each call. When the undo operation is executed, the latest saved snapshot is pulled from the stack and the canvas is redrawn to achieve the undo operation. The actual tests also met expectations.

Performance optimization

In the previous section, we implemented the canvas undo function roughly. Why rough? One obvious reason is poor performance. Our solution is the equivalent of repainting the entire canvas every time. Assuming a lot of steps, we will store a lot of pre-stored image data in the simulation stack, that is, memory. In addition, the getImageData and putImageData methods can cause serious performance problems when drawing images is too complex. Why is putImageData so slow? . We can also verify this with the data from this test case on Jsperf. Taobao FED also mentioned in the Canvas best practice that “do not use putImageData method in animation”. In addition, the article mentioned that “call the API with the low rendering cost whenever possible”. This is where we can start thinking about how to optimize.

Said before, our way through to the entire canvas to save the snapshot to record every operation, change the Angle to think, if we keep the action of each drawing to an array, at every time of performing undo, first of all to empty canvas, then redraw the drawing action array, can also implement undo function. In terms of feasibility, this reduces the amount of data stored in memory, and avoids using putImageData, which is expensive to render. Take drawImage as the comparison object, and look at the test case on Jsperf. There is an order of magnitude difference in performance between the two.

The improved application mode is roughly as follows:

class WrappedCanvas {
    constructor (canvas) {
        this.ctx = canvas.getContext('2d');
        this.width = this.ctx.canvas.width;
        this.height = this.ctx.canvas.height;
        this.executionArray = []; } drawImage (... params) {this.executionArray.push({
            method: 'drawImage'.params: params
        });
        this.ctx.drawImage(... params); } clearCanvas () {this.ctx.clearRect(0.0.this.width, this.height);
    }
    undo () {
        if (this.executionArray.length > 0) {
            // Empty the canvas
            this.clearCanvas();
            // Delete the current operation
            this.executionArray.pop();
            // Perform drawing actions one by one to redraw
            for (let exe of this.executionArray) {
                this.ctx[exe.method](... exe.params) } } } }Copy the code

Newcomers to the pit canvas, if there are mistakes and deficiencies, welcome to point out.

This article was first published on my blog (click here to view it).

reference

Tips: Use Canvas in front to achieve image watermarking synthesis

Canvas Best Practices (Performance)

Canvas | MDN – Web API interface