The HTML <canvas> element is a blank drawing surface, and JavaScript is the pen. With a few commands you can draw rectangles, circles, lines, text and images, pixel by pixel, at any moment. Canvas powers browser games, charts, drawing apps, image editors, particle effects, signature pads and generative art.
Canvas works differently from the DOM. When you add a <div>, it stays on the page as an element you can select and style. When you draw a circle on a canvas, the browser just colours some pixels and forgets the circle exists. To move it, you clear the canvas and draw it again in a new position, many times a second. That's how animation works, and requestAnimationFrame is the tool that makes it smooth.
In this lesson you'll set up a canvas, draw shapes, paths and text, build an animation loop that runs at the screen's refresh rate, make movement smooth on every device using time, respond to the mouse, and keep drawings sharp on high-density screens. The HTML side was introduced in the HTML canvas lesson.
Setting up and drawing shapes
const canvas = document.querySelector("canvas");
const ctx = canvas.getContext("2d");
ctx.fillStyle = "#16a34a";
ctx.fillRect(20, 20, 100, 60); // x, y, width, height
ctx.strokeStyle = "#1f2937";
ctx.lineWidth = 4;
ctx.strokeRect(140, 20, 100, 60);getContext("2d")gives you the drawing context, an object with all the drawing methods.- Coordinates start at the top-left corner:
xgrows to the right,ygrows downwards. fillStyleandstrokeStyleset colours for fills and outlines; they stay set until you change them.clearRect(0, 0, canvas.width, canvas.height)wipes the canvas.
Paths, circles and text
| Method | Draws |
|---|---|
beginPath() | Starts a new shape |
moveTo(x, y), lineTo(x, y) | Lines |
arc(x, y, r, start, end) | Circles and arcs (angles in radians; Math.PI * 2 is a full circle) |
closePath() | Joins back to the start |
fill(), stroke() | Paints the current path |
fillText(text, x, y) | Text, using ctx.font = "20px sans-serif" |
drawImage(img, x, y, w, h) | An image, once it has loaded |
Always call beginPath() before a new shape, or the new shape joins onto the old one and both get repainted.
The animation loop
function frame(time) {
ctx.clearRect(0, 0, canvas.width, canvas.height); // 1. clear
update(time); // 2. move things
draw(); // 3. draw everything
requestAnimationFrame(frame); // 4. ask for the next frame
}
requestAnimationFrame(frame);requestAnimationFrame (often shortened to rAF) runs your function just before the browser paints the next frame, usually 60 times a second, or 120 on fast screens. It pauses automatically when the tab is hidden, saving battery. It's much smoother than setInterval, which isn't synchronised with the screen.
Time-based movement
Screens refresh at different rates, so moving "5 pixels per frame" is twice as fast on a 120Hz screen as on 60Hz. Instead, measure the time since the last frame and move by speed × time:
let last = performance.now();
function frame(now) {
const dt = (now - last) / 1000; // seconds since the last frame
last = now;
x += 120 * dt; // 120 pixels per second on every screen
requestAnimationFrame(frame);
}The time argument rAF passes in is in milliseconds, matching performance.now(). To stop a loop, keep the id from requestAnimationFrame and call cancelAnimationFrame(id).
Sharp canvases on high-density screens
A canvas has two sizes: its drawing size (the width and height attributes, in pixels) and its display size (set with CSS). On phones and Retina screens, devicePixelRatio is 2 or 3, so a 300px-wide canvas shown at 300 CSS pixels looks blurry. Fix it by multiplying the drawing size and scaling the context:
const dpr = window.devicePixelRatio || 1;
canvas.width = 300 * dpr; canvas.height = 150 * dpr;
canvas.style.width = "300px";
ctx.scale(dpr, dpr);JavaScript Canvas Syntax
Canvas essentials
const ctx = canvas.getContext("2d");
ctx.fillStyle = "tomato"; ctx.fillRect(x, y, w, h);
ctx.beginPath(); ctx.arc(x, y, r, 0, Math.PI * 2); ctx.fill();
ctx.font = "16px sans-serif"; ctx.fillText("Score: 3", 10, 20);
ctx.clearRect(0, 0, canvas.width, canvas.height);
const id = requestAnimationFrame(loop);
cancelAnimationFrame(id);JavaScript Canvas Summary
canvas.getContext("2d")gives the drawing API;(0, 0)is the top-left corner.- Use
fillRect, paths withbeginPath,arc,lineTo,fillandstroke, andfillText. - Animate by clearing and redrawing each frame inside a
requestAnimationFrameloop. - Move by speed × elapsed time so animations run at the same speed on every screen.
- Scale by
devicePixelRatiofor sharp drawings on high-density screens.
JavaScript Canvas Examples
Each example runs in the page. Edit the code and press Run to see what changes.
Drawing shapes, paths and text
<canvas id="c" width="320" height="180" style="border: 1px solid #ddd; max-width: 100%"></canvas>const ctx = document.querySelector("#c").getContext("2d");
ctx.fillStyle = "#bfdbfe";
ctx.fillRect(0, 120, 320, 60);
ctx.fillStyle = "#f59e0b";
ctx.beginPath();
ctx.arc(260, 45, 26, 0, Math.PI * 2);
ctx.fill();
ctx.fillStyle = "#dc2626";
ctx.fillRect(60, 80, 90, 60);
ctx.fillStyle = "#7f1d1d";
ctx.beginPath();
ctx.moveTo(50, 80);
ctx.lineTo(105, 40);
ctx.lineTo(160, 80);
ctx.closePath();
ctx.fill();
ctx.fillStyle = "#1f2937";
ctx.font = "bold 16px sans-serif";
ctx.fillText("Home sweet home", 170, 110);Line-by-line explanation
- Lines 3 to 4A blue rectangle for the ground.
- Lines 6 to 9A circle for the sun: a full arc from 0 to 2π radians.
- Lines 11 to 12The house walls.
- Lines 14 to 19A triangle roof drawn as a closed path.
- Lines 21 to 23Text with a chosen font.
A bouncing ball with requestAnimationFrame
<canvas id="c" width="320" height="180" style="border: 1px solid #ddd; max-width: 100%"></canvas>
<button id="toggle">Pause</button>const canvas = document.querySelector("#c");
const ctx = canvas.getContext("2d");
const ball = { x: 50, y: 50, vx: 140, vy: 100, r: 14 };
let last = performance.now();
let running = true;
let id;
function frame(now) {
const dt = Math.min((now - last) / 1000, 0.05);
last = now;
ball.x += ball.vx * dt;
ball.y += ball.vy * dt;
if (ball.x < ball.r || ball.x > canvas.width - ball.r) ball.vx *= -1;
if (ball.y < ball.r || ball.y > canvas.height - ball.r) ball.vy *= -1;
ctx.clearRect(0, 0, canvas.width, canvas.height);
ctx.fillStyle = "#16a34a";
ctx.beginPath();
ctx.arc(ball.x, ball.y, ball.r, 0, Math.PI * 2);
ctx.fill();
id = requestAnimationFrame(frame);
}
id = requestAnimationFrame(frame);
document.querySelector("#toggle").addEventListener("click", (e) => {
running = !running;
e.target.textContent = running ? "Pause" : "Play";
if (running) { last = performance.now(); id = requestAnimationFrame(frame); }
else cancelAnimationFrame(id);
});Line-by-line explanation
- Line 3The ball's position, velocity in pixels per second, and radius.
- Line 9Seconds since the last frame, capped so a paused tab doesn't make it jump.
- Lines 11 to 12Move by velocity × time.
- Lines 13 to 14Reverse direction when the ball hits an edge.
- Lines 16 to 20Clear, then redraw the ball in its new position.
- Line 21Ask for the next frame.
- Lines 28 to 29Restart or cancel the loop with the saved id.
A tiny drawing app
Draw with your mouse or finger.
<canvas id="pad" style="width: 100%; height: 200px; border: 1px solid #ddd; touch-action: none"></canvas>
<button id="clear">Clear</button>const pad = document.querySelector("#pad");
const ctx = pad.getContext("2d");
const dpr = window.devicePixelRatio || 1;
pad.width = pad.clientWidth * dpr;
pad.height = pad.clientHeight * dpr;
ctx.scale(dpr, dpr);
ctx.lineWidth = 4;
ctx.lineCap = "round";
ctx.strokeStyle = "#2563eb";
let drawing = false;
pad.addEventListener("pointerdown", (e) => {
drawing = true;
ctx.beginPath();
ctx.moveTo(e.offsetX, e.offsetY);
});
pad.addEventListener("pointermove", (e) => {
if (!drawing) return;
ctx.lineTo(e.offsetX, e.offsetY);
ctx.stroke();
});
pad.addEventListener("pointerup", () => (drawing = false));
pad.addEventListener("pointerleave", () => (drawing = false));
document.querySelector("#clear").addEventListener("click", () => ctx.clearRect(0, 0, pad.width, pad.height));Line-by-line explanation
- Lines 3 to 6Match the drawing size to the display size times the pixel ratio, so lines are sharp.
- Lines 12 to 16Pointer events cover mouse, touch and pen; start a new line where the user presses.
- Lines 17 to 21Extend the line as the pointer moves.
- Lines 22 to 23Stop drawing when the pointer is released or leaves the canvas.
Common JavaScript Canvas Mistakes
Animating with setInterval
setInterval(draw, 16); // not in sync with the screen, keeps running in hidden tabsfunction loop() { draw(); requestAnimationFrame(loop); }
requestAnimationFrame(loop);Forgetting beginPath
ctx.arc(50, 50, 10, 0, Math.PI * 2); ctx.fill();
ctx.fillStyle = "red";
ctx.arc(100, 50, 10, 0, Math.PI * 2); ctx.fill(); // both circles turn redctx.beginPath(); ctx.arc(50, 50, 10, 0, Math.PI * 2); ctx.fill();
ctx.fillStyle = "red";
ctx.beginPath(); ctx.arc(100, 50, 10, 0, Math.PI * 2); ctx.fill();Sizing a canvas only with CSS
canvas.style.width = "600px"; // drawing size is still 300 x 150, so it's stretched and blurrycanvas.width = 600 * devicePixelRatio;
canvas.style.width = "600px";
ctx.scale(devicePixelRatio, devicePixelRatio);JavaScript Canvas in Real Projects
Canvas in the real world
Chart libraries such as Chart.js draw on canvas. Browser games, from simple puzzles to physics games, run a rAF loop. Figma and other design tools render their editors with canvas and WebGL. Signature pads on delivery and banking apps capture pointer events on a canvas, and photo editors use canvas to crop, filter and export images.
JavaScript Canvas Exercises
Answer each one and check it instantly. If you get stuck, open the hint before the answer.
Get the 2D drawing context.
const ctx = canvas.("2d");
ctx.fillRect(10, 10, 50, 50);
getContext("2d") returns the drawing API.On a canvas, where is the point (0, 100)?
ctx.fillRect(0, 100, 10, 10);Which function should drive a smooth animation loop?
Why multiply speed by elapsed time each frame?
drawDots(ctx, points) should clear the whole canvas, then draw a separate filled circle of radius 5 at each point. Fix the three bugs.
<canvas id="c" width="200" height="100"></canvas>function drawDots(ctx, points) {
ctx.clearRect(0, 0, 10, 10);
for (const p of points) {
ctx.arc(p.x, p.y, 5, 0, Math.PI);
ctx.fill();
}
}
const ctx = document.querySelector("#c").getContext("2d");
drawDots(ctx, [{ x: 20, y: 20 }, { x: 100, y: 50 }]);- Draws full circles
- Clears the whole canvas
- Circles are separate shapes
JavaScript Canvas Quiz
5 questions to check what you learned.
Which element do you draw on with the Canvas API?
- svg
- img
- canvas
- div
How do you draw a full circle with arc?
- arc(x, y, r, 0, 360)
- circle(x, y, r)
- arc(x, y, r, 0, Math.PI)
- arc(x, y, r, 0, Math.PI * 2)
How do you stop a requestAnimationFrame loop?
- clearInterval(id)
- cancelAnimationFrame(id)
- stopAnimation()
- return false
Why does a canvas look blurry on a phone?
- Phones don't support canvas
- The colours are wrong
- Its drawing size is smaller than the screen's real pixels, so it's stretched
- requestAnimationFrame is too slow
Why does each animation frame usually start with clearRect?
- Canvas keeps old pixels, so shapes would leave trails without clearing
- To release memory
- It resets fillStyle
- It's required by requestAnimationFrame
JavaScript Canvas Challenge
Draw a bar chart
Write drawChart(ctx, values) that:
- Clears the whole canvas
- Draws one filled rectangle per value, each
30pixels wide, with a10pixel gap, starting atx = 10 - Each bar is
valuepixels tall and sits on the bottom edge of the canvas (so itsyiscanvas.height - value) - Uses
fillStyle"#2563eb"
<canvas id="chart" width="200" height="120" style="border: 1px solid #ddd"></canvas>function drawChart(ctx, values) {
}
drawChart(document.querySelector("#chart").getContext("2d"), [40, 90, 60]);- First bar is 40px tall at x 10
- Bars have gaps
- Clears before drawing