Drawing and Animating with Canvas

Draw shapes, text and images on an HTML canvas with JavaScript, animate them smoothly with requestAnimationFrame, and handle mouse input and sharp drawing on high-density screens.

Lesson 45 of 56Intermediate16 min read

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

JavaScript
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: x grows to the right, y grows downwards.
  • fillStyle and strokeStyle set 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

MethodDraws
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

JavaScript
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:

JavaScript
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:

JavaScript
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 with beginPath, arc, lineTo, fill and stroke, and fillText.
  • Animate by clearing and redrawing each frame inside a requestAnimationFrame loop.
  • Move by speed × elapsed time so animations run at the same speed on every screen.
  • Scale by devicePixelRatio for 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.

Example 1

Drawing shapes, paths and text

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);
ResultEdit the code, then press Run or Ctrl+Enter
Line-by-line explanation
  1. Lines 3 to 4A blue rectangle for the ground.
  2. Lines 6 to 9A circle for the sun: a full arc from 0 to 2π radians.
  3. Lines 11 to 12The house walls.
  4. Lines 14 to 19A triangle roof drawn as a closed path.
  5. Lines 21 to 23Text with a chosen font.
Example 2

A bouncing ball with requestAnimationFrame

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);
});
ResultEdit the code, then press Run or Ctrl+Enter
Line-by-line explanation
  1. Line 3The ball's position, velocity in pixels per second, and radius.
  2. Line 9Seconds since the last frame, capped so a paused tab doesn't make it jump.
  3. Lines 11 to 12Move by velocity × time.
  4. Lines 13 to 14Reverse direction when the ball hits an edge.
  5. Lines 16 to 20Clear, then redraw the ball in its new position.
  6. Line 21Ask for the next frame.
  7. Lines 28 to 29Restart or cancel the loop with the saved id.
Example 3

A tiny drawing app

Draw with your mouse or finger.

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));
ResultEdit the code, then press Run or Ctrl+Enter
Line-by-line explanation
  1. Lines 3 to 6Match the drawing size to the display size times the pixel ratio, so lines are sharp.
  2. Lines 12 to 16Pointer events cover mouse, touch and pen; start a new line where the user presses.
  3. Lines 17 to 21Extend the line as the pointer moves.
  4. Lines 22 to 23Stop drawing when the pointer is released or leaves the canvas.

Common JavaScript Canvas Mistakes

Animating with setInterval

Don't
setInterval(draw, 16); // not in sync with the screen, keeps running in hidden tabs
Do
function loop() { draw(); requestAnimationFrame(loop); }
requestAnimationFrame(loop);

Forgetting beginPath

Don't
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 red
Do
ctx.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

Don't
canvas.style.width = "600px"; // drawing size is still 300 x 150, so it's stretched and blurry
Do
canvas.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.

Fill in the blankseasy

Get the 2D drawing context.

const ctx = canvas.("2d");
ctx.fillRect(10, 10, 50, 50);
Predict the outputmedium

On a canvas, where is the point (0, 100)?

ctx.fillRect(0, 100, 10, 10);
Multiple choiceeasy

Which function should drive a smooth animation loop?

Multiple choicemedium

Why multiply speed by elapsed time each frame?

Fix the bugmedium

drawDots(ctx, points) should clear the whole canvas, then draw a separate filled circle of radius 5 at each point. Fix the three bugs.

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 }]);
ResultEdit the code, then press Run or Ctrl+Enter
Checks
  • Draws full circles
  • Clears the whole canvas
  • Circles are separate shapes

JavaScript Canvas Quiz

5 questions to check what you learned.

  1. Which element do you draw on with the Canvas API?

    • svg
    • img
    • canvas
    • div
  2. 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)
  3. How do you stop a requestAnimationFrame loop?

    • clearInterval(id)
    • cancelAnimationFrame(id)
    • stopAnimation()
    • return false
  4. 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
  5. 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:

  1. Clears the whole canvas
  2. Draws one filled rectangle per value, each 30 pixels wide, with a 10 pixel gap, starting at x = 10
  3. Each bar is value pixels tall and sits on the bottom edge of the canvas (so its y is canvas.height - value)
  4. Uses fillStyle "#2563eb"
function drawChart(ctx, values) {

}

drawChart(document.querySelector("#chart").getContext("2d"), [40, 90, 60]);
ResultEdit the code, then press Run or Ctrl+Enter
Checks
  • First bar is 40px tall at x 10
  • Bars have gaps
  • Clears before drawing