Recently I have been working on a project related to web page image processing, which can be regarded as my first experience with canvas. The project requirements include a function to add watermarks to images. We know that the usual way to implement the function of adding watermarks to images on the browser side is to use the drawImage
method of canvas
. For ordinary synthesis (such as the synthesis of a base image and a PNG watermark image), the general implementation principle is as follows:
var canvas = document.getElementById(canvas);var ctx = canvas.getContext('2d');// img: base image // watermarkImg: watermark image // x, y are the coordinates of placing img on the canvas ctx.drawImage( img, x, y);ctx.drawImage(watermarkImg, x, y);
Just use drawImage()
directly and continuously to draw the corresponding image onto canvas
.
The above is the background introduction. But what is a little troublesome is that there is another function that needs to be implemented in the requirement to add a watermark, which is that the user can switch the position of the watermark. We naturally think about whether we can implement the undo
function of canvas
. When the user switches the watermark position, first undo the previous drawImage
operation, and then redraw the watermark image position.
restore
/ save
?
The most efficient and convenient way is to check whether canvas 2D
native API has this function. After some searching, the pair of APIs restore
/ save
came into view. Let’s first take a look at the descriptions of these two APIs:
CanvasRenderingContext2D.restore() is the Canvas 2D API method to restore the canvas to its most recently saved state by popping the top state in the drawing state stack. If there is no saved state, this method makes no changes.
CanvasRenderingContext2D.save() is the method of Canvas 2D API to save the entire state of canvas by putting the current state into the stack.
At first glance it seems to meet the needs. Let's 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 last saved default state ctx.fillRect(150, 75, 100, 100);
The result is shown below:
Strange, it seems to be inconsistent with the results we expected. The result we want is to be able to save a snapshot of the current canvas after calling the save
method, and to be able to completely return to the state of the last saved snapshot after calling the resolve
method.
Let’s take a closer look at the API. It turns out that we missed an important concept: drawing state
, which is the drawing state. The drawing state saved to the stack contains the following parts:
The current values of the following properties: strokeStyle, fillStyle, globalAlpha, lineWidth, lineCap, lineJoin, miterLimit, lineDashOffset, shadowOffsetX, shadowOffsetY, shadowBlur, shadowColor, globalCompositeOperation, font, textAlign, textBaseline, direction, imageSmoothingEnabled.
Well, the changes to the canvas after drawImage
operation don't exist in the draw state at all. Therefore, using resolve
/ save
cannot achieve the undo function we need.
Since the native API's stack for saving drawing status cannot meet the needs, naturally we will think of simulating a stack for saving operations ourselves. The question that follows is: What data should be saved on the stack after each drawing operation? As mentioned before, what we want is to be able to save a snapshot of the current canvas after each drawing operation. If we can get the snapshot data and use the snapshot data to restore the canvas, the problem will be solved.
Fortunately, canvas 2D
natively provides APIs for obtaining snapshots and restoring the canvas through snapshots - getImageData
/ putImageData
. The following is the API description:
/* * @param { Number } sx The x coordinate of the upper left corner of the rectangular area of image data to be extracted * @param { Number } sy The y coordinate of the upper left corner of the rectangular area of image data to be extracted * @param { Number } sw Will be The width of the rectangular area of image data to be extracted * @param { Number } sh The height of the rectangular area of image data to be extracted * @return { Object } ImageData contains canvas Given rectangular image data */ ImageData ctx.getImageData(sx, sy, sw, sh); /* * @param { Object } imagedata object containing pixel values * @param { Number } dx source image data in the target canvas The position offset (the offset in the x-axis direction) * @param { Number } dy The position offset of the source image data in the target canvas (the offset in the y-axis direction) */ void ctx.putImageData(imagedata, dx, dy);
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); } }}
We encapsulate the drawImage
method of canvas
, and save a snapshot of the previous state to the simulated stack before each call to this method. When performing an undo
operation, remove the latest saved snapshot from the stack and then redraw the canvas to implement the undo operation. Actual testing also met expectations.
In the previous section, we implemented the undo function of canvas
very roughly. Why do you say rough? One obvious reason is that this solution performs poorly. Our solution is equivalent to redrawing the entire canvas every time. Assuming there are many operation steps, we will save a lot of pre-stored image data in the simulation stack, which is the memory. In addition, when drawing pictures is too complex, the two methods getImageData
and putImageData
will cause serious performance problems. There is a detailed discussion on stackoverflow: Why is putImageData so slow? . We can also verify this from the data of this test case on jsperf. Taobao FED also mentioned in Canvas best practices that try not to use the putImageData
method in animations. In addition, the article also mentioned that APIs with lower rendering overhead should be called as much as possible. We can start from here to think about how to optimize.
As mentioned before, we record each operation by saving a snapshot of the entire canvas. Thinking about it from another perspective, if we save each drawing action in an array, each time an undo operation is performed, the canvas is first cleared, and then Redrawing this drawing action array can also implement the function of undoing the operation. In terms of feasibility, first of all, this can reduce the amount of data saved to memory, and secondly, it avoids the use of putImageData
has higher rendering overhead. Taking drawImage
as the comparison object and looking at this test case on jsperf, there is an order of magnitude difference in performance between the two.
Therefore, we believe that this optimization solution is feasible.
The improved application method 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) { // Clear the canvas this.clearCanvas(); // Delete the current operation this.executionArray.pop(); // Execute drawing actions one by one to redraw for (let exe of this.executionArray) { this[exe.method](...exe.params) } } }}
If you are new to canvas, please point out any errors or deficiencies. The above is the entire content of this article. I hope it will be helpful to everyone’s study. I also hope everyone will support VeVb Wulin Network.