02: Canvas
The first step is to resize the canvas to the window size.
// * 1: get canvas context
const ctx = canvas.getContext("2d");
// *2: resize canvas to entire window
canvas.width = window.innerWidth;
canvas.height = window.innerHeight;
Canvas API
Style
ctx.fillStyle
: gets/sets the fill color for fill operationsctx.strokeStyle
: sets the color of outlining shapes on the canvas. Accepts a color property.ctx.font
: the font style for text creation methods. Set this to a stringctx.lineWidth
: sets the line width of the pen, the thickness of it. Accepts a number value.
ctx.font = "30px Comic Sans MS";
ctx.fillStyle = "red";
ctx.textAlign = "center";
lineCap
and lineJoin
When your line width gets above 2 pixels, the lineCap
and lineJoin
concepts come in handy to control how lines look and connect.
- The
ctx.lineCap
property defines how the ends of a line look - The
ctx.lineJoin
property defines how lines look when connecting to each other in a path.
The ctx.lineCap
property can be these values: "butt"
, "square"
, "round"
The ctx.lineJoin
property can be these values: "miter"
, "round"
, "bevel"
Simple Shapes
ctx.fillRect(x, y, width, height)
: draws and fills a rectangle at the points.
Paths
Drawing paths to the canvas involves beginning a path, moving to a starting point, drawing the path with methods, and then submitting the path to the canvas by stroking it or filling it.
- You start a path with the
ctx.beginPath()
method. - Then you instantiate a starting point with the
ctx.moveTo(x, y)
method, which moves the canvas cursor to a specific location. - Then you start drawing lines with the
ctx.rect()
,ctx.arc()
, andctx.lineTo()
methods to draw on the path. - Submit the path to the canvas for drawing or filling with the
ctx.stroke()
orctx.fill()
methods.
methods
ctx.rect(x, y, width, height)
: draws a rectangular path.ctx.lineTo(x, y)
: draws a line to the specified point from its current position.ctx.beginPath()
: begins drawing for stuff like circles. Need to execute this before drawing stuff like circles.ctx.closePath()
: closes the path by moving the canvas cursor back to the original position set bymoveTo()
.ctx.arc(x, y, radius, radians)
: draws an arc around the specified center coordinates (x, y), with the specifiedradius
, and for the amount of radians you specify.ctx.stroke()
: end the path by stroking it.ctx.fill()
: end the path by filling it.
let drawing = document.getElementById("drawing");
let context = drawing.getContext("2d");
// start the path
context.beginPath();
// draw outer circle
context.arc(100, 100, 99, 0, 2 * Math.PI, false);
// draw inner circle
context.moveTo(194, 100);
context.arc(100, 100, 94, 0, 2 * Math.PI, false);
// draw minute hand
context.moveTo(100, 100);
context.lineTo(100, 15);
// draw hour hand
context.moveTo(100, 100);
context.lineTo(35, 100);
// stroke the path
context.stroke();
Drawing text
Use these methods to draw text to the canvas:
ctx.fillText(font, x, y)
: draws filled text on canvas at the specified center coordinatesctx.strokeText(font, x, y)
: draws unfilled text on canvas at the specified center coordinates
Set these properties to change how text is drawn to the canvas:
ctx.font
: sets the font family and size of the font, like"20px Bangers"
.
Drawing shadows
To draw shadows, use these properties:
ctx.shadowColor
—The CSS color in which the shadow should be drawn. The default is black.ctx.shadowOffsetX
—The x-coordinate offset from the x-coordinate of the shape or path. The default is 0.ctx.shadowOffsetY
—The y-coordinate offset from the y-coordinate of the shape or path. The default is 0.ctx.shadowBlur
—The number of pixels to blur. If set to 0, the shadow has no blur. The default is 0.
Drawing gradients
Linear Gradient
Here are the steps to draw gradients:
- Create a gradient with
context.createLinearGradient()
method.- The 4 arguments you need to pass into this method are the x and y coordinates for the top left point of the gradient, and the x and y coordinates for the bottom right point of the gradient.
const gradient = context.createLinearGradient(start_x, start_y, end_x, end_y);
- Add color stops between 0 and 1 using the
gradient.addColorStop(ratio, color)
method, that adds a specific color stop at the specified percentage point in the gradient.
gradient.addColorStop(0, "white");
gradient.addColorStop(0.5, "gray");
gradient.addColorStop(1, "black");
- Set the fill style of the canvas context to the gradient
context.fillStyle = gradient
- Draw shapes and see how the gradient appears!
// 1. create gradient
let gradient = context.createLinearGradient(30, 30, 70, 70);
gradient.addColorStop(0, "white");
gradient.addColorStop(0.5, "gray");
gradient.addColorStop(1, "black");
// 2. set fill style to gradient
context.fillStyle = gradient
// 3. draw shapes with exact coordinates as gradient
context.fillRect(30, 30, 70, 70)
Here is a simple utility function to draw gradients on rectangles:
function createRectLinearGradient(context, x, y, width, height) {
return context.createLinearGradient(x, y, x+width, y+height);
}
let gradient = createRectLinearGradient(context, 30, 30, 50, 50);
gradient.addColorStop(0, "white");
gradient.addColorStop(1, "black");
// draw a gradient rectangle
context.fillStyle = gradient;
context.fillRect(30, 30, 50, 50);
Radial Gradient
The arguments for the context.createRadialGradient()
method are as follows:
- The first three arguments are the x, y, and radius respectively for the starting circle.
- The last three arguments are the x, y, and radius respectively for the ending circle.
let gradient = context.createRadialGradient(55, 55, 10, 55, 55, 30);
gradient.addColorStop(0, "white");
gradient.addColorStop(1, "black");
// draw a red rectangle
context.fillStyle = "#ff0000";
context.fillRect(10, 10, 50, 50);
// draw a gradient rectangle
context.fillStyle = gradient;
context.fillRect(30, 30, 50, 50);
Images
Drawing images
The ctx.drawImage()
method is used to render images. The first argument will always be the image source, which should be an Image
instance.
ctx.drawImage(src, x, y, width, height)
: renders an image using the coordinates you pass as the top left coordinate of the image, with the width and height you want to the image to be.- Aspect ratio is not preserved.
ctx.drawImage(src, sx, sy, sw, sh, dx, dy, dw, dh)
: Renders a certain portion of the image using image cropping. Here is how to use the arguments:sx
,sy
,sw
, andsh
refer to the x, y, width, and height respectively of the source, which concerns the image dimensions. So these values refer to the part you want to crop out on the imagesx
,sy
,sw
, andsh
refer to the x, y, width, and height respectively of the destination, which concerns the coordinates and dimensions of the canvas. It's basically where you want to the render the cropped out image on the canvas.
Accessing image data from canvas
ctx.drawImage(image, 0, 0, canvas.width, canvas.height);
const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);
THe ctx.getImageData()
method returns an ImageData
instance, and on the ImageData.data
property, you get a byte array of the rgba color values for each pixel in the image.
Every 4 consecutive elements in the array refers to the rgba values, respectively, of a single pixel in the image.
These are the properties returned by the ImageData
instance:
imageData.width
- the width of the image in pixelsimageData.height
- the height of the image in pixelsimageData.data
- a byte array of the rgba values for each pixel in the image
This is a standard way of looping through the image data byte array and accessing/manipulating those rgba values:
for (let i = 0; i < imageData.data.length; i += 4) {
const red = imageData.data[i];
const green = imageData.data[i + 1];
const blue = imageData.data[i + 2];
const alpha = imageData.data[i + 3];
}
After manipulating the image data, you can modify how the image is drawn on canvas by using the ctx.putImageData(ImageData, x, y)
method, which just takes in an image data instance.
ctx.putImageData(imageData, x, y);
ctx.putImageData(imageData, 0, 0);
You can manipulate those pixels to do something like a grayscale effect:
A grayscale effect involves taking adding all the rgb values for a single pixel, and then dividing by three, getting the average color. You then set that average for all three components to make the pixel gray, but varying in intensity.
function grayscale() {
ctx.drawImage(image, 0, 0, canvas.width, canvas.height);
// 1. get image data
const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);
for (let i = 0; i < imageData.data.length; i += 4) {
const average =
(imageData.data[i] + imageData.data[i + 1] + imageData.data[i + 2]) / 3;
imageData.data[i] = average;
imageData.data[i + 1] = average;
imageData.data[i + 2] = average;
}
ctx.putImageData(imageData, 0, 0);
}
saving canvas as an image
Use the ctx.toDataURL()
method to save what the current canvas looks like as a base 64 image string (by default PNG). You can then do stuff like save the base64 string as a file.
let drawing = document.getElementById("drawing");
// get data URI of the image
let imgURI = ctx.toDataURL("image/png");
// display the image
let image = document.createElement("img");
image.src = imgURI;
document.body.appendChild(image);
Canvas performance
The canvas.width
and canvas.height
properties aren't just used to size the canvas. They actually represent the number of bytes allocated to render graphics.
Each pixel on the canvas uses 4 bytes of data. Thus a canvas.width
of 100 and a canvas.height
of 100 would use 40,000 bytes of data.
Here is the best technique to get the optimum image quality using a canvas:
- Size the canvas first using CSS
width
andheight
styles - In the js, set the
canvas.width
andcanvas.height
to your corresponding CSS values, but multiply by the pixel ratio to ensure crisp graphics on high-end devices, which you can get fromwindow.devicePixelRatio
.
Canvas animations
requestAnimationFrame()
The requestAnimationFrame()
method is used to run some code every frame. An example is as follows:
function updateProgress() {
var div = document.getElementById("status");
div.style.width = (parseInt(div.style.width, 10) + 5) + "%";
if (div.style.left != "100%") {
// you need to call requestAnimationFrame() again to run on every frame
requestAnimationFrame(updateProgress);
}
}
requestAnimationFrame(updateProgress);
The requestAnimationFrame()
method returns an id, which you can use to cancel the recursive frames with the cancelAnimationFrame(requestID)
method.
let requestID = window.requestAnimationFrame(() => {
console.log('Repaint!');
});
window.cancelAnimationFrame(requestID);
Canvas Examples
Canvas Paint App
We will have a boolean flag for whether the user can draw or not, triggered by the mousedown
and mouseup
events. So even the user is holding the mouse down, they should be able to draw, and not able to draw otherwise.
- Then get the mouse coordinates on the canvas, and draw a line from the previous mouse coordinates to the current mouse coordinates.
// set able to draw flag, update coordinates
canvas.addEventListener("mousedown", (e) => {
[lastX, lastY] = [e.pageX, e.pageY];
isDrawing = true;
});
// set not able to draw flag
canvas.addEventListener("mouseup", () => {
isDrawing = false;
});
canvas.addEventListener("mousemove", (e) => {
if (!isDrawing) return;
// * draw line
ctx.beginPath();
ctx.moveTo(lastX, lastY);
ctx.lineTo(e.pageX, e.pageY);
ctx.stroke();
// * update coordinates
[lastX, lastY] = [e.pageX, e.pageY];
hue += 1;
ctx.strokeStyle = `hsl(${hue}, 75%, 50%)`;
});