JavaScript Essentials

Canvas API Basics

45 min Lesson 54 of 60

Introduction to the Canvas Element

The HTML <canvas> element provides a drawable surface in the browser that you can control entirely through JavaScript. Unlike other HTML elements that are rendered by the browser engine, the canvas is a blank bitmap that you draw on pixel by pixel using a powerful drawing API. It is used for rendering graphs and charts, creating game graphics, building image editors, generating visual effects, and creating interactive animations.

The canvas element itself is just a container. All the drawing is performed through a rendering context -- an object that provides the methods and properties for drawing. The most commonly used context is the 2D rendering context, which provides a rich set of methods for drawing shapes, text, images, and paths.

Setting Up the Canvas Element

To use the canvas, you first add the <canvas> element to your HTML with explicit width and height attributes. These attributes define the actual drawing surface resolution in pixels. This is different from CSS width and height, which only control how the canvas is displayed on the page. If you set the canvas size only through CSS, the drawing surface defaults to 300x150 pixels and is then scaled, resulting in blurry graphics.

Example: Canvas HTML Setup

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Canvas Basics</title>
    <style>
        canvas {
            border: 2px solid #333;
            display: block;
            margin: 20px auto;
        }
    </style>
</head>
<body>
    <canvas id="myCanvas" width="800" height="600">
        Your browser does not support the canvas element.
    </canvas>

    <script src="canvas.js"></script>
</body>
</html>
Note: The text between the opening and closing <canvas> tags is fallback content displayed only when the browser does not support canvas. Modern browsers all support canvas, but including fallback content is good practice for accessibility.

Getting the 2D Context

To start drawing, you need to obtain a reference to the canvas element and then get its 2D rendering context. The getContext('2d') method returns a CanvasRenderingContext2D object that provides all the drawing methods you will use throughout this lesson.

Example: Obtaining the 2D Context

// Get the canvas element
const canvas = document.getElementById('myCanvas');

// Check if canvas is supported
if (!canvas.getContext) {
    console.error('Canvas is not supported in this browser');
}

// Get the 2D rendering context
const ctx = canvas.getContext('2d');

// Now you can use ctx to draw on the canvas
console.log('Canvas size:', canvas.width, 'x', canvas.height);
console.log('Context ready:', ctx !== null);

Understanding the Coordinate System

The canvas uses a coordinate system where the origin (0, 0) is at the top-left corner. The x-axis increases to the right, and the y-axis increases downward. This is different from the mathematical coordinate system where y increases upward. Every pixel on the canvas has a unique (x, y) coordinate. For an 800x600 canvas, the valid x values range from 0 to 799 and the valid y values range from 0 to 599.

Example: Visualizing the Coordinate System

const canvas = document.getElementById('myCanvas');
const ctx = canvas.getContext('2d');

// Draw coordinate axes
ctx.strokeStyle = '#999';
ctx.lineWidth = 1;

// X-axis
ctx.beginPath();
ctx.moveTo(0, 0);
ctx.lineTo(canvas.width, 0);
ctx.stroke();

// Y-axis
ctx.beginPath();
ctx.moveTo(0, 0);
ctx.lineTo(0, canvas.height);
ctx.stroke();

// Mark some points with their coordinates
ctx.fillStyle = '#333';
ctx.font = '14px monospace';

const points = [
    { x: 50, y: 50 },
    { x: 200, y: 100 },
    { x: 400, y: 300 },
    { x: 600, y: 200 }
];

points.forEach(function(point) {
    // Draw a dot at each point
    ctx.beginPath();
    ctx.arc(point.x, point.y, 5, 0, Math.PI * 2);
    ctx.fill();

    // Label the point with its coordinates
    ctx.fillText('(' + point.x + ', ' + point.y + ')', point.x + 10, point.y - 10);
});

Drawing Rectangles

Rectangles are the only primitive shape directly supported by the canvas API through dedicated methods. There are three rectangle methods: fillRect() draws a filled rectangle, strokeRect() draws a rectangle outline, and clearRect() clears a rectangular area making it fully transparent. Each method takes four parameters: x position, y position, width, and height.

Example: Drawing Rectangles

const canvas = document.getElementById('myCanvas');
const ctx = canvas.getContext('2d');

// Draw a filled rectangle (solid color)
ctx.fillStyle = '#3498db';
ctx.fillRect(50, 50, 200, 120);

// Draw a stroked rectangle (outline only)
ctx.strokeStyle = '#e74c3c';
ctx.lineWidth = 3;
ctx.strokeRect(300, 50, 200, 120);

// Draw a filled rectangle, then clear part of it
ctx.fillStyle = '#2ecc71';
ctx.fillRect(50, 220, 200, 120);
// Clear a hole in the green rectangle
ctx.clearRect(90, 250, 120, 60);

// Combine fill and stroke on the same rectangle
ctx.fillStyle = '#f39c12';
ctx.strokeStyle = '#e67e22';
ctx.lineWidth = 4;
ctx.fillRect(300, 220, 200, 120);
ctx.strokeRect(300, 220, 200, 120);

// Draw a grid of small rectangles
for (let row = 0; row < 5; row++) {
    for (let col = 0; col < 10; col++) {
        const hue = (row * 10 + col) * 7.2;
        ctx.fillStyle = 'hsl(' + hue + ', 70%, 60%)';
        ctx.fillRect(50 + col * 55, 400 + row * 35, 50, 30);
    }
}

Drawing Paths: Lines and Complex Shapes

All shapes besides rectangles are drawn using paths. A path is a sequence of points connected by lines, arcs, or curves. You build a path step by step using a series of commands, and then you either fill or stroke the path to make it visible. The key path methods are: beginPath() starts a new path, moveTo(x, y) moves the pen without drawing, lineTo(x, y) draws a straight line to the specified point, closePath() draws a line back to the start of the current sub-path, fill() fills the path with the current fill style, and stroke() draws the path outline with the current stroke style.

Example: Drawing Lines and Triangles

const canvas = document.getElementById('myCanvas');
const ctx = canvas.getContext('2d');

// Draw a simple line
ctx.beginPath();
ctx.moveTo(50, 50);
ctx.lineTo(250, 50);
ctx.strokeStyle = '#333';
ctx.lineWidth = 2;
ctx.stroke();

// Draw a triangle using lineTo and closePath
ctx.beginPath();
ctx.moveTo(350, 120);   // Top vertex
ctx.lineTo(280, 220);   // Bottom-left vertex
ctx.lineTo(420, 220);   // Bottom-right vertex
ctx.closePath();         // Closes back to the start (350, 120)
ctx.fillStyle = '#9b59b6';
ctx.fill();
ctx.strokeStyle = '#8e44ad';
ctx.lineWidth = 3;
ctx.stroke();

// Draw a zigzag line
ctx.beginPath();
ctx.moveTo(50, 300);
for (let i = 1; i <= 8; i++) {
    const x = 50 + i * 60;
    const y = i % 2 === 0 ? 300 : 360;
    ctx.lineTo(x, y);
}
ctx.strokeStyle = '#e74c3c';
ctx.lineWidth = 3;
ctx.stroke();

// Draw a star shape
function drawStar(ctx, cx, cy, outerRadius, innerRadius, points) {
    ctx.beginPath();
    for (let i = 0; i < points * 2; i++) {
        const radius = i % 2 === 0 ? outerRadius : innerRadius;
        const angle = (i * Math.PI) / points - Math.PI / 2;
        const x = cx + radius * Math.cos(angle);
        const y = cy + radius * Math.sin(angle);
        if (i === 0) {
            ctx.moveTo(x, y);
        } else {
            ctx.lineTo(x, y);
        }
    }
    ctx.closePath();
}

drawStar(ctx, 600, 160, 80, 35, 5);
ctx.fillStyle = '#f1c40f';
ctx.fill();
ctx.strokeStyle = '#f39c12';
ctx.lineWidth = 2;
ctx.stroke();

Drawing Arcs and Circles

The arc() method draws a circular arc or a full circle. It takes six parameters: the center x and y coordinates, the radius, the start angle, the end angle (both in radians), and an optional boolean for counterclockwise direction. A full circle starts at 0 and ends at 2 * Math.PI (approximately 6.2832 radians, which equals 360 degrees).

Example: Drawing Circles and Arcs

const canvas = document.getElementById('myCanvas');
const ctx = canvas.getContext('2d');

// Draw a filled circle
ctx.beginPath();
ctx.arc(150, 150, 80, 0, Math.PI * 2);
ctx.fillStyle = '#3498db';
ctx.fill();

// Draw a circle outline
ctx.beginPath();
ctx.arc(380, 150, 80, 0, Math.PI * 2);
ctx.strokeStyle = '#e74c3c';
ctx.lineWidth = 4;
ctx.stroke();

// Draw a semicircle (half circle)
ctx.beginPath();
ctx.arc(600, 150, 80, 0, Math.PI); // 0 to PI = half circle
ctx.fillStyle = '#2ecc71';
ctx.fill();

// Draw a quarter circle (pie slice)
ctx.beginPath();
ctx.moveTo(150, 380); // Move to center
ctx.arc(150, 380, 80, 0, Math.PI / 2);
ctx.closePath();
ctx.fillStyle = '#f39c12';
ctx.fill();
ctx.strokeStyle = '#e67e22';
ctx.lineWidth = 2;
ctx.stroke();

// Draw a pac-man shape
ctx.beginPath();
ctx.arc(380, 380, 80, 0.3, Math.PI * 2 - 0.3);
ctx.lineTo(380, 380);
ctx.closePath();
ctx.fillStyle = '#f1c40f';
ctx.fill();

// Draw concentric circles (target)
var colors = ['#e74c3c', '#ffffff', '#e74c3c', '#ffffff', '#e74c3c'];
for (let i = colors.length - 1; i >= 0; i--) {
    ctx.beginPath();
    ctx.arc(600, 380, 20 * (i + 1), 0, Math.PI * 2);
    ctx.fillStyle = colors[i];
    ctx.fill();
}
Pro Tip: Remember that canvas uses radians, not degrees. To convert degrees to radians, multiply by Math.PI / 180. For example, 90 degrees equals 90 * Math.PI / 180 which is Math.PI / 2. A common helper function is function degreesToRadians(deg) { return deg * Math.PI / 180; }.

Bezier Curves

For smooth, flowing curves, the canvas provides two types of Bezier curves. A quadratic Bezier curve uses one control point and is drawn with quadraticCurveTo(cpx, cpy, x, y). A cubic Bezier curve uses two control points for more complex shapes and is drawn with bezierCurveTo(cp1x, cp1y, cp2x, cp2y, x, y). Both methods draw a curve from the current pen position to the endpoint (x, y), with the control points influencing the curve shape.

Example: Drawing Bezier Curves

const canvas = document.getElementById('myCanvas');
const ctx = canvas.getContext('2d');

// Quadratic Bezier curve
ctx.beginPath();
ctx.moveTo(50, 200);
ctx.quadraticCurveTo(250, 50, 450, 200); // Control point at (250, 50)
ctx.strokeStyle = '#3498db';
ctx.lineWidth = 3;
ctx.stroke();

// Visualize the control point
ctx.beginPath();
ctx.arc(250, 50, 5, 0, Math.PI * 2);
ctx.fillStyle = '#e74c3c';
ctx.fill();
ctx.setLineDash([5, 5]);
ctx.beginPath();
ctx.moveTo(50, 200);
ctx.lineTo(250, 50);
ctx.lineTo(450, 200);
ctx.strokeStyle = '#ccc';
ctx.lineWidth = 1;
ctx.stroke();
ctx.setLineDash([]); // Reset dash pattern

// Cubic Bezier curve
ctx.beginPath();
ctx.moveTo(50, 400);
ctx.bezierCurveTo(150, 280, 350, 520, 450, 400);
ctx.strokeStyle = '#2ecc71';
ctx.lineWidth = 3;
ctx.stroke();

// Draw a smooth wave using multiple Bezier curves
ctx.beginPath();
ctx.moveTo(500, 300);
ctx.bezierCurveTo(550, 250, 600, 250, 650, 300);
ctx.bezierCurveTo(700, 350, 750, 350, 800, 300);
ctx.strokeStyle = '#9b59b6';
ctx.lineWidth = 3;
ctx.stroke();

// Draw a heart shape using Bezier curves
ctx.beginPath();
ctx.moveTo(600, 180);
ctx.bezierCurveTo(600, 160, 570, 130, 540, 130);
ctx.bezierCurveTo(490, 130, 490, 180, 490, 180);
ctx.bezierCurveTo(490, 210, 540, 250, 600, 280);
ctx.bezierCurveTo(660, 250, 710, 210, 710, 180);
ctx.bezierCurveTo(710, 180, 710, 130, 660, 130);
ctx.bezierCurveTo(630, 130, 600, 160, 600, 180);
ctx.fillStyle = '#e74c3c';
ctx.fill();

Colors: fillStyle and strokeStyle

The fillStyle property sets the color or style used to fill shapes, while strokeStyle sets the color or style for shape outlines. Both properties accept any valid CSS color value: named colors, hex codes, RGB, RGBA, HSL, and HSLA. You can also assign gradients and patterns, which we will cover next.

Example: Working with Colors

const canvas = document.getElementById('myCanvas');
const ctx = canvas.getContext('2d');

// Named colors
ctx.fillStyle = 'coral';
ctx.fillRect(20, 20, 100, 60);

// Hex colors
ctx.fillStyle = '#6c5ce7';
ctx.fillRect(140, 20, 100, 60);

// RGB colors
ctx.fillStyle = 'rgb(46, 204, 113)';
ctx.fillRect(260, 20, 100, 60);

// RGBA colors (with transparency)
ctx.fillStyle = 'rgba(231, 76, 60, 0.7)';
ctx.fillRect(380, 20, 100, 60);

// HSL colors
ctx.fillStyle = 'hsl(210, 80%, 55%)';
ctx.fillRect(500, 20, 100, 60);

// HSLA with transparency
ctx.fillStyle = 'hsla(48, 95%, 55%, 0.8)';
ctx.fillRect(620, 20, 100, 60);

// Global alpha affects ALL subsequent drawing
ctx.globalAlpha = 0.5;
ctx.fillStyle = '#e74c3c';
ctx.fillRect(50, 120, 200, 100);
ctx.fillStyle = '#3498db';
ctx.fillRect(150, 160, 200, 100); // Overlapping area blends

// Reset global alpha
ctx.globalAlpha = 1.0;

Gradients: Linear and Radial

Gradients allow you to create smooth color transitions. A linear gradient transitions colors along a straight line and is created with createLinearGradient(x0, y0, x1, y1). A radial gradient transitions colors between two circles and is created with createRadialGradient(x0, y0, r0, x1, y1, r1). After creating a gradient object, you add color stops to define where each color appears along the gradient.

Example: Linear and Radial Gradients

const canvas = document.getElementById('myCanvas');
const ctx = canvas.getContext('2d');

// Linear gradient (horizontal)
const linearHorizontal = ctx.createLinearGradient(50, 0, 350, 0);
linearHorizontal.addColorStop(0, '#e74c3c');
linearHorizontal.addColorStop(0.5, '#f39c12');
linearHorizontal.addColorStop(1, '#2ecc71');
ctx.fillStyle = linearHorizontal;
ctx.fillRect(50, 30, 300, 80);

// Linear gradient (vertical)
const linearVertical = ctx.createLinearGradient(0, 140, 0, 280);
linearVertical.addColorStop(0, '#3498db');
linearVertical.addColorStop(1, '#2c3e50');
ctx.fillStyle = linearVertical;
ctx.fillRect(50, 140, 300, 140);

// Linear gradient (diagonal)
const linearDiagonal = ctx.createLinearGradient(400, 30, 700, 280);
linearDiagonal.addColorStop(0, '#9b59b6');
linearDiagonal.addColorStop(0.5, '#e74c3c');
linearDiagonal.addColorStop(1, '#f1c40f');
ctx.fillStyle = linearDiagonal;
ctx.fillRect(400, 30, 300, 250);

// Radial gradient
const radialGradient = ctx.createRadialGradient(200, 430, 10, 200, 430, 120);
radialGradient.addColorStop(0, '#f1c40f');
radialGradient.addColorStop(0.5, '#e67e22');
radialGradient.addColorStop(1, '#e74c3c');
ctx.fillStyle = radialGradient;
ctx.beginPath();
ctx.arc(200, 430, 120, 0, Math.PI * 2);
ctx.fill();

// Radial gradient for a 3D sphere effect
const sphereGradient = ctx.createRadialGradient(530, 400, 5, 560, 430, 100);
sphereGradient.addColorStop(0, '#ffffff');
sphereGradient.addColorStop(0.3, '#3498db');
sphereGradient.addColorStop(1, '#1a5276');
ctx.fillStyle = sphereGradient;
ctx.beginPath();
ctx.arc(560, 430, 100, 0, Math.PI * 2);
ctx.fill();

Patterns

The createPattern() method lets you use an image, another canvas, or a video as a repeating pattern to fill shapes. The method takes the image source and a repetition type: 'repeat' tiles in both directions, 'repeat-x' tiles horizontally, 'repeat-y' tiles vertically, and 'no-repeat' shows the image only once.

Example: Creating and Using Patterns

const canvas = document.getElementById('myCanvas');
const ctx = canvas.getContext('2d');

// Create a pattern from a small canvas (checkerboard)
const patternCanvas = document.createElement('canvas');
patternCanvas.width = 40;
patternCanvas.height = 40;
const patternCtx = patternCanvas.getContext('2d');

// Draw a checkerboard tile
patternCtx.fillStyle = '#ecf0f1';
patternCtx.fillRect(0, 0, 40, 40);
patternCtx.fillStyle = '#bdc3c7';
patternCtx.fillRect(0, 0, 20, 20);
patternCtx.fillRect(20, 20, 20, 20);

// Use the small canvas as a repeating pattern
const checkerPattern = ctx.createPattern(patternCanvas, 'repeat');
ctx.fillStyle = checkerPattern;
ctx.fillRect(50, 50, 300, 200);

// Create a pattern from an image
const img = new Image();
img.onload = function() {
    const imagePattern = ctx.createPattern(img, 'repeat');
    ctx.fillStyle = imagePattern;
    ctx.fillRect(400, 50, 300, 200);
};
img.src = 'texture.png';

// Create a striped pattern
const stripeCanvas = document.createElement('canvas');
stripeCanvas.width = 20;
stripeCanvas.height = 20;
const stripeCtx = stripeCanvas.getContext('2d');
stripeCtx.fillStyle = '#3498db';
stripeCtx.fillRect(0, 0, 20, 20);
stripeCtx.fillStyle = '#2980b9';
stripeCtx.fillRect(0, 0, 10, 20);

const stripePattern = ctx.createPattern(stripeCanvas, 'repeat');
ctx.fillStyle = stripePattern;
ctx.beginPath();
ctx.arc(200, 400, 100, 0, Math.PI * 2);
ctx.fill();

Drawing Text

The canvas API provides two methods for rendering text: fillText(text, x, y) draws filled text and strokeText(text, x, y) draws text outlines. You control the appearance through the font property (using CSS font syntax), textAlign for horizontal alignment, and textBaseline for vertical alignment. An optional fourth parameter limits the maximum width, scaling the text to fit if necessary.

Example: Drawing and Styling Text

const canvas = document.getElementById('myCanvas');
const ctx = canvas.getContext('2d');

// Basic filled text
ctx.font = '36px Arial';
ctx.fillStyle = '#2c3e50';
ctx.fillText('Hello Canvas!', 50, 60);

// Stroked text (outline only)
ctx.font = 'bold 48px Georgia';
ctx.strokeStyle = '#e74c3c';
ctx.lineWidth = 2;
ctx.strokeText('Outlined Text', 50, 130);

// Combined fill and stroke for a bold effect
ctx.font = 'bold 42px Verdana';
ctx.fillStyle = '#3498db';
ctx.strokeStyle = '#2c3e50';
ctx.lineWidth = 1;
ctx.fillText('Bold Effect', 50, 200);
ctx.strokeText('Bold Effect', 50, 200);

// Text alignment demonstration
ctx.font = '20px Arial';
ctx.fillStyle = '#333';
var centerX = canvas.width / 2;

// Draw a vertical center line for reference
ctx.beginPath();
ctx.moveTo(centerX, 240);
ctx.lineTo(centerX, 400);
ctx.strokeStyle = '#ccc';
ctx.lineWidth = 1;
ctx.stroke();

ctx.fillStyle = '#333';
ctx.textAlign = 'left';
ctx.fillText('Left aligned', centerX, 270);

ctx.textAlign = 'center';
ctx.fillText('Center aligned', centerX, 310);

ctx.textAlign = 'right';
ctx.fillText('Right aligned', centerX, 350);

// Reset alignment
ctx.textAlign = 'left';

// Measure text width
ctx.font = '24px monospace';
var text = 'Measured text width';
var metrics = ctx.measureText(text);
ctx.fillText(text, 50, 450);
ctx.fillText('Width: ' + metrics.width.toFixed(1) + 'px', 50, 480);

Drawing Images

The drawImage() method draws images, video frames, or other canvases onto your canvas. It has three forms: the simplest takes an image and position drawImage(image, x, y), the scaled form adds width and height drawImage(image, x, y, width, height), and the slicing form lets you crop a portion of the source image drawImage(image, sx, sy, sWidth, sHeight, dx, dy, dWidth, dHeight) where the s parameters define the source rectangle and the d parameters define the destination rectangle.

Example: Drawing Images on Canvas

const canvas = document.getElementById('myCanvas');
const ctx = canvas.getContext('2d');

const img = new Image();
img.onload = function() {
    // Draw at original size at position (50, 50)
    ctx.drawImage(img, 50, 50);

    // Draw scaled to specific dimensions
    ctx.drawImage(img, 300, 50, 200, 150);

    // Draw a cropped portion (sprite sheet slicing)
    // Source: start at (10, 10), take 100x100 area
    // Destination: place at (550, 50), scale to 150x150
    ctx.drawImage(img, 10, 10, 100, 100, 550, 50, 150, 150);

    // Draw with a border frame
    ctx.strokeStyle = '#2c3e50';
    ctx.lineWidth = 4;
    ctx.strokeRect(298, 48, 204, 154);
};
img.src = 'landscape.jpg';

// Draw from another canvas
const offscreenCanvas = document.createElement('canvas');
offscreenCanvas.width = 100;
offscreenCanvas.height = 100;
const offCtx = offscreenCanvas.getContext('2d');
offCtx.fillStyle = '#9b59b6';
offCtx.beginPath();
offCtx.arc(50, 50, 45, 0, Math.PI * 2);
offCtx.fill();

// Use the offscreen canvas as an image source
ctx.drawImage(offscreenCanvas, 50, 350);
ctx.drawImage(offscreenCanvas, 170, 350);
ctx.drawImage(offscreenCanvas, 290, 350);
Warning: Always draw images inside the onload event handler. If you try to draw an image before it has finished loading, nothing will appear on the canvas. For multiple images, use a loading counter or Promise.all to ensure all images are ready before drawing.

Transformations: translate, rotate, scale

Canvas transformations allow you to move, rotate, and scale the entire coordinate system rather than individual shapes. This makes it easy to draw complex scenes with rotated or scaled elements. The three main transformation methods are: translate(x, y) moves the origin point, rotate(angle) rotates the canvas around the current origin (in radians), and scale(x, y) scales the coordinate system. Transformations are cumulative -- each one builds on the previous state.

Example: Using Transformations

const canvas = document.getElementById('myCanvas');
const ctx = canvas.getContext('2d');

// Translation: move the origin
ctx.fillStyle = '#3498db';
ctx.fillRect(0, 0, 80, 40); // Drawn at original origin

ctx.translate(150, 80);
ctx.fillStyle = '#e74c3c';
ctx.fillRect(0, 0, 80, 40); // Drawn at translated origin (150, 80)

// Reset transformations
ctx.setTransform(1, 0, 0, 1, 0, 0);

// Rotation: rotate around a point
// To rotate around the center of a shape:
// 1. Translate to the center of the shape
// 2. Rotate
// 3. Draw the shape centered at origin
ctx.translate(400, 100);
ctx.rotate(Math.PI / 6); // 30 degrees

ctx.fillStyle = '#2ecc71';
ctx.fillRect(-50, -25, 100, 50); // Centered at origin

ctx.setTransform(1, 0, 0, 1, 0, 0);

// Scale: make things bigger or smaller
ctx.translate(200, 300);
ctx.scale(2, 2); // Double the size
ctx.fillStyle = '#9b59b6';
ctx.fillRect(0, 0, 50, 30); // Appears as 100x60

ctx.setTransform(1, 0, 0, 1, 0, 0);

// Combine transformations to draw a pattern
for (let i = 0; i < 12; i++) {
    ctx.translate(600, 350);
    ctx.rotate((i * Math.PI * 2) / 12);
    ctx.fillStyle = 'hsl(' + (i * 30) + ', 70%, 55%)';
    ctx.fillRect(50, -10, 60, 20);
    ctx.setTransform(1, 0, 0, 1, 0, 0);
}

Saving and Restoring State

The save() and restore() methods manage the canvas drawing state stack. When you call save(), the current state (including all transformations, clipping regions, fillStyle, strokeStyle, lineWidth, font, globalAlpha, and more) is pushed onto a stack. When you call restore(), the most recently saved state is popped from the stack and applied. This is essential when using transformations, as it lets you apply temporary changes and then return to the previous state cleanly.

Example: save() and restore() in Practice

const canvas = document.getElementById('myCanvas');
const ctx = canvas.getContext('2d');

// Set base state
ctx.fillStyle = '#333';
ctx.font = '20px Arial';
ctx.fillText('Base state', 20, 30);

// Save state and make changes
ctx.save();
ctx.fillStyle = '#e74c3c';
ctx.font = 'bold 28px Georgia';
ctx.translate(50, 80);
ctx.fillText('Modified state', 0, 0);

// Save again (nested) and make more changes
ctx.save();
ctx.fillStyle = '#3498db';
ctx.rotate(0.1);
ctx.translate(0, 50);
ctx.fillText('Nested modified state', 0, 0);

// Restore to first saved state
ctx.restore();
ctx.translate(0, 100);
ctx.fillText('Back to first save (red Georgia)', 0, 0);

// Restore to original base state
ctx.restore();
ctx.fillText('Back to base state (dark Arial)', 20, 300);

// Practical example: drawing multiple rotated shapes
function drawRotatedRect(ctx, x, y, width, height, angle, color) {
    ctx.save();
    ctx.translate(x + width / 2, y + height / 2);
    ctx.rotate(angle);
    ctx.fillStyle = color;
    ctx.fillRect(-width / 2, -height / 2, width, height);
    ctx.restore(); // State is fully restored after each call
}

drawRotatedRect(ctx, 400, 100, 80, 40, Math.PI / 4, '#2ecc71');
drawRotatedRect(ctx, 520, 100, 80, 40, -Math.PI / 6, '#f39c12');
drawRotatedRect(ctx, 640, 100, 80, 40, Math.PI / 3, '#9b59b6');
Pro Tip: Always pair save() and restore() calls. A common pattern is to call save() at the beginning of a drawing function and restore() at the end. This ensures each function cleans up its own state changes and does not affect other drawing code.

Animation with requestAnimationFrame

To create smooth animations on canvas, use requestAnimationFrame() instead of setInterval() or setTimeout(). The browser calls your animation function right before the next screen repaint, typically 60 times per second. This ensures smooth animation that is synchronized with the display refresh rate and automatically pauses when the browser tab is not visible, saving CPU and battery.

Example: Animating a Bouncing Ball

const canvas = document.getElementById('myCanvas');
const ctx = canvas.getContext('2d');

// Ball properties
const ball = {
    x: 100,
    y: 100,
    radius: 25,
    dx: 4,   // Horizontal velocity
    dy: 3,   // Vertical velocity
    color: '#e74c3c'
};

function drawBall() {
    ctx.beginPath();
    ctx.arc(ball.x, ball.y, ball.radius, 0, Math.PI * 2);
    ctx.fillStyle = ball.color;
    ctx.fill();
    ctx.strokeStyle = '#c0392b';
    ctx.lineWidth = 2;
    ctx.stroke();
}

function update() {
    // Move the ball
    ball.x += ball.dx;
    ball.y += ball.dy;

    // Bounce off walls
    if (ball.x + ball.radius > canvas.width || ball.x - ball.radius < 0) {
        ball.dx = -ball.dx;
    }
    if (ball.y + ball.radius > canvas.height || ball.y - ball.radius < 0) {
        ball.dy = -ball.dy;
    }
}

function animate() {
    // Clear the entire canvas
    ctx.clearRect(0, 0, canvas.width, canvas.height);

    // Draw background
    ctx.fillStyle = '#ecf0f1';
    ctx.fillRect(0, 0, canvas.width, canvas.height);

    // Update position and draw
    update();
    drawBall();

    // Request the next frame
    requestAnimationFrame(animate);
}

// Start the animation
animate();

Example: Animating Multiple Objects with Delta Time

const canvas = document.getElementById('myCanvas');
const ctx = canvas.getContext('2d');

// Create multiple particles
const particles = [];
for (let i = 0; i < 50; i++) {
    particles.push({
        x: Math.random() * canvas.width,
        y: Math.random() * canvas.height,
        radius: Math.random() * 8 + 2,
        dx: (Math.random() - 0.5) * 200, // pixels per second
        dy: (Math.random() - 0.5) * 200,
        hue: Math.random() * 360
    });
}

let lastTime = 0;

function animate(currentTime) {
    // Calculate delta time in seconds
    const deltaTime = (currentTime - lastTime) / 1000;
    lastTime = currentTime;

    // Skip the first frame (deltaTime would be huge)
    if (deltaTime > 0.1) {
        requestAnimationFrame(animate);
        return;
    }

    // Clear canvas
    ctx.clearRect(0, 0, canvas.width, canvas.height);

    // Update and draw each particle
    particles.forEach(function(particle) {
        // Update position based on velocity and delta time
        particle.x += particle.dx * deltaTime;
        particle.y += particle.dy * deltaTime;

        // Wrap around edges
        if (particle.x < 0) particle.x = canvas.width;
        if (particle.x > canvas.width) particle.x = 0;
        if (particle.y < 0) particle.y = canvas.height;
        if (particle.y > canvas.height) particle.y = 0;

        // Slowly change hue
        particle.hue = (particle.hue + 30 * deltaTime) % 360;

        // Draw particle
        ctx.beginPath();
        ctx.arc(particle.x, particle.y, particle.radius, 0, Math.PI * 2);
        ctx.fillStyle = 'hsl(' + particle.hue + ', 80%, 60%)';
        ctx.fill();
    });

    requestAnimationFrame(animate);
}

// Start animation
requestAnimationFrame(animate);

Building a Simple Drawing App

Let us combine everything you have learned to build a functional drawing application. The app will let the user draw freehand lines with the mouse, choose colors and brush sizes, and clear the canvas. This demonstrates event handling, path drawing, and canvas state management working together.

Example: Complete Drawing Application

// HTML structure needed:
// <canvas id="drawCanvas" width="800" height="500"></canvas>
// <div id="controls">
//   <input type="color" id="colorPicker" value="#333333">
//   <input type="range" id="brushSize" min="1" max="50" value="5">
//   <span id="sizeLabel">5px</span>
//   <button id="clearBtn">Clear</button>
//   <button id="undoBtn">Undo</button>
// </div>

const canvas = document.getElementById('drawCanvas');
const ctx = canvas.getContext('2d');

// Drawing state
let isDrawing = false;
let lastX = 0;
let lastY = 0;
let currentColor = '#333333';
let currentLineWidth = 5;

// Undo history
const history = [];
const maxHistory = 20;

// Save current state to history
function saveState() {
    if (history.length >= maxHistory) {
        history.shift(); // Remove oldest state
    }
    history.push(canvas.toDataURL());
}

// Set up the initial canvas
ctx.fillStyle = '#ffffff';
ctx.fillRect(0, 0, canvas.width, canvas.height);
saveState();

// Get mouse position relative to canvas
function getPosition(event) {
    const rect = canvas.getBoundingClientRect();
    return {
        x: event.clientX - rect.left,
        y: event.clientY - rect.top
    };
}

// Start drawing
canvas.addEventListener('mousedown', function(event) {
    isDrawing = true;
    var pos = getPosition(event);
    lastX = pos.x;
    lastY = pos.y;
});

// Draw as mouse moves
canvas.addEventListener('mousemove', function(event) {
    if (!isDrawing) return;

    var pos = getPosition(event);

    ctx.beginPath();
    ctx.moveTo(lastX, lastY);
    ctx.lineTo(pos.x, pos.y);
    ctx.strokeStyle = currentColor;
    ctx.lineWidth = currentLineWidth;
    ctx.lineCap = 'round';
    ctx.lineJoin = 'round';
    ctx.stroke();

    lastX = pos.x;
    lastY = pos.y;
});

// Stop drawing
canvas.addEventListener('mouseup', function() {
    if (isDrawing) {
        isDrawing = false;
        saveState();
    }
});

canvas.addEventListener('mouseleave', function() {
    if (isDrawing) {
        isDrawing = false;
        saveState();
    }
});

// Color picker
document.getElementById('colorPicker').addEventListener('input', function(event) {
    currentColor = event.target.value;
});

// Brush size slider
document.getElementById('brushSize').addEventListener('input', function(event) {
    currentLineWidth = parseInt(event.target.value, 10);
    document.getElementById('sizeLabel').textContent = currentLineWidth + 'px';
});

// Clear button
document.getElementById('clearBtn').addEventListener('click', function() {
    ctx.fillStyle = '#ffffff';
    ctx.fillRect(0, 0, canvas.width, canvas.height);
    saveState();
});

// Undo button
document.getElementById('undoBtn').addEventListener('click', function() {
    if (history.length <= 1) return; // Nothing to undo
    history.pop(); // Remove current state
    var previousState = history[history.length - 1];

    var img = new Image();
    img.onload = function() {
        ctx.clearRect(0, 0, canvas.width, canvas.height);
        ctx.drawImage(img, 0, 0);
    };
    img.src = previousState;
});
Note: The drawing app uses canvas.toDataURL() to save canvas snapshots for the undo feature. This converts the entire canvas to a base64-encoded PNG image string. For large canvases, consider using getImageData() and putImageData() instead, as they work with raw pixel data without the overhead of PNG encoding.

Canvas Performance Tips

Canvas drawing performance becomes critical when rendering complex scenes or running animations at 60 frames per second. Here are key optimization techniques to keep your canvas running smoothly:

  • Minimize state changes -- Group drawing operations by style (color, line width). Changing fillStyle or strokeStyle between every draw call is expensive.
  • Use offscreen canvases -- Pre-render complex static elements to an offscreen canvas, then draw them onto the main canvas with a single drawImage() call.
  • Clear only what changed -- Instead of clearing the entire canvas each frame, clear only the regions that need updating.
  • Avoid floating-point coordinates -- Use integer coordinates for sharp pixel-aligned rendering. Floating-point coordinates force anti-aliasing calculations.
  • Batch path operations -- Combine multiple shapes into a single path when they share the same style, then call fill() or stroke() once.
  • Use requestAnimationFrame -- Never use setInterval for animations. It does not synchronize with the display refresh rate and can cause tearing.

Example: Offscreen Canvas for Performance

// Create an offscreen canvas for static background
const bgCanvas = document.createElement('canvas');
bgCanvas.width = 800;
bgCanvas.height = 600;
const bgCtx = bgCanvas.getContext('2d');

// Draw complex static background once
function drawBackground() {
    bgCtx.fillStyle = '#1a1a2e';
    bgCtx.fillRect(0, 0, bgCanvas.width, bgCanvas.height);

    // Draw 200 stars (expensive but only done once)
    for (let i = 0; i < 200; i++) {
        bgCtx.beginPath();
        bgCtx.arc(
            Math.random() * bgCanvas.width,
            Math.random() * bgCanvas.height,
            Math.random() * 2 + 0.5,
            0, Math.PI * 2
        );
        bgCtx.fillStyle = 'rgba(255, 255, 255, ' + (Math.random() * 0.8 + 0.2) + ')';
        bgCtx.fill();
    }
}

drawBackground(); // Render background once

// In the animation loop, just draw the pre-rendered background
const mainCanvas = document.getElementById('myCanvas');
const mainCtx = mainCanvas.getContext('2d');

function animate() {
    // Draw pre-rendered background (very fast: single drawImage call)
    mainCtx.drawImage(bgCanvas, 0, 0);

    // Draw animated elements on top
    drawAnimatedElements(mainCtx);

    requestAnimationFrame(animate);
}

animate();

Practice Exercise

Build an interactive canvas project that combines drawing, animation, and user interaction. Create an 800x600 canvas with the following features. First, draw a starfield background with 100 small circles of varying sizes and opacities. Second, create an animated spaceship (a triangle) that the user controls with the arrow keys -- use translate() and rotate() to handle movement and direction. Third, when the user presses the spacebar, draw a small circle projectile that flies in the direction the spaceship is facing. Fourth, add five circular asteroid targets at random positions that change color when hit by a projectile. Use save() and restore() for all transformations, implement the animation with requestAnimationFrame(), use an offscreen canvas for the static starfield background, and display a score counter and FPS meter drawn with fillText(). Add a gradient fill to the spaceship and use arc() for all circular objects.