Revision:
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.
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.
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
<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
<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>
example: move mouse into canvas to start animation
<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>
example: move mouse into canvas to start animation
<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>
example: move mouse into canvas to start animation
<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>
example: Move the ball using your mouse and release it with a click
<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>
With the ImageData object you can directly read and write a data array to manipulate pixel data.
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;
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!
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 |
---|---|---|
<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>
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
<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>
Zoom into pictures and see the div 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
example: zoom example (p.S. issue to be solved)
Source | imageSmoothingEnabled=true | imageSmoothingEnabled=false |
---|---|---|
<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>
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);
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.
Cache various sizes of your images on an offscreen canvas when loading as opposed to constantly scaling them in drawImage().
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>
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.
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})`;
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 });