HTML - tutorial - 38 - canvas - 02

Revision:


Content

Basic animations Advanced animations Pixel manipulation with canvas Optimizing canvas - performance tips


Basic animations

top

JavaScript is used to control <canvas> elements, what makes it very easy to make (interactive) animations.

Probably the biggest limitation is, that once a shape gets drawn, it stays that way. If we need to move it we have to redraw it and everything that was drawn before it. It takes a lot of time to redraw complex frames and the performance depends highly on the speed of the computer it's running on.

Basic animation steps

Thes steps to draw a frame are:

Clear the canvas : unless the shapes you'll be drawing fill the complete canvas (for instance a backdrop image), you need to clear any shapes that have been drawn previously. The easiest way to do this is using the clearRect() method.
Save the canvas state : if you're changing any setting (such as styles, transformations, etc.) which affect the canvas state and you want to make sure the original state is used each time a frame is drawn, you need to save that original state.
Draw animated shapes : the step where you do the actual frame rendering.
Restore the canvas state : if you've saved the state, restore it before drawing a new frame.

Controlling an animation

Shapes are drawn to the canvas by using the canvas methods directly or by calling custom functions.

In normal circumstances, we only see results appear on the canvas when the script finishes executing. For instance, it isn't possible to do an animation from within a for loop. That means we need a way to execute our drawing functions over a period of time.

There are different ways to control an animation :

setInterval() : starts repeatedly executing the function specified by function every delay milliseconds.
setTimeout() : executes the function specified by function in delay milliseconds.
requestAnimationFrame(callback) : tells the browser that you wish to perform an animation and requests that the browser call a specified function to update an animation before the next repaint.

If you don't want any user interaction you can use the setInterval() function, which repeatedly executes the supplied code.
If we wanted to make a game, we could use keyboard or mouse events to control the animation and use setTimeout().
By setting listeners using addEventListener(), we catch any user interaction and execute our animation functions.

The requestAnimationFrame method provides a smoother and more efficient way for animating by calling the animation frame when the system is ready to paint the frame. The number of callbacks is usually 60 times per second and may be reduced to a lower rate when running in background tabs.

example: animated clock
The current time
code:
                    <div>
                        <canvas id="canvas" width="150" height="150">The current time</canvas>
                    </div>
                    <script>
                        function clock() {
                            const now = new Date();
                            const canvas = document.getElementById("canvas");
                            const ctx = canvas.getContext("2d");
                            ctx.save();
                            ctx.clearRect(0, 0, 150, 150);
                            ctx.translate(75, 75);
                            ctx.scale(0.4, 0.4);
                            ctx.rotate(-Math.PI / 2);
                            ctx.strokeStyle = "black";
                            ctx.fillStyle = "white";
                            ctx.lineWidth = 8;
                            ctx.lineCap = "round";
            
                            // Hour marks
                            ctx.save();
                            for (let i = 0; i < 12; i++) {
                                ctx.beginPath();
                                ctx.rotate(Math.PI / 6);
                                ctx.moveTo(100, 0);
                                ctx.lineTo(120, 0);
                                ctx.stroke();
                            }
                            ctx.restore();
            
                            // Minute marks
                            ctx.save();
                            ctx.lineWidth = 5;
                            for (let i = 0; i < 60; i++) {
                                if (i % 5 !== 0) {
                                    ctx.beginPath();
                                    ctx.moveTo(117, 0);
                                    ctx.lineTo(120, 0);
                                    ctx.stroke();
                                }
                                ctx.rotate(Math.PI / 30);
                            }
                            ctx.restore();
            
                            const sec = now.getSeconds();
                            const min = now.getMinutes();
                            const hr = now.getHours() % 12;
            
                            ctx.fillStyle = "black";
            
                            // Write image description
                            canvas.innerText = `The time is: ${hr}:${min}`;
            
                            // Write Hours
                            ctx.save();
                            ctx.rotate(
                                (Math.PI / 6) * hr + (Math.PI / 360) * min + (Math.PI / 21600) * sec
                            );
                            ctx.lineWidth = 14;
                            ctx.beginPath();
                            ctx.moveTo(-20, 0);
                            ctx.lineTo(80, 0);
                            ctx.stroke();
                            ctx.restore();
            
                            // Write Minutes
                            ctx.save();
                            ctx.rotate((Math.PI / 30) * min + (Math.PI / 1800) * sec);
                            ctx.lineWidth = 10;
                            ctx.beginPath();
                            ctx.moveTo(-28, 0);
                            ctx.lineTo(112, 0);
                            ctx.stroke();
                            ctx.restore();
            
                            // Write seconds
                            ctx.save();
                            ctx.rotate((sec * Math.PI) / 30);
                            ctx.strokeStyle = "#D40000";
                            ctx.fillStyle = "#D40000";
                            ctx.lineWidth = 6;
                            ctx.beginPath();
                            ctx.moveTo(-30, 0);
                            ctx.lineTo(83, 0);
                            ctx.stroke();
                            ctx.beginPath();
                            ctx.arc(0, 0, 10, 0, Math.PI * 2, true);
                            ctx.fill();
                            ctx.beginPath();
                            ctx.arc(95, 0, 10, 0, Math.PI * 2, true);
                            ctx.stroke();
                            ctx.fillStyle = "rgba(0, 0, 0, 0)";
                            ctx.arc(0, 0, 3, 0, Math.PI * 2, true);
                            ctx.fill();
                            ctx.restore();
            
                            ctx.beginPath();
                            ctx.lineWidth = 14;
                            ctx.strokeStyle = "#325FA2";
                            ctx.arc(0, 0, 142, 0, Math.PI * 2, true);
                            ctx.stroke();
            
                            ctx.restore();
            
                            window.requestAnimationFrame(clock);
                        }
            
                        window.requestAnimationFrame(clock);
                    </script>
                

example: looping panorama
Holiday memories
code:
                <div>
                    <canvas id="canvas-a" width="450" height="350">Holiday memories</canvas>
                </div>
                <script>
                    const img = new Image();
                    // User Variables - customize these to change the image being scrolled, its direction, and the speed.
                    img.src = "../pics/2.jpg";
                    const canvasXSize = 800;
                    const canvasYSize = 200;
                    const speed = 30; // lower is faster
                    const scale = 1.05;
                    const y = -4.5; // vertical offset
        
                    // Main program
                    const dx = 0.75;
                    let imgW;
                    let imgH;
                    let x = 0;
                    let clearX;
                    let clearY;
                    let ctx;
        
                    img.onload = () => {
                        imgW = img.width * scale;
                        imgH = img.height * scale;
        
                        if (imgW > canvasXSize) {
                            // Image larger than canvas
                            x = canvasXSize - imgW;
                        }
        
                        // Check if image dimension is larger than canvas
                        clearX = Math.max(imgW, canvasXSize);
                        clearY = Math.max(imgH, canvasYSize);
        
                        // Get canvas context
                        ctx = document.getElementById("canvas-a").getContext("2d");
        
                        // Set refresh rate
                        return setInterval(draw, speed);
                    };
        
                    function draw() {
                        ctx.clearRect(0, 0, clearX, clearY); // clear the canvas
        
                        // If image is <= canvas size
                        if (imgW <= canvasXSize) {
                            // Reset, start from beginning
                            if (x > canvasXSize) {
                            x = -imgW + x;
                            }
        
                            // Draw additional image1
                            if (x > 0) {
                            ctx.drawImage(img, -imgW + x, y, imgW, imgH);
                            }
        
                            // Draw additional image2
                            if (x - imgW > 0) {
                            ctx.drawImage(img, -imgW * 2 + x, y, imgW, imgH);
                            }
                        } else {
                            // Image is > canvas size
                            // Reset, start from beginning
                            if (x > canvasXSize) {
                            x = canvasXSize - imgW;
                            }
        
                            // Draw additional image
                            if (x > canvasXSize - imgW) {
                            ctx.drawImage(img, x - imgW + 1, y, imgW, imgH);
                            }
                        }
        
                        // Draw image
                        ctx.drawImage(img, x, y, imgW, imgH);
        
                        // Amount to move
                        x += dx;
                    }
                </script>
            

Advanced animations

top

drawing a ball

example: move mouse into canvas to start animation
code:
                    <div>
                        <canvas id="canvas_b" style="border: 0.2vw solid green" width="600" height="300"></canvas>
                    </div>
                    <script>
                        const canvas_b = document.getElementById("canvas_b");
                        const ctx_b = canvas_b.getContext("2d");
                        let raf;
            
                        const ball = {
                            x: 100,
                            y: 100,
                            vx: 5,
                            vy: 2, 
                            radius: 25,
                            color: "blue",
                            draw_b() {
                                ctx_b.beginPath();
                                ctx_b.arc(this.x, this.y, this.radius, 0, Math.PI * 2, true);
                                ctx_b.closePath();
                                ctx_b.fillStyle = this.color;
                                ctx_b.fill();
                            },
                        };
                        function draw_b() {
                            ctx_b.clearRect(0, 0, canvas_b.width, canvas_b.height);
                            ball.draw_b();
                            ball.x += ball.vx;
                            ball.y += ball.vy;
                            if (ball.y + ball.vy > canvas_b.height || ball.y + ball.vy < 0) {
                            ball.vy = -ball.vy;
                            }
                            if (ball.x + ball.vx > canvas_b.width || ball.x + ball.vx < 0) {
                                ball.vx = -ball.vx;
                            }
                            raf = window.requestAnimationFrame(draw_b);
                        }
                        canvas_b.addEventListener("mouseover", (e) => {
                            raf = window.requestAnimationFrame(draw_b);
                        });
                        canvas_b.addEventListener("mouseout", (e) => {
                            window.cancelAnimationFrame(raf);
                        });
                        ball.draw_b();
                        
                    </script>

                

Add acceleration

example: move mouse into canvas to start animation
code:
                    <div>
                        <canvas id="canvas_c" style="border: 0.2vw solid blue" width="600" height="300"></canvas>
                    </div>
                    <script>
                        const canvas_c = document.getElementById("canvas_c");
                        const ctx_c = canvas_c.getContext("2d");
                        let raf_c;
            
                        const ball_c = {
                            x: 100,
                            y: 100,
                            vx: 5,
                            vy: 2, 
                            radius: 25,
                            color: "red",
                            draw_c() {
                                ctx_c.beginPath();
                                ctx_c.arc(this.x, this.y, this.radius, 0, Math.PI * 2, true);
                                ctx_c.closePath();
                                ctx_c.fillStyle = this.color;
                                ctx_c.fill();
                            },
                        };
                        function draw_c() {
                            ctx_c.clearRect(0, 0, canvas_b.width, canvas_b.height);
                            ball_c.draw_c();
                            ball_c.x += ball_c.vx;
                            ball_c.y += ball_c.vy;
                            ball_c.vy *= 0.99;
                            ball_c.vy += 0.25;
                            if (ball_c.y + ball_c.vy > canvas_c.height || ball_c.y + ball_c.vy < 0) {
                            ball_c.vy = -ball_c.vy;
                            }
                            if (ball_c.x + ball_c.vx > canvas_c.width || ball_c.x + ball_c.vx < 0) {
                                ball_c.vx = -ball_c.vx;
                            }
                            raf_c = window.requestAnimationFrame(draw_c);
                        }
                        canvas_c.addEventListener("mouseover", (e) => {
                            raf_c = window.requestAnimationFrame(draw_c);
                        });
                        canvas_c.addEventListener("mouseout", (e) => {
                            window.cancelAnimationFrame(raf_c);
                        });
                        ball_c.draw_c();
                    </script>
                

Traling effect

example: move mouse into canvas to start animation
code:
                    <div>
                        <canvas id="canvas_d" style="border: 0.2vw solid red" width="600" height="300"></canvas>
                    </div>
                    <script>
                        const canvas_d = document.getElementById("canvas_d");
                        const ctx_d = canvas_d.getContext("2d");
                        let raf_d;
            
                        const ball_d = {
                            x: 100,
                            y: 100,
                            vx: 5,
                            vy: 2, 
                            radius: 25,
                            color: "green",
                            draw_d() {
                                ctx_d.beginPath();
                                ctx_d.arc(this.x, this.y, this.radius, 0, Math.PI * 2, true);
                                ctx_d.closePath();
                                ctx_d.fillStyle = this.color;
                                ctx_d.fill();
                            },
                        };
                        function draw_d() {
                            ctx_d.fillStyle = "rgba(255, 255, 255, 0.3)"
                            ctx_d.fillRect(0,0, canvas_d.width, canvas_d.height);
                            ball_d.draw_d();
                            ball_d.x += ball_d.vx;
                            ball_d.y += ball_d.vy;
                            ball_d.vy *= 0.99;
                            ball_d.vy += 0.25;
                            if (ball_d.y + ball_d.vy > canvas_d.height || ball_d.y + ball_d.vy < 0) {
                            ball_d.vy = -ball_d.vy;
                            }
                            if (ball_d.x + ball_d.vx > canvas_d.width || ball_d.x + ball_d.vx < 0) {
                                ball_d.vx = -ball_d.vx;
                            }
                            raf_d = window.requestAnimationFrame(draw_d);
                        }
                        canvas_d.addEventListener("mouseover", (e) => {
                            raf_d = window.requestAnimationFrame(draw_d);
                        });
                        canvas_d.addEventListener("mouseout", (e) => {
                            window.cancelAnimationFrame(raf_d);
                        });
                        ball_d.draw_d();
                        
                    </script>
                

Mouse control

example: Move the ball using your mouse and release it with a click
code:
                        <div>
                            <canvas id="canvas_1a" style="border: 0.2vw solid magenta" width="600" height="300"></canvas>
                        </div>
                        <script>
                            const canvas_1a = document.getElementById("canvas_1a");
                            const ctx_1a = canvas_1a.getContext("2d");
                            let raf_1a;
                            let running = false;
                
                            const ball_1a = {
                            x: 100,
                            y: 100,
                            vx: 5,
                            vy: 1,
                            radius: 25,
                            color: "burlywood",
                            draw_1a() {
                                ctx_1a.beginPath();
                                ctx_1a.arc(this.x, this.y, this.radius, 0, Math.PI * 2, true);
                                ctx_1a.closePath();
                                ctx_1a.fillStyle = this.color;
                                ctx_1a.fill();
                            },
                            };
                
                            function clear() {
                            ctx_1a.fillStyle = "rgba(255, 255, 255, 0.3)";
                            ctx_1a.fillRect(0, 0, canvas_1a.width, canvas_1a.height);
                            }
                
                            function draw_1a() {
                            clear();
                            ball_1a.draw_1a();
                            ball_1a.x += ball_1a.vx;
                            ball_1a.y += ball_1a.vy;
                
                            if (ball_1a.y + ball.vy > canvas_1a.height || ball_1a.y + ball_1a.vy < 0) {
                                ball_1a.vy = -ball_1a.vy;
                            }
                            if (ball_1a.x + ball_1a.vx > canvas_1a.width || ball_1a.x + ball_1a.vx < 0) {
                                ball_1a.vx = -ball_1a.vx;
                            }
                
                            raf_1a = window.requestAnimationFrame(draw_1a);
                            }
                
                            canvas_1a.addEventListener("mousemove", function (e) {
                            if (!running) {
                                clear();
                                ball_1a.x = e.offsetX;
                                ball_1a.y = e.offsetY;
                                ball_1a.draw_1a();
                            }
                            });
                
                            canvas_1a.addEventListener("click", function (e) {
                            if (!running) {
                                raf_1a = window.requestAnimationFrame(draw_1a);
                                running = true;
                            }
                            });
                
                            canvas_1a.addEventListener("mouseout", function (e) {
                            window.cancelAnimationFrame(raf_1a);
                            running = false;
                            });
                
                            ball_1a.draw_1a();
                        </script>
                    

Pixel manipulation with canvas

top

With the ImageData object you can directly read and write a data array to manipulate pixel data.

The ImageData object

The ImageData object represents the underlying pixel data of an area of a canvas object. It contains the following read-only attributes:

width : the width of the image in pixels.
height : the height of the image in pixels.
data : a Uint8ClampedArray representing a one-dimensional array containing the data in the RGBA order, with integer values between 0 and 255 (included).

The data property returns a Uint8ClampedArray, which can be accessed to look at the raw pixel data; each pixel is represented by four one-byte values (red, green, blue, and alpha, in that order; that is, "RGBA" format).

Each color component is represented by an integer between 0 and 255.
Each component is assigned a consecutive index within the array, with the top left pixel's red component being at index 0 within the array.
Pixels then proceed from left to right, then downward, throughout the array.
The Uint8ClampedArray contains height × width × 4 bytes of data, with index values ranging from 0 to (height×width×4)-1.

examples

to read the blue component's value from the pixel at column 200, row 50 in the image, you would do the following : const blueComponent = imageData.data[50 * (imageData.width * 4) + 200 * 4 + 2];

If given a set of coordinates (X and Y), you may end up doing something like this:

        const xCoord = 50;
        const yCoord = 100;
        const canvasWidth = 1024;
        
        const getColorIndicesForCoord = (x, y, width) => {
          const red = y * (width * 4) + x * 4;
          return [red, red + 1, red + 2, red + 3];
        };
        
        const colorIndices = getColorIndicesForCoord(xCoord, yCoord, canvasWidth);
        
        const [redIndex, greenIndex, blueIndex, alphaIndex] = colorIndices;
    

You may also access the size of the pixel array in bytes by reading the Uint8ClampedArray.length attribute:const numBytes = imageData.data.length;

Creating an ImageData object

To create a new, blank ImageData object, use the createImageData() method. There are two versions of the createImageData() method:

const myImageData = ctx.createImageData(width, height); : this creates a new ImageData object with the specified dimensions. All pixels are preset to transparent black (all zeroes, i.e., rgba(0,0,0,0)).

const myImageData = ctx.createImageData(anotherImageData); : this creates a new ImageData object with the same dimensions as the object specified by anotherImageData. The new object's pixels are all preset to transparent black. This does not copy the image data!

Getting the pixel data for a context

To obtain an ImageData object containing a copy of the pixel data for a canvas context, use the getImageData() method: const myImageData = ctx.getImageData(left, top, width, height);

This method returns an ImageData object representing the pixel data for the area of the canvas whose corners are represented by the points (left, top), (left+width, top), (left, top+height), and (left+width, top+height). The coordinates are specified in canvas coordinate space units. Any pixels outside the canvas are returned as transparent black in the resulting ImageData object.

example: a color picker
Source Hovered color Selected color
code:
                    <div>
                        <table>
                            <thead>
                                <tr>
                                    <th>Source</th>
                                    <th>Hovered color</th>
                                    <th>Selected color</th>
                                </tr>
                            </thead>
                            <tbody>
                            <tr>
                                    <td>
                                    <canvas id="canvas_1b" width="300" height="227"></canvas>
                                    </td>
                                    <td class="color-cell" id="hovered-color"></td>
                                    <td class="color-cell" id="selected-color"></td>
                            </tr>
                            </tbody>
                        </table>
                    </div>
                    <style>
                        .color-cell {color: white; text-align: center;}
                        th {width: 15vw;}
                    </style>
                    <script>
                        const img_1b = new Image();
                        img_1b.crossOrigin = "anonymous";
                        img_1b.src = "../pics/2.jpg";
                        const canvas_1b = document.getElementById("canvas_1b");
                        const ctx_1b = canvas_1b.getContext("2d");
                        img_1b.addEventListener("load", () => {
                            ctx_1b.drawImage(img_1b, 0, 0);
                            img_1b.style.display = "none";
                        });
                        const hoveredColor = document.getElementById("hovered-color");
                        const selectedColor = document.getElementById("selected-color");
            
                        function pick(event, destination) {
                            const bounding = canvas_1b.getBoundingClientRect();
                            const x = event.clientX - bounding.left;
                            const y = event.clientY - bounding.top;
                            const pixel = ctx_1b.getImageData(x, y, 1, 1);
                            const data = pixel.data;
            
                            const rgba = `rgba(${data[0]}, ${data[1]}, ${data[2]}, ${data[3] / 255})`;
                            destination.style.background = rgba;
                            destination.textContent = rgba;
            
                            return rgba;
                        }
            
                        canvas_1b.addEventListener("mousemove", (event) => pick(event, hoveredColor));
                        canvas_1b.addEventListener("click", (event) => pick(event, selectedColor));
                    </script>
                

Painting pixel data into a context

Use the putImageData() method to paint pixel data into a context: ctx.putImageData(myImageData, dx, dy);

The dx and dy parameters indicate the device coordinates within the context at which to paint the top left corner of the pixel data you wish to draw.
For example, to paint the entire image represented by myImageData to the top left corner of the context, you can do the following: ctx.putImageData(myImageData, 0, 0);

example: grayscaling and inverting colors
code:
                    <div>
                        <canvas id="canvas_1c" width="500" height="227"></canvas>
                        <form>
                            <input type="radio" id="original" name="color" value="original" checked>
                            <label for="original">Original</label>
                            <input type="radio" id="grayscale" name="color" value="grayscale">
                            <label for="grayscale">Grayscale</label>
                            <input type="radio" id="inverted" name="color" value="inverted">
                            <label for="inverted">Inverted</label>
                            <input type="radio" id="sepia" name="color" value="sepia">
                            <label for="sepia">Sepia</label>
                        </form>
                    </div>
                    <script>
                        var img_1c = new Image();
                        img_1c.crossOrigin = "anonymous";
                        img_1c.src = "../pics/2.jpg";
            
                        var canvas_1c = document.getElementById("canvas_1c");
                        var ctx_1c = canvas_1c.getContext("2d");
                        img_1c.onload = () => {
                            ctx_1c.drawImage(img_1c, 0, 0);
                        };
                        var original = function() {
                            ctx_1c.drawImage(img_1c, 0, 0);
                        };
            
                        var sepia = function() {
                            ctx_1c.drawImage(img_1c, 0, 0);
                            const imageData = ctx_1c.getImageData(0, 0, canvas_1c.width, canvas_1c.height);
                            const data = imageData.data;
                            for (var i = 0; i < data.length; i += 4) {
                                let red = data[i], green = data[i + 1], blue = data[i + 2];
            
                                data[i] = Math.min(Math.round(0.393 * red + 0.769 * green + 0.189 * blue), 255);
                                data[i + 1] = Math.min(Math.round(0.349 * red + 0.686 * green + 0.168 * blue), 255);
                                data[i + 2] = Math.min(Math.round(0.272 * red + 0.534 * green + 0.131 * blue), 255);
                            }
                            ctx_1c.putImageData(imageData, 0, 0);
                        }
            
                        var invert = function() {
                            ctx_1c.drawImage(img_1c, 0, 0);
                            const imageData = ctx_1c.getImageData(0, 0, canvas_1c.width, canvas_1c.height);
                            const data = imageData.data;
                            for (let i = 0; i < data.length; i += 4) {
                                data[i] = 255 - data[i]; // red
                                data[i + 1] = 255 - data[i + 1]; // green
                                data[i + 2] = 255 - data[i + 2]; // blue
                            }
                            ctx_1c.putImageData(imageData, 0, 0);
                        };
            
                        var grayscale = function() {
                            ctx_1c.drawImage(img_1c, 0, 0);
                            const imageData = ctx_1c.getImageData(0, 0, canvas_1c.width, canvas_1c.height);
                            const data = imageData.data;
                            for (let i = 0; i < data.length; i += 4) {
                                const avg = (data[i] + data[i + 1] + data[i + 2]) / 3;
                                data[i] = avg; // red
                                data[i + 1] = avg; // green
                                data[i + 2] = avg; // blue
                            }
                            ctx_1c.putImageData(imageData, 0, 0);
                        };
            
                        const inputs = document.querySelectorAll("[name=color]");
                        for (const input of inputs) {
                            input.addEventListener("change", (evt) => {
                                switch (evt.target.value) {
                                case "inverted":
                                    return invert();
                                case "grayscale":
                                    return grayscale();
                                case "sepia":
                                    return sepia();
                                default:
                                    return original();
                                }
                            });
                        }
                    </script>
                

Zooming and anti-aliasing

Zoom into pictures and see the details can be done with the help of the drawImage() method, a second canvas and the imageSmoothingEnabled property. A third canvas without imageSmoothingEnabled is also drawn onto to be able to have a side by side comparison.

example

            zoomctx.drawImage(
            canvas,
            Math.min(Math.max(0, x - 5), img.width - 10),
            Math.min(Math.max(0, y - 5), img.height - 10),
            10,
            10,
            0,
            0,
            200,
            200 
        
We get the position of the mouse and crop an image of 5 pixels left and above to 5 pixels right and below.

Then we copy that one over to another canvas and resize the image to the size we want it to.

In the zoom canvas we resize a 10×10 pixel crop of the original canvas to 200×200.
example: zoom example (p.S. issue to be solved)
Source imageSmoothingEnabled=true imageSmoothingEnabled=false
code:
                    <div>
                        <table>
                            <thead>
                                <tr>
                                    <th>Source</th>
                                    <th>imageSmoothingEnabled=true</th>
                                    <th>imageSmoothingEnabled=false</th>
                                </tr>
                            </thead>
                            <tbody>
                                <tr>
                                    <td>
                                        <canvas id="canvas_Z" width="300" height="400"></canvas>
                                    </td>
                                    <td>
                                        <canvas id="smoothed-zoom" width="200" height="400"></canvas>
                                    </td>
                                    <td>
                                        <canvas id="pixelated-zoom" width="200" height="400"></canvas>
                                    </td>
                                </tr>
                            </tbody>
                        </table>
                    </div>
                    <script>
                    var img_Z = new Image();
                        img_Z.crossOrigin = 'anonymous';
                        img_Z.src = '../pics/4.jpg';
                        img_Z.onload = function() {
                            draw_Z(this);
                        };
            
                        function draw_Z(img) {
                            var canvas_Z = document.getElementById('canvas_Z');
                            var ctx_Z = canvas_Z.getContext('2d');
                            ctx_Z.drawImage(img_Z, 0, 0);
            
                            var smoothedZoomCtx = document.getElementById('smoothed-zoom').getContext('2d');
                            smoothedZoomCtx.imageSmoothingEnabled = true;
                            smoothedZoomCtx.mozImageSmoothingEnabled = true;
                            smoothedZoomCtx.webkitImageSmoothingEnabled = true;
                            smoothedZoomCtx.msImageSmoothingEnabled = true;
            
                            var pixelatedZoomCtx = document.getElementById('pixelated-zoom').getContext('2d');
                            pixelatedZoomCtx.imageSmoothingEnabled = false;
                            pixelatedZoomCtx.mozImageSmoothingEnabled = false;
                            pixelatedZoomCtx.webkitImageSmoothingEnabled = false;
                            pixelatedZoomCtx.msImageSmoothingEnabled = false;
            
                            var zoom = function(ctx_Z, xx, yy) {
                                ctx_Z.drawImage(canvas_Z,
                                Math.min(Math.max(0, xx - 5), img.width - 10),
                                Math.min(Math.max(0, yy - 5), img.height - 10),
                                10, 10,
                                0, 0,
                                200, 200);
                            };
            
                            canvas.addEventListener('mousemove', function(event) {
                                const xx = event.layerX;
                                const yy = event.layerY;
                                zoom(smoothedZoomCtx, xx, yy);
                                zoom(pixelatedZoomCtx, xx, yy);
                            });
                        }
                    </script>
                

Optimizing canvas - performance tips

top

Pre-render similar primitives or repeating objects on an offscreen canvas

If you find yourself repeating some of the same drawing operations on each animation frame, consider offloading them to an offscreen canvas.
You can then render the offscreen image to your primary canvas as often as needed, without unnecessarily repeating the steps needed to generate it in the first place.

 
            myCanvas.offscreenCanvas = document.createElement("canvas");
            myCanvas.offscreenCanvas.width = myCanvas.width;
            myCanvas.offscreenCanvas.height = myCanvas.height;
            
            myCanvas.getContext("2d").drawImage(myCanvas.offScreenCanvas, 0, 0);
        

Avoid floating-point coordinates and use integers instead

Sub-pixel rendering occurs when you render objects on a canvas without whole values.

ctx.drawImage(myImage, 0.3, 0.5);

This forces the browser to do extra calculations to create the anti-aliasing effect. To avoid this, make sure to round all co-ordinates used in calls to drawImage() using Math.floor(), for example.

Don't scale images in drawImage

Cache various sizes of your images on an offscreen canvas when loading as opposed to constantly scaling them in drawImage().

Use multiple layered canvases for complex scenes

In your application, you may find that some objects need to move or change frequently, while others remain relatively static. A possible optimization in this situation is to layer your items using multiple <canvas> elements.

example

            <div id="stage">
                <canvas id="ui-layer" width="480" height="320"></canvas>
                <canvas id="game-layer" width="480" height="320"></canvas>
                <canvas id="background-layer" width="480" height="320"></canvas>
            </div>
            <style>
                #stage {width: 480px; height: 320px; position: relative; border: 2px solid black;}
                canvas { position: absolute;}
                #ui-layer {z-index: 3;}
                #game-layer {z-index: 2;}
                #background-layer {z-index: 1;}
              </style>
        

Use plain CSS for large background images

If you have a static background image, you can draw it onto a plain <div> element using the CSS background property and position it under the canvas. This will negate the need to render the background to the canvas on every tick.

Scaling canvas using CSS transforms

CSS transforms are faster since they use the GPU. The best case is to not scale the canvas, or have a smaller canvas and scale up rather than a bigger canvas and scale down.

            const scaleX = window.innerWidth / canvas.width;
            const scaleY = window.innerHeight / canvas.height;
            
            const scaleToFit = Math.min(scaleX, scaleY);
            const scaleToCover = Math.max(scaleX, scaleY);
            
            stage.style.transformOrigin = "0 0"; //scale from top left
            stage.style.transform = `scale(${scaleToFit})`;
        

Turn off transparency

If your application uses canvas and doesn't need a transparent backdrop, set the alpha option to false when creating a drawing context with HTMLCanvasElement.getContext(). This information can be used internally by the browser to optimize rendering.

            const ctx = canvas.getContext("2d", { alpha: false });