Intermediate

Creating Graphics with Canvas and SVG

The rich features of JavaScript, combined with the power of modern web browsers, have blurred the lines between the capabilities of web-based and native applications. This course discusse...

Table of Contents


Module 1: Course Overview

Almost every website needs some graphics to provide the best possible user experience. This course shows you how to add custom graphics to your web pages using the HTML Canvas and SVG documents. Some of the major topics covered include:

  • How to get started with both SVG and Canvas graphics
  • Adding basic shapes and paths to your images
  • How to add text and external images into your graphics

Module 2: Getting Started with Canvas and SVG Graphics

Introduction

The rich features of JavaScript, combined with the power of modern web browsers, have blurred the lines between the capabilities of web-based and native applications. This course discusses one of the areas that native applications typically dominate: graphics.

Why Add Custom Graphics?

There are several compelling use cases for adding custom graphics to web applications:

Custom Charts Depending on what you’re trying to do, your team might decide to use a prebuilt charting library, and that’s perfectly acceptable. But there are many cases where the charting requirements aren’t quite met by a pre-existing library, or the requirements aren’t strong enough to justify importing an entire new third-party dependency. The ability to create simple charts using custom graphics can simplify your application, make it more maintainable, and get it out the door faster.

Image Editing It might sound intimidating to try and add image editing into a web browser, but the capability available with custom graphics actually makes a lot of simple editors relatively easy to implement.

Sketching That might be for some sort of game, or sketching APIs are often used for people to add signatures to documents. All of these use cases are supported by custom graphics.

Custom Games Modern games are highly interactive. Using custom graphics, you can create rich, interactive game experiences directly in the browser without requiring any plugins or additional installations.


Overview of SVG and Canvas Graphics

There are two different ways to add graphics to web applications covered in this course.

SVG (Scalable Vector Graphics)

  • XML-based language, similar to HTML
  • You add elements into the HTML document that declaratively influence how graphics are displayed
  • Integrates very well with HTML and CSS — because it’s XML-based and sits right in the document with other HTML elements, it lends itself well to tight integration with the rest of the document
  • Consistent quality regardless of rendered size — SVG does not describe pixel-by-pixel what to draw; it gives the browser directions and lets it interpret them. This allows it to redraw graphics regardless of how much you zoom in or out, resulting in clear, crisp visuals at any zoom level
<!-- Example SVG document structure -->
<svg xmlns="http://www.w3.org/2000/svg" height="400" width="400">
    <circle cx="200" cy="200" r="150" fill="#DECCB8" stroke="black" stroke-width="2"/>
    <g id="meringue">
        <path d="M 200 200 q 65 -65 0 -130" stroke="#b3702d" fill="transparent"/>
    </g>
    <text x="200" y="30" text-anchor="middle">My Graphic</text>
</svg>

The key elements visible in an SVG document:

  • An outer <svg> tag that wraps the entire graphics element
  • Child elements like <circle>, <g> (group), <path>, and <text>

Canvas Graphics

  • JavaScript-rendered — you describe what to draw using imperative JavaScript code
  • A <canvas> tag identifies where in the page the drawing will appear
  • Rendering is done programmatically through a drawing context
<!-- Minimal Canvas setup -->
<canvas id="myCanvas" height="400" width="400"></canvas>
<script>
    const canvas = document.getElementById('myCanvas');
    const ctx = canvas.getContext('2d');
    // All drawing happens through ctx
</script>

Architecture Comparison

flowchart LR
    subgraph SVG["SVG (Declarative)"]
        A[HTML Document] --> B[SVG Element]
        B --> C[circle / rect / path / text]
        C --> D[Browser Renders via DOM]
        D --> E[CSS Styleable & Animatable]
    end

    subgraph Canvas["Canvas (Imperative)"]
        F[HTML Document] --> G[canvas Element]
        G --> H[JavaScript]
        H --> I[getContext '2d']
        I --> J[Drawing Commands]
        J --> K[Bitmap Pixels]
    end

First Steps with SVG

The skeleton of an SVG element is all you need to start. Let’s examine each part:

<svg xmlns="http://www.w3.org/2000/svg" height="100" width="100">
    <!-- SVG content goes here -->
</svg>

Breaking it down:

PartPurpose
<svg> tagWraps the entire SVG image; identifies the section of the document the browser will treat as an image
xmlns attributeThe XML namespace — defines the vocabulary for the SVG element. Tells the browser about SVG-specific elements like <circle> that aren’t part of standard HTML
height / widthSets the dimensions of the image within the HTML page. These can be manipulated with CSS, but should be set here as they influence how internal elements are rendered

First Steps with Canvas

Canvas requires a bit more setup work than SVG. Here is the minimum needed to start with Canvas graphics:

<!DOCTYPE html>
<html>
<body>
    <!-- The canvas element defines WHERE the image will be drawn -->
    <canvas id="myCanvas" height="400" width="400"></canvas>

    <script>
        // Step 1: Get a reference to the canvas element
        const canvas = document.getElementById('myCanvas');

        // Step 2: Obtain a drawing context
        // All actual drawing is done through this context
        const ctx = canvas.getContext('2d');

        // Step 3: Draw using the context
        ctx.fillStyle = 'blue';
        ctx.fillRect(10, 10, 100, 100);
    </script>
</body>
</html>

Key concepts:

  • The <canvas> tag represents where the image will be rendered (just like <svg> does for SVG)
  • The id attribute on the canvas is useful to retrieve a JavaScript reference to it
  • Canvas graphics are rendered using JavaScript — the canvas tag identifies the location, but the rendering instructions are in JavaScript
  • getContext('2d') returns the drawing context, which is the object through which all drawing commands are issued
  • The '2d' string indicates 2D rendering. Other contexts exist (e.g., 'webgl' for 3D), but this course focuses on 2D

Course Roadmap

The rest of this course examines the capabilities of SVG graphics and Canvas graphics in parallel. Each module focuses on a specific capability, then applies it using both SVG and Canvas as part of building a small course demo (a custom pie builder).

flowchart TD
    M2[Module 2: Getting Started] --> M3[Module 3: Basic Shapes]
    M3 --> M4[Module 4: Paths]
    M4 --> M5[Module 5: Text]
    M5 --> M6[Module 6: Images]
    M6 --> M7[Module 7: Unique Strengths]

    M3 --> SVG3[SVG: rect / circle / ellipse]
    M3 --> CVS3[Canvas: fillRect / ellipse]

    M4 --> SVG4[SVG: path element & d attribute]
    M4 --> CVS4[Canvas: beginPath / moveTo / lineTo]

    M5 --> SVG5[SVG: text element]
    M5 --> CVS5[Canvas: fillText / measureText]

    M6 --> SVG6[SVG: image element + xlink namespace]
    M6 --> CVS6[Canvas: drawImage]

    M7 --> SVG7[SVG: CSS Transitions & Animation]
    M7 --> CVS7[Canvas: Mouse Events & Sketching]

Topics covered:

  1. Basic Shapes — rectangles, circles, arcs, lines
  2. Paths — arbitrary shapes using path commands
  3. Text — adding and styling text in graphics
  4. Images — embedding pre-rendered images
  5. Unique Strengths — CSS animation with SVG; interactive sketching with Canvas

Module 3: Drawing Basic Shapes

Drawing Basic Shapes with SVG

Basic shapes in SVG are declared as XML elements with attributes describing their geometry, color, and stroke. Let’s build from the outer SVG tag inward.

The rect Element

<svg xmlns="http://www.w3.org/2000/svg" height="200" width="200">
    <rect
        x="10"
        y="10"
        width="50"
        height="50"
        stroke="black"
        stroke-width="5"
        fill="rgb(200, 100, 50)"
    />
</svg>
AttributeDescription
x, yPosition of the upper-left corner
width, heightDimensions of the rectangle
strokeOutline/border color (accepts named colors or RGB)
stroke-widthWidth of the outline in pixels
fillFill/body color

The circle Element

<svg xmlns="http://www.w3.org/2000/svg" height="400" width="400">
    <circle
        cx="200"
        cy="200"
        r="150"
        fill="#DECCB8"
        stroke="black"
        stroke-width="2"
    />
</svg>
AttributeDescription
cx, cyCenter point coordinates
rRadius
fillFill color
strokeBorder color

The ellipse Element

Similar to circle but with two radii:

<ellipse cx="200" cy="200" rx="150" ry="100" fill="lightblue"/>

The line Element

<line x1="0" y1="0" x2="200" y2="200" stroke="black" stroke-width="2"/>

SVG Coordinate System

flowchart LR
    subgraph coord["SVG Coordinate System"]
        direction TB
        O["(0,0) — Origin\n(top-left)"]
        X["X → increases right"]
        Y["Y ↓ increases down"]
    end

Important: The Y axis in SVG (and Canvas) increases downward, which is the opposite of typical math coordinates.


Demo: Basic Shapes with SVG

The demo builds an interactive custom pie builder. Starting with basic shapes, we use two concentric circles to represent the pie crust, with JavaScript wiring up the color selection.

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Custom Pie Builder</title>
    <style>
        #options {
            width: 300px;
            display: inline-block;
            vertical-align: top;
        }
    </style>
</head>
<body>
    <svg xmlns="http://www.w3.org/2000/svg" width="400" height="400">
        <rect x="0" y="0" width="400" height="400" fill="transparent" stroke="black"/>
        <circle id="crust-outer" cx="200" cy="200" r="150"
            fill="transparent"/>
        <circle id="crust-inner" cx="200" cy="200" r="110"
            fill="transparent"/>
    </svg>
    <div id="options">
        <fieldset>
            <legend>Please choose a crust</legend>
            <input type="radio" name="crust" value="none" checked/>None<br>
            <input type="radio" name="crust" value="pastry"/>Pastry<br>
            <input type="radio" name="crust" value="graham"/>Graham Cracker<br>
            <input type="radio" name="crust" value="chocolate"/>Chocolate<br>
        </fieldset>
    </div>
    <script>
        const crustElements = document.querySelectorAll('[name=crust]');
        const outerCrust = document.getElementById('crust-outer');
        const innerCrust = document.getElementById('crust-inner');

        document.getElementById('options').addEventListener('change', (e) => {
            if (e.target.name === 'crust') {
                let colors = [];
                switch (e.target.value) {
                case 'none':
                    colors = ['transparent', 'transparent'];
                    break;
                case 'pastry':
                    colors = ['#DECCB8', '#F2DFC9'];
                    break;
                case 'graham':
                    colors = ['#BA870D', '#DEA010'];
                    break;
                case 'chocolate':
                    colors = ['#2E1802', '#422303'];
                    break;
                default:
                    return;
                }
                innerCrust.setAttribute('fill', colors[0]);
                outerCrust.setAttribute('fill', colors[1]);
            }
        });
    </script>
</body>
</html>

Key observations:

  • SVG elements are referenced via getElementById just like any other DOM element
  • Attributes are updated dynamically using setAttribute
  • The two concentric circles create the crust ring effect: the larger outer circle (r="150") and the inner circle (r="110") together form the visible ring
  • The crust ring is the area between radius 110 and 150

Drawing Basic Shapes with Canvas

Now let’s do the same thing using Canvas graphics. Where SVG uses declarative structures, Canvas takes an imperative approach — you tell the browser step by step what to do.

Rectangles

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

// Filled rectangle
ctx.fillStyle = '#DECCB8';
ctx.fillRect(x, y, width, height);

// Stroked rectangle (outline only)
ctx.strokeStyle = 'black';
ctx.strokeRect(x, y, width, height);

// Clear a rectangle (erase pixels)
ctx.clearRect(0, 0, canvas.width, canvas.height);

Circles and Arcs using ellipse

ctx.beginPath();
ctx.ellipse(
    centerX,    // x center
    centerY,    // y center
    radiusX,    // horizontal radius
    radiusY,    // vertical radius
    rotation,   // rotation angle (radians)
    startAngle, // start angle (radians)
    endAngle    // end angle (radians)
);
ctx.fill();    // fill the shape
ctx.stroke();  // or stroke the outline

For a full circle: ctx.ellipse(200, 200, 150, 150, 0, 0, 2 * Math.PI)

Canvas Drawing State

Canvas maintains a drawing state that includes properties like fillStyle and strokeStyle. These must be set before drawing commands:

ctx.fillStyle = '#FF0000';   // Set fill color
ctx.strokeStyle = 'black';   // Set stroke color
ctx.lineWidth = 2;           // Set line width

Demo: Basic Shapes with Canvas

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Custom Pie Builder</title>
    <style>
        #options {
            width: 300px;
            display: inline-block;
            vertical-align: top;
        }
    </style>
</head>
<body>
    <canvas id="canvas" height="400" width="400"></canvas>
    <div id="options">
        <fieldset>
            <legend>Please choose a crust</legend>
            <input type="radio" name="crust" value="none" checked/>None<br>
            <input type="radio" name="crust" value="pastry"/>Pastry<br>
            <input type="radio" name="crust" value="graham"/>Graham Cracker<br>
            <input type="radio" name="crust" value="chocolate"/>Chocolate<br>
        </fieldset>
    </div>
    <script>
        document.getElementById('options').addEventListener('change', () => {
            const options = {};
            document.querySelectorAll('[name=crust]').forEach(elem => {
                if (elem.checked) {
                    options.crust = elem.value;
                }
            });
            render(options);
        });

        function render(options) {
            const canvas = document.getElementById('canvas');
            const ctx = canvas.getContext('2d');
            // Always clear the canvas before re-drawing
            ctx.clearRect(0, 0, canvas.width, canvas.height);

            let outerCrustColor = '';
            let innerCrustColor = '';
            switch (options.crust) {
                case 'pastry':
                    outerCrustColor = '#DECCB8';
                    innerCrustColor = '#F2DFC9';
                    break;
                case 'graham':
                    outerCrustColor = '#BA870D';
                    innerCrustColor = '#DEA010';
                    break;
                case 'chocolate':
                    outerCrustColor = '#2E1802';
                    innerCrustColor = '#422303';
                    break;
            }
            if (outerCrustColor && innerCrustColor) {
                // Draw outer crust circle (radius 150)
                ctx.fillStyle = outerCrustColor;
                ctx.beginPath();
                ctx.ellipse(200, 200, 150, 150, 0, 0, 2 * Math.PI);
                ctx.fill();

                // Draw inner crust circle (radius 110) on top
                ctx.fillStyle = innerCrustColor;
                ctx.beginPath();
                ctx.ellipse(200, 200, 110, 110, 0, 0, 2 * Math.PI);
                ctx.fill();
            }
        }
    </script>
</body>
</html>

Key differences from SVG:

  • Canvas uses a render function pattern — because there is no DOM to update, the entire canvas must be redrawn on each change
  • ctx.clearRect(0, 0, canvas.width, canvas.height) clears the entire canvas before each redraw
  • Drawing order matters — elements drawn later appear on top (painter’s model)
  • Instead of setAttribute, you simply re-run the render function with updated state

Module 4: Creating Arbitrary Shapes with Paths

Creating Custom Paths with SVG

Basic shapes take you a long way, but for arbitrary forms, paths are the tool of choice. A path in SVG is declared using the <path> element and its d attribute, which contains a series of drawing commands.

Path Commands

The d attribute contains a mini-language for drawing:

CommandTypeDescription
M x,yMove To (absolute)Move the pen to a point without drawing
m dx,dyMove To (relative)Move relative to current position
L x,yLine To (absolute)Draw a straight line to a point
l dx,dyLine To (relative)Draw a line using relative coordinates
H xHorizontal Line (absolute)Draw a horizontal line to x
h dxHorizontal Line (relative)Draw a horizontal line by dx pixels
V yVertical Line (absolute)Draw a vertical line to y
v dyVertical Line (relative)Draw a vertical line by dy pixels
Q cx,cy x,yQuadratic Bezier (absolute)Draw a quadratic curve
q dcx,dcy dx,dyQuadratic Bezier (relative)Draw a quadratic curve (relative)
C cx1,cy1 cx2,cy2 x,yCubic BezierDraw a cubic Bezier curve
A rx,ry angle large-arc sweep x,yArcDraw an elliptical arc
Z or zClose PathDraw a line back to the start point

Uppercase = absolute coordinates, lowercase = relative coordinates

Drawing a Triangle with SVG Paths

<svg xmlns="http://www.w3.org/2000/svg" width="200" height="200">
    <path
        d="M 100 75
           l 50 100
           h -100
           Z"
        fill="lightblue"
        stroke="black"
        stroke-width="2"
    />
</svg>

Breakdown:

  1. M 100 75 — Move pen to (100, 75) — the top of the triangle
  2. l 50 100 — Draw line 50px right, 100px down — bottom-right corner
  3. h -100 — Draw horizontal line 100px to the left — bottom-left corner
  4. Z — Close path, drawing back to (100, 75)

Quadratic Bezier Curves

The q command draws a smooth curve through a control point:

q controlPointDX,controlPointDY endPointDX,endPointDY
<!-- Meringue highlight: a curved line from center to top -->
<path d="M 200 200 q 65 -65 0 -130" stroke="#b3702d" fill="transparent"/>

This draws a quadratic curve: starts at (200,200), uses a control point offset of (65,-65), and ends 0px right, 130px up from the start — creating a swooping curved line.


Demo: Paths with SVG

Adding meringue topping to the pie using paths. The meringue is a group (<g>) containing a base circle and six curved highlight paths rotated using CSS transforms:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Custom Pie Builder</title>
    <style>
        #options { width: 300px; display: inline-block; vertical-align: top; }

        /* Rotate each path highlight around the center of the image */
        #meringue path { transform-origin: 200px 200px; }
        #meringue path:nth-of-type(1) { transform: rotate(0deg); }
        #meringue path:nth-of-type(2) { transform: rotate(60deg); }
        #meringue path:nth-of-type(3) { transform: rotate(120deg); }
        #meringue path:nth-of-type(4) { transform: rotate(180deg); }
        #meringue path:nth-of-type(5) { transform: rotate(240deg); }
        #meringue path:nth-of-type(6) { transform: rotate(300deg); }
    </style>
</head>
<body>
    <svg xmlns="http://www.w3.org/2000/svg" width="400" height="400">
        <rect x="0" y="0" width="400" height="400" fill="transparent" stroke="black"/>
        <circle id="crust-outer" cx="200" cy="200" r="150" fill="transparent"/>
        <circle id="crust-inner" cx="200" cy="200" r="110" fill="transparent"/>
        <circle id="filling"     cx="200" cy="200" r="140" fill="transparent"/>

        <!-- Meringue topping: circle base + 6 highlight paths -->
        <g id="meringue" style="display:none;">
            <circle cx="200" cy="200" r="130" fill="white" stroke="#b3702d"/>
            <!-- Each path is the same shape; CSS rotates them into 6 positions -->
            <path d="M 200 200 q 65 -65 0 -130" stroke="#b3702d" fill="transparent"/>
            <path d="M 200 200 q 65 -65 0 -130" stroke="#b3702d" fill="transparent"/>
            <path d="M 200 200 q 65 -65 0 -130" stroke="#b3702d" fill="transparent"/>
            <path d="M 200 200 q 65 -65 0 -130" stroke="#b3702d" fill="transparent"/>
            <path d="M 200 200 q 65 -65 0 -130" stroke="#b3702d" fill="transparent"/>
            <path d="M 200 200 q 65 -65 0 -130" stroke="#b3702d" fill="transparent"/>
        </g>
    </svg>
    <div id="options">
        <fieldset>
            <legend>Please choose a crust</legend>
            <input type="radio" name="crust" value="none" checked/>None<br>
            <input type="radio" name="crust" value="pastry"/>Pastry<br>
            <input type="radio" name="crust" value="graham"/>Graham Cracker<br>
            <input type="radio" name="crust" value="chocolate"/>Chocolate<br>
        </fieldset>
        <fieldset>
            <legend>Please choose a filling</legend>
            <input type="radio" name="filling" value="none" checked/>None<br>
            <input type="radio" name="filling" value="keylime"/>Key Lime<br>
            <input type="radio" name="filling" value="pumpkin"/>Pumpkin<br>
            <input type="radio" name="filling" value="lemon"/>Lemon<br>
        </fieldset>
        <fieldset>
            <legend>Please choose a topping</legend>
            <input type="radio" name="topping" value="none" checked/>None<br>
            <input type="radio" name="topping" value="meringue"/>Meringue<br>
        </fieldset>
    </div>
    <script>
        const outerCrust = document.getElementById('crust-outer');
        const innerCrust = document.getElementById('crust-inner');
        const filling    = document.getElementById('filling');
        const meringue   = document.getElementById('meringue');

        document.getElementById('options').addEventListener('change', (e) => {
            switch (e.target.name) {
            case 'crust':
                let colors = [];
                switch (e.target.value) {
                case 'none':      colors = ['transparent', 'transparent']; break;
                case 'pastry':    colors = ['#DECCB8', '#F2DFC9']; break;
                case 'graham':    colors = ['#BA870D', '#DEA010']; break;
                case 'chocolate': colors = ['#2E1802', '#422303']; break;
                default: return;
                }
                innerCrust.setAttribute('fill', colors[0]);
                outerCrust.setAttribute('fill', colors[1]);
                break;
            case 'filling':
                switch (e.target.value) {
                case 'none':     filling.setAttribute('fill', 'transparent'); break;
                case 'keylime':  filling.setAttribute('fill', '#DFF5A2'); break;
                case 'pumpkin':  filling.setAttribute('fill', '#BD7502'); break;
                case 'lemon':    filling.setAttribute('fill', '#FFF4BD'); break;
                }
                break;
            case 'topping':
                switch (e.target.value) {
                case 'none':     meringue.style.display = 'none'; break;
                case 'meringue': meringue.style.display = ''; break;
                }
                break;
            }
        });
    </script>
</body>
</html>

Notable patterns:

  • The <g> element groups related SVG children — toggling display on the group shows/hides all children
  • CSS transform-origin and rotate distribute identical path shapes evenly around the center
  • SVG attributes like fill and stroke accept the same color values as CSS

Creating Custom Paths with Canvas

Canvas paths use a similar concept but with an imperative API — you call methods sequentially to define a path, then render it.

The Canvas Path API

ctx.beginPath();         // Start a new path (clears previous path definition)
ctx.moveTo(x, y);        // Move pen to position (no drawing)
ctx.lineTo(x, y);        // Draw straight line to absolute position
ctx.closePath();         // Draw line from current position back to start
ctx.stroke();            // Render the path as an outline
ctx.fill();              // Render the path as a filled shape

Drawing a Triangle with Canvas

ctx.beginPath();
ctx.moveTo(100, 75);     // Top of triangle
ctx.lineTo(150, 175);    // Bottom-right
ctx.lineTo(50, 175);     // Bottom-left
ctx.closePath();         // Back to top
ctx.stroke();

SVG uses relative coordinates with lowercase commands (e.g., l 50 100).
Canvas uses absolute coordinates with lineTo(x, y).

Quadratic Bezier Curves in Canvas

ctx.beginPath();
ctx.moveTo(startX, startY);
ctx.quadraticCurveTo(controlX, controlY, endX, endY);
ctx.stroke();

Using SVG Path Data in Canvas

Canvas also supports the SVG path definition syntax directly:

const path = new Path2D('M 100 75 l 50 100 h -100 Z');
ctx.fill(path);
ctx.stroke(path);

Save and Restore for Transformations

When applying transforms (like rotation) to individual elements, use save() and restore() to avoid contaminating the global drawing state:

for (let i = 0; i < 6; i++) {
    ctx.save();                                // Save current transform state
    ctx.translate(200, 200);                   // Move origin to center
    ctx.rotate(i / 6 * 2 * Math.PI);          // Rotate by i/6 of a full turn
    ctx.beginPath();
    ctx.moveTo(0, 0);
    ctx.quadraticCurveTo(65, -65, 0, -130);
    ctx.stroke();
    ctx.restore();                             // Restore original transform state
}

Demo: Paths with Canvas

The full Canvas version of the pie builder, now including the meringue topping drawn using paths and the save/restore rotation pattern:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Custom Pie Builder</title>
    <style>
        #options { width: 300px; display: inline-block; vertical-align: top; }
    </style>
</head>
<body>
    <canvas id="canvas" height="400" width="400"></canvas>
    <div id="options">
        <fieldset>
            <legend>Please choose a crust</legend>
            <input type="radio" name="crust" value="none" checked/>None<br>
            <input type="radio" name="crust" value="pastry"/>Pastry<br>
            <input type="radio" name="crust" value="graham"/>Graham Cracker<br>
            <input type="radio" name="crust" value="chocolate"/>Chocolate<br>
        </fieldset>
        <fieldset>
            <legend>Please choose a filling</legend>
            <input type="radio" name="filling" value="none" checked/>None<br>
            <input type="radio" name="filling" value="keylime"/>Key Lime<br>
            <input type="radio" name="filling" value="pumpkin"/>Pumpkin<br>
            <input type="radio" name="filling" value="lemon"/>Lemon<br>
        </fieldset>
        <fieldset>
            <legend>Please choose a topping</legend>
            <input type="radio" name="topping" value="none" checked/>None<br>
            <input type="radio" name="topping" value="meringue"/>Meringue<br>
        </fieldset>
    </div>
    <script>
        document.getElementById('options').addEventListener('change', () => {
            const options = {};
            document.querySelectorAll('[name=crust]').forEach(elem => {
                if (elem.checked) options.crust = elem.value;
            });
            document.querySelectorAll('[name=filling]').forEach(elem => {
                if (elem.checked) options.filling = elem.value;
            });
            document.querySelectorAll('[name=topping]').forEach(elem => {
                if (elem.checked) options.topping = elem.value;
            });
            render(options);
        });

        function render(options) {
            const canvas = document.getElementById('canvas');
            const ctx = canvas.getContext('2d');
            ctx.clearRect(0, 0, canvas.width, canvas.height);

            // --- Crust ---
            let outerCrustColor = '';
            let innerCrustColor = '';
            switch (options.crust) {
                case 'pastry':    outerCrustColor = '#DECCB8'; innerCrustColor = '#F2DFC9'; break;
                case 'graham':    outerCrustColor = '#BA870D'; innerCrustColor = '#DEA010'; break;
                case 'chocolate': outerCrustColor = '#2E1802'; innerCrustColor = '#422303'; break;
            }
            if (outerCrustColor && innerCrustColor) {
                ctx.fillStyle = outerCrustColor;
                ctx.beginPath();
                ctx.ellipse(200, 200, 150, 150, 0, 0, 2 * Math.PI);
                ctx.fill();

                ctx.fillStyle = innerCrustColor;
                ctx.beginPath();
                ctx.ellipse(200, 200, 110, 110, 0, 0, 2 * Math.PI);
                ctx.fill();
            }

            // --- Filling ---
            switch (options.filling) {
                case 'keylime':  ctx.fillStyle = '#DFF5A2'; break;
                case 'pumpkin':  ctx.fillStyle = '#BD7502'; break;
                case 'lemon':    ctx.fillStyle = '#FFF48D'; break;
                default:         ctx.fillStyle = 'transparent';
            }
            ctx.beginPath();
            ctx.ellipse(200, 200, 140, 140, 0, 0, 2 * Math.PI);
            ctx.fill();

            // --- Topping ---
            if (options.topping === 'meringue') {
                // Draw meringue base circle
                ctx.fillStyle = 'white';
                ctx.strokeStyle = '#B3702D';
                ctx.beginPath();
                ctx.ellipse(200, 200, 130, 130, 0, 0, 2 * Math.PI);
                ctx.fill();
                ctx.stroke();

                // Draw 6 highlight curves rotated around the center
                for (let i = 0; i < 6; i++) {
                    ctx.save();
                    ctx.translate(200, 200);
                    ctx.rotate(i / 6 * 2 * Math.PI);
                    ctx.beginPath();
                    ctx.moveTo(0, 0);
                    ctx.quadraticCurveTo(65, -65, 0, -130);
                    ctx.stroke();
                    ctx.restore();
                }
            }
        }
    </script>
</body>
</html>

Module 5: Adding Text to Custom Graphics

Adding text to graphics is a relatively straightforward topic, but the approaches differ significantly between SVG and Canvas.

Demo: Adding Text to SVG Images

In SVG, text is simply another element with attributes controlling positioning and appearance. The <text> element supports nested <tspan> elements for applying different styles to sub-sections of text.

<svg xmlns="http://www.w3.org/2000/svg" width="400" height="400">
    <!-- ... other elements ... -->

    <!--
        x="200"              Horizontal position (center of 400px wide image)
        y="30"               Vertical position (near top)
        text-anchor="middle" Center-align the text at the x coordinate
        font-family          Font to use
        fill                 Text color
        font-size            Font size in pixels
    -->
    <text
        x="200"
        y="30"
        text-anchor="middle"
        font-family="sans-serif"
        fill="#a593c2"
        font-size="25"
    >
        <!-- tspan applies different styles to part of the text -->
        <tspan fill="#4dcfa8" font-weight="bold">Bethany's</tspan>
        Custom Pie Maker
    </text>
</svg>

text-anchor values:

ValueEffect
startLeft-align at the x position (default)
middleCenter the text at the x position
endRight-align at the x position

Complete SVG demo with text:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Custom Pie Builder</title>
    <style>
        #options { width: 300px; display: inline-block; vertical-align: top; }
        #meringue path { transform-origin: 200px 200px; }
        #meringue path:nth-of-type(1) { transform: rotate(0deg); }
        #meringue path:nth-of-type(2) { transform: rotate(60deg); }
        #meringue path:nth-of-type(3) { transform: rotate(120deg); }
        #meringue path:nth-of-type(4) { transform: rotate(180deg); }
        #meringue path:nth-of-type(5) { transform: rotate(240deg); }
        #meringue path:nth-of-type(6) { transform: rotate(300deg); }
    </style>
</head>
<body>
    <svg xmlns="http://www.w3.org/2000/svg" width="400" height="400">
        <rect x="0" y="0" width="400" height="400" fill="transparent" stroke="black"/>
        <circle id="crust-outer" cx="200" cy="200" r="150" fill="transparent"/>
        <circle id="crust-inner" cx="200" cy="200" r="110" fill="transparent"/>
        <circle id="filling"     cx="200" cy="200" r="140" fill="transparent"/>
        <g id="meringue" style="display:none;">
            <circle cx="200" cy="200" r="130" fill="white" stroke="#b3702d"/>
            <path d="M 200 200 q 65 -65 0 -130" stroke="#b3702d" fill="transparent"/>
            <path d="M 200 200 q 65 -65 0 -130" stroke="#b3702d" fill="transparent"/>
            <path d="M 200 200 q 65 -65 0 -130" stroke="#b3702d" fill="transparent"/>
            <path d="M 200 200 q 65 -65 0 -130" stroke="#b3702d" fill="transparent"/>
            <path d="M 200 200 q 65 -65 0 -130" stroke="#b3702d" fill="transparent"/>
            <path d="M 200 200 q 65 -65 0 -130" stroke="#b3702d" fill="transparent"/>
        </g>
        <!-- Branding text with two-color tspan approach -->
        <text x="200" y="30" text-anchor="middle" font-family="sans-serif" fill="#a593c2" font-size="25">
            <tspan fill="#4dcfa8" font-weight="bold">Bethany's</tspan>
            Custom Pie Maker
        </text>
    </svg>
    <!-- options and script unchanged from Module 4 demo -->
</body>
</html>

Demo: Adding Text to Canvas Images

Canvas lacks a built-in centering facility. Since there is no text-anchor="middle", you must manually calculate the total width of all text spans, then compute the starting X position.

// Define text spans with their individual styles
const spans = [
    {
        font:      'bold 25px sans-serif',
        text:      "Bethany's",
        fillStyle: '#A593C2'
    },
    {
        font:      '25px sans-serif',
        text:      ' Custom Pie Maker',
        fillStyle: '#4DCFA8'
    }
];

// Step 1: Measure each span to calculate total width
let totalWidth = 0;
spans.forEach(span => {
    ctx.font = span.font;                    // Must set font before measuring
    span.dims = ctx.measureText(span.text);  // Returns a TextMetrics object
    totalWidth += span.dims.width;
});

// Step 2: Calculate starting X so the whole string is centered at x=200
let currentX = 200 - totalWidth / 2;

// Step 3: Draw each span at its calculated position
spans.forEach(span => {
    ctx.fillStyle = span.fillStyle;
    ctx.font      = span.font;
    ctx.fillText(span.text, currentX, 30);  // fillText(text, x, y)
    currentX += span.dims.width;             // Advance x for next span
});

Canvas text API summary:

Method / PropertyDescription
ctx.fontCSS font string: 'bold 25px sans-serif'
ctx.fillStyleText fill color
ctx.strokeStyleText outline color
ctx.fillText(text, x, y)Draw filled text at position
ctx.strokeText(text, x, y)Draw outlined text at position
ctx.measureText(text)Returns TextMetrics with .width property
ctx.textAlign'left', 'center', 'right', 'start', 'end'
ctx.textBaseline'top', 'middle', 'alphabetic', 'bottom'

Complete Canvas demo with text:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Custom Pie Builder</title>
    <style>
        #options { width: 300px; display: inline-block; vertical-align: top; }
    </style>
</head>
<body>
    <canvas id="canvas" height="400" width="400"></canvas>
    <!-- options fieldsets (crust, filling, topping) as before -->
    <script>
        /* event listener and options collection unchanged */

        function render(options) {
            const canvas = document.getElementById('canvas');
            const ctx = canvas.getContext('2d');
            ctx.clearRect(0, 0, canvas.width, canvas.height);

            // ... crust, filling, topping drawing (same as Module 4) ...

            // Multi-span centered branding text
            const spans = [
                { font: 'bold 25px sans-serif', text: "Bethany's",        fillStyle: '#A593C2' },
                { font: '25px sans-serif',       text: ' Custom Pie Maker', fillStyle: '#4DCFA8' }
            ];

            let totalWidth = 0;
            spans.forEach(span => {
                ctx.font = span.font;
                span.dims = ctx.measureText(span.text);
                totalWidth += span.dims.width;
            });

            let currentX = 200 - totalWidth / 2;
            spans.forEach(span => {
                ctx.fillStyle = span.fillStyle;
                ctx.font = span.font;
                ctx.fillText(span.text, currentX, 30);
                currentX += span.dims.width;
            });
        }
    </script>
</body>
</html>

Module 6: Importing Images into Custom Graphics

At this point the course demo is looking good. Adding an external logo image is the last capability needed to round it out.

Demo: Importing Images with SVG

SVG uses its own <image> element (not the HTML <img> tag). To reference external images, a second XML namespace — xlink — must be imported alongside the default SVG namespace.

<svg
    xmlns="http://www.w3.org/2000/svg"
    xmlns:xlink="http://www.w3.org/1999/xlink"
    width="400"
    height="400"
>
    <!-- ... other SVG elements ... -->

    <!--
        x="320", y="320"         Positions the image in the lower-right corner
        width="50", height="50"  Keeps the logo small (watermark-style)
        xlink:href               References the image file via the xlink namespace
    -->
    <image x="320" y="320" width="50" height="50" xlink:href="./logo.png"/>
</svg>

Why xmlns:xlink?

Complete SVG demo with image:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Custom Pie Builder</title>
    <style>
        #options { width: 300px; display: inline-block; vertical-align: top; }
        #meringue path { transform-origin: 200px 200px; }
        #meringue path:nth-of-type(1) { transform: rotate(0deg); }
        #meringue path:nth-of-type(2) { transform: rotate(60deg); }
        #meringue path:nth-of-type(3) { transform: rotate(120deg); }
        #meringue path:nth-of-type(4) { transform: rotate(180deg); }
        #meringue path:nth-of-type(5) { transform: rotate(240deg); }
        #meringue path:nth-of-type(6) { transform: rotate(300deg); }
    </style>
</head>
<body>
    <svg xmlns="http://www.w3.org/2000/svg"
         xmlns:xlink="http://www.w3.org/1999/xlink"
         width="400" height="400">
        <rect x="0" y="0" width="400" height="400" fill="transparent" stroke="black"/>
        <circle id="crust-outer" cx="200" cy="200" r="150" fill="transparent"/>
        <circle id="crust-inner" cx="200" cy="200" r="110" fill="transparent"/>
        <circle id="filling"     cx="200" cy="200" r="140" fill="transparent"/>
        <g id="meringue" style="display:none;">
            <circle cx="200" cy="200" r="130" fill="white" stroke="#b3702d"/>
            <path d="M 200 200 q 65 -65 0 -130" stroke="#b3702d" fill="transparent"/>
            <path d="M 200 200 q 65 -65 0 -130" stroke="#b3702d" fill="transparent"/>
            <path d="M 200 200 q 65 -65 0 -130" stroke="#b3702d" fill="transparent"/>
            <path d="M 200 200 q 65 -65 0 -130" stroke="#b3702d" fill="transparent"/>
            <path d="M 200 200 q 65 -65 0 -130" stroke="#b3702d" fill="transparent"/>
            <path d="M 200 200 q 65 -65 0 -130" stroke="#b3702d" fill="transparent"/>
        </g>
        <text x="200" y="30" text-anchor="middle" font-family="sans-serif" fill="#a593c2" font-size="25">
            <tspan fill="#4dcfa8" font-weight="bold">Bethany's</tspan>
            Custom Pie Maker
        </text>
        <!-- External logo image in lower-right corner -->
        <image x="320" y="320" width="50" height="50" xlink:href="./logo.png"/>
    </svg>
    <!-- options and script unchanged from previous module -->
</body>
</html>

Demo: Importing Images with Canvas

In Canvas, images are loaded as HTML Image objects in JavaScript. Because loading is asynchronous, the image must be drawn inside the load event handler — never before.

// Create an Image object in JavaScript
const image = new Image();

// Register the load event BEFORE setting src
// Drawing must happen after the image data is available
image.addEventListener('load', () => {
    ctx.drawImage(
        image,   // The image source
        320,     // Destination x
        320,     // Destination y
        50,      // Destination width
        50       // Destination height
    );
});

// Setting src triggers the browser to load the image
// The 'load' event fires once the data is ready
image.src = './logo.png';

drawImage overloads:

// 3-argument form: destination position only (natural size)
ctx.drawImage(image, dx, dy);

// 5-argument form: destination position + size (resize)
ctx.drawImage(image, dx, dy, dWidth, dHeight);

// 9-argument form: source crop + destination position + size
ctx.drawImage(image, sx, sy, sWidth, sHeight, dx, dy, dWidth, dHeight);

Complete Canvas demo with image:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Custom Pie Builder</title>
    <style>
        #options { width: 300px; display: inline-block; vertical-align: top; }
    </style>
</head>
<body>
    <canvas id="canvas" height="400" width="400"></canvas>
    <!-- options fieldsets unchanged -->
    <script>
        /* event listener unchanged */

        function render(options) {
            const canvas = document.getElementById('canvas');
            const ctx = canvas.getContext('2d');
            ctx.clearRect(0, 0, canvas.width, canvas.height);

            // ... crust, filling, topping, text drawing (same as Module 5) ...

            // Load and draw logo image
            const image = new Image();
            image.addEventListener('load', () => {
                ctx.drawImage(image, 320, 320, 50, 50);
            });
            image.src = './logo.png';
        }
    </script>
</body>
</html>

Module 7: Exploring the Unique Strengths of Canvas and SVG Graphics

In this final module, we step back and look at some of the unique strengths of each technology. Rather than keeping SVG and Canvas in lockstep, each demo showcases what the technology does particularly well.

  • SVG → CSS-driven animation
  • Canvas → Interactive mouse-driven sketching

These demonstrations can help inform which technology to choose for a given use case.

Demo: Animating SVG Images

SVG elements are part of the DOM, which means CSS transitions and animations work on them natively — with no JavaScript required for the animation itself.

The pattern:

  1. Add a CSS class (.animated) with a transition rule
  2. Apply that class to the SVG elements you want to animate
  3. Use JavaScript to change properties (transforms, colors) — CSS handles the smooth animation automatically
.animated {
    transition: all 1s;  /* Animate all CSS property changes over 1 second */
}
<circle id="crust-outer" cx="200" cy="200" r="150"
    fill="transparent" class="animated"/>
<circle id="crust-inner" cx="200" cy="200" r="110"
    fill="transparent" class="animated"/>
<circle id="filling"     cx="200" cy="200" r="140"
    fill="transparent" class="animated"/>
<g id="meringue" class="animated">
    <!-- ... -->
</g>
const animateButton = document.getElementById('animate');
let isExploded = false;

animateButton.addEventListener('click', () => {
    if (isExploded) {
        // Reassemble: return everything to original position
        animateButton.innerHTML = 'Explode!';
        outerCrust.style.transform = 'translate(0, 0) scale(1)';
        innerCrust.style.transform = 'translate(0, 0) scale(1)';
        filling.style.transform    = 'translate(0, 0) scale(1)';
        meringue.style.transform   = 'translate(0, 0) scale(1)';
    } else {
        // Explode: spread layers apart so each is visible
        animateButton.innerHTML = 'Reassemble';
        outerCrust.style.transform = 'translate(-20px, 20px) scale(0.6)';
        innerCrust.style.transform = 'translate(-20px, 20px) scale(0.6)';
        filling.style.transform    = 'translate(180px, 20px) scale(0.6)';
        meringue.style.transform   = 'translate(80px, 180px) scale(0.6)';
    }
    isExploded = !isExploded;
});

Complete SVG animation demo:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Custom Pie Builder</title>
    <style>
        #options { width: 300px; display: inline-block; vertical-align: top; }
        #meringue path { transform-origin: 200px 200px; }
        #meringue path:nth-of-type(1) { transform: rotate(0deg); }
        #meringue path:nth-of-type(2) { transform: rotate(60deg); }
        #meringue path:nth-of-type(3) { transform: rotate(120deg); }
        #meringue path:nth-of-type(4) { transform: rotate(180deg); }
        #meringue path:nth-of-type(5) { transform: rotate(240deg); }
        #meringue path:nth-of-type(6) { transform: rotate(300deg); }

        /* The key: a single CSS rule enables smooth animated transitions */
        .animated {
            transition: all 1s;
        }
    </style>
</head>
<body>
    <svg xmlns="http://www.w3.org/2000/svg"
         xmlns:xlink="http://www.w3.org/1999/xlink"
         width="400" height="400">
        <rect x="0" y="0" width="400" height="400" fill="transparent" stroke="black"/>
        <circle id="crust-outer" cx="200" cy="200" r="150" fill="transparent" class="animated"/>
        <circle id="crust-inner" cx="200" cy="200" r="110" fill="transparent" class="animated"/>
        <circle id="filling"     cx="200" cy="200" r="140" fill="transparent" class="animated"/>
        <g id="meringue" style="display:none;" class="animated">
            <circle cx="200" cy="200" r="130" fill="white" stroke="#b3702d"/>
            <path d="M 200 200 q 65 -65 0 -130" stroke="#b3702d" fill="transparent"/>
            <path d="M 200 200 q 65 -65 0 -130" stroke="#b3702d" fill="transparent"/>
            <path d="M 200 200 q 65 -65 0 -130" stroke="#b3702d" fill="transparent"/>
            <path d="M 200 200 q 65 -65 0 -130" stroke="#b3702d" fill="transparent"/>
            <path d="M 200 200 q 65 -65 0 -130" stroke="#b3702d" fill="transparent"/>
            <path d="M 200 200 q 65 -65 0 -130" stroke="#b3702d" fill="transparent"/>
        </g>
        <text x="200" y="30" text-anchor="middle" font-family="sans-serif" fill="#a593c2" font-size="25">
            <tspan fill="#4dcfa8" font-weight="bold">Bethany's</tspan>
            Custom Pie Maker
        </text>
        <image x="320" y="320" width="50" height="50" xlink:href="./logo.png"/>
    </svg>
    <div id="options">
        <fieldset>
            <legend>Please choose a crust</legend>
            <input type="radio" name="crust" value="none" checked/>None<br>
            <input type="radio" name="crust" value="pastry"/>Pastry<br>
            <input type="radio" name="crust" value="graham"/>Graham Cracker<br>
            <input type="radio" name="crust" value="chocolate"/>Chocolate<br>
        </fieldset>
        <fieldset>
            <legend>Please choose a filling</legend>
            <input type="radio" name="filling" value="none" checked/>None<br>
            <input type="radio" name="filling" value="keylime"/>Key Lime<br>
            <input type="radio" name="filling" value="pumpkin"/>Pumpkin<br>
            <input type="radio" name="filling" value="lemon"/>Lemon<br>
        </fieldset>
        <fieldset>
            <legend>Please choose a topping</legend>
            <input type="radio" name="topping" value="none" checked/>None<br>
            <input type="radio" name="topping" value="meringue"/>Meringue<br>
        </fieldset>
        <button id="animate">Explode!</button>
    </div>
    <script>
        const outerCrust   = document.getElementById('crust-outer');
        const innerCrust   = document.getElementById('crust-inner');
        const filling      = document.getElementById('filling');
        const meringue     = document.getElementById('meringue');
        const animateButton = document.getElementById('animate');
        let isExploded = false;

        document.getElementById('options').addEventListener('change', (e) => {
            switch (e.target.name) {
            case 'crust':
                let colors = [];
                switch (e.target.value) {
                case 'none':      colors = ['transparent', 'transparent']; break;
                case 'pastry':    colors = ['#DECCB8', '#F2DFC9']; break;
                case 'graham':    colors = ['#BA870D', '#DEA010']; break;
                case 'chocolate': colors = ['#2E1802', '#422303']; break;
                default: return;
                }
                innerCrust.setAttribute('fill', colors[0]);
                outerCrust.setAttribute('fill', colors[1]);
                break;
            case 'filling':
                switch (e.target.value) {
                case 'none':    filling.setAttribute('fill', 'transparent'); break;
                case 'keylime': filling.setAttribute('fill', '#DFF5A2'); break;
                case 'pumpkin': filling.setAttribute('fill', '#BD7502'); break;
                case 'lemon':   filling.setAttribute('fill', '#FFF4BD'); break;
                }
                break;
            case 'topping':
                switch (e.target.value) {
                case 'none':     meringue.style.display = 'none'; break;
                case 'meringue': meringue.style.display = ''; break;
                }
                break;
            }
        });

        animateButton.addEventListener('click', () => {
            if (isExploded) {
                animateButton.innerHTML = 'Explode!';
                outerCrust.style.transform = 'translate(0, 0) scale(1)';
                innerCrust.style.transform = 'translate(0, 0) scale(1)';
                filling.style.transform    = 'translate(0, 0) scale(1)';
                meringue.style.transform   = 'translate(0, 0) scale(1)';
            } else {
                animateButton.innerHTML = 'Reassemble';
                outerCrust.style.transform = 'translate(-20px, 20px) scale(0.6)';
                innerCrust.style.transform = 'translate(-20px, 20px) scale(0.6)';
                filling.style.transform    = 'translate(180px, 20px) scale(0.6)';
                meringue.style.transform   = 'translate(80px, 180px) scale(0.6)';
            }
            isExploded = !isExploded;
        });
    </script>
</body>
</html>

Demo: Creating a Sketching Tool with Canvas

Canvas excels at mouse-driven, real-time drawing because it responds directly to browser events. There is no continuous drawing mode — instead, the mousemove event is sampled at high frequency, and each movement draws a tiny line segment from the previous position to the current one.

The technique: Connect each mousemove sample to the previous one using a moveTo / lineTo pair. This creates smooth, continuous-looking strokes from discrete samples.

let isDrawing = false;
let prevX;
let prevY;

// Start drawing when mouse button is pressed
canvas.addEventListener('mousedown', () => {
    isDrawing = true;
});

// Stop drawing when mouse button is released
canvas.addEventListener('mouseup', () => {
    isDrawing = false;
});

// Draw as the mouse moves
canvas.addEventListener('mousemove', ({x, y}) => {
    if (!isDrawing) {
        // Reset tracking when not drawing
        prevX = null;
        prevY = null;
        return;
    }
    if (prevX && prevY) {
        ctx.beginPath();
        ctx.moveTo(prevX, prevY);  // Start from last position
        ctx.lineTo(x, y);          // Draw to current position
        ctx.stroke();
    }
    // Update previous position for the next event
    prevX = x;
    prevY = y;
});

Why not just plot individual points? Mouse events fire at limited frequency. At fast speeds, there would be visible gaps between dots. Drawing a line from the last recorded position to the current one fills those gaps automatically.

Complete Canvas sketching tool:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Canvas Sketching Tool</title>
</head>
<body>
    <canvas id="canvas" height="600" width="600"></canvas>
    <script>
        const canvas = document.getElementById('canvas');
        const ctx = canvas.getContext('2d');

        // Draw a border to show the bounds of the canvas
        ctx.beginPath();
        ctx.rect(0, 0, 600, 600);
        ctx.stroke();

        let isDrawing = false;
        let prevX;
        let prevY;

        canvas.addEventListener('mousedown', () => {
            isDrawing = true;
        });

        canvas.addEventListener('mouseup', () => {
            isDrawing = false;
        });

        canvas.addEventListener('mousemove', ({x, y}) => {
            if (!isDrawing) {
                prevX = null;
                prevY = null;
                return;
            }
            if (prevX && prevY) {
                ctx.beginPath();
                ctx.moveTo(prevX, prevY);
                ctx.lineTo(x, y);
                ctx.stroke();
            }
            prevX = x;
            prevY = y;
        });
    </script>
</body>
</html>

Course Summary

We covered a significant amount of ground across this course:

  1. Getting Started — Learned the basics of what SVG and Canvas documents are and how they each render graphics. Understood the fundamental philosophical difference: SVG is declarative (describe what you want), Canvas is imperative (describe how to do it step by step).

  2. Basic Shapes — Drew squares, rectangles, circles, arcs, and lines onto custom graphics. Explored the key attributes and API methods for each technology.

  3. Paths — Learned the path definition language and how it gives extreme flexibility to draw just about any shape. The SVG d attribute mini-language and the Canvas beginPath / moveTo / lineTo / quadraticCurveTo API.

  4. Text — Added styled, positioned text to graphics. Compared SVG’s declarative text-anchor="middle" against Canvas’s manual measurement-and-calculation approach.

  5. Images — Embedded external pre-rendered images using SVG’s <image> element with the xlink namespace, and Canvas’s asynchronous new Image() / drawImage() pattern.

  6. Unique Strengths — Explored CSS-driven animation with SVG (effortless with just a transition class) and real-time interactive sketching with Canvas (direct event handling on a bitmap surface).


Reference: SVG vs Canvas Comparison

flowchart TB
    subgraph SVG_Strengths["SVG Strengths"]
        S1[Resolution Independent — scales to any size]
        S2[DOM Integration — CSS, events, dev tools]
        S3[Declarative — clean, readable markup]
        S4[CSS Animations — transition / keyframes]
        S5[Accessibility — screenreaders can see elements]
        S6[Easy to modify — setAttribute to change visuals]
    end

    subgraph Canvas_Strengths["Canvas Strengths"]
        C1[Pixel-level control]
        C2[High performance for many objects]
        C3[Real-time interaction — mouse events, sketching]
        C4[Image manipulation — pixel data via getImageData]
        C5[Games — requestAnimationFrame game loop]
        C6[Bitmap output — toDataURL / toBlob export]
    end
FeatureSVGCanvas
Rendering modelVector (resolution-independent)Raster (bitmap pixels)
API styleDeclarative (XML elements)Imperative (JavaScript calls)
DOM integrationFull — elements are DOM nodesNone — canvas is a single opaque element
CSS stylingYes — style attributes, classes, transitionsNo — properties set via JavaScript API
AnimationCSS transitions/keyframes work nativelyMust redraw manually each frame
Event handlingPer-element events (click, hover, etc.)Canvas-level only — must detect hit zones manually
PerformanceDegrades with many elementsHandles large numbers of drawn objects well
Image export<svg> can be serialized to stringcanvas.toDataURL() / canvas.toBlob()
AccessibilityElements visible to screen readersInherently inaccessible (pixels only)
Best forIcons, diagrams, interactive charts, illustrationsGames, real-time interaction, image editing, data visualization

Reference: SVG Path Command Cheat Sheet

flowchart LR
    Start([Pen starts here]) --> M["M x,y — Move To\n(no drawing)"]
    M --> L["L x,y — Line To\n(absolute)"]
    L --> l["l dx,dy — Line To\n(relative)"]
    l --> H["H x — Horizontal Line\n(absolute)"]
    H --> V["V y — Vertical Line\n(absolute)"]
    V --> Q["Q cx,cy x,y\nQuadratic Bezier"]
    Q --> C["C cx1,cy1 cx2,cy2 x,y\nCubic Bezier"]
    C --> A["A rx,ry angle large-arc sweep x,y\nArc"]
    A --> Z["Z — Close Path\n(back to start)"]

Path Command Quick Reference

Path Command Summary:
  M / m   Move To            (absolute / relative)
  L / l   Line To            (absolute / relative)
  H / h   Horizontal Line    (absolute / relative)
  V / v   Vertical Line      (absolute / relative)
  Q / q   Quadratic Bezier   (absolute / relative)
  C / c   Cubic Bezier       (absolute / relative)
  A / a   Elliptical Arc     (absolute / relative)
  Z       Close Path

  UPPERCASE = absolute coordinates
  lowercase = relative coordinates (offset from current position)

Canvas Path API Quick Reference

ctx.beginPath()                           // Start new path
ctx.moveTo(x, y)                          // Move without drawing
ctx.lineTo(x, y)                          // Straight line (absolute)
ctx.quadraticCurveTo(cpx, cpy, x, y)     // Quadratic Bezier
ctx.bezierCurveTo(cp1x,cp1y,cp2x,cp2y,x,y) // Cubic Bezier
ctx.arc(x, y, radius, startAngle, endAngle, anticlockwise)
ctx.ellipse(x, y, rx, ry, rotation, startAngle, endAngle)
ctx.rect(x, y, width, height)
ctx.closePath()                           // Connect back to start
ctx.stroke()                              // Render outline
ctx.fill()                                // Render filled shape

// Save/restore state for transforms
ctx.save()
ctx.translate(x, y)
ctx.rotate(radians)
ctx.scale(sx, sy)
ctx.restore()

SVG Element Quick Reference

<!-- Shapes -->
<rect x y width height fill stroke stroke-width rx ry/>
<circle cx cy r fill stroke stroke-width/>
<ellipse cx cy rx ry fill stroke/>
<line x1 y1 x2 y2 stroke stroke-width/>
<polygon points="x1,y1 x2,y2 x3,y3" fill stroke/>
<polyline points="..." fill stroke/>

<!-- Paths -->
<path d="M... L... Q... Z" fill stroke/>

<!-- Text -->
<text x y text-anchor font-family font-size fill>
    Content here
    <tspan fill font-weight>Styled span</tspan>
</text>

<!-- Images -->
<image x y width height xlink:href="./file.png"/>

<!-- Grouping -->
<g id="group" class="animated" style="display:none;">
    <!-- child elements -->
</g>

<!-- Document root -->
<svg xmlns="http://www.w3.org/2000/svg"
     xmlns:xlink="http://www.w3.org/1999/xlink"
     width height>
</svg>

Search Terms

graphics · canvas · svg · html · css · web · fundamentals · frontend · development · drawing · images · path · paths · shapes · custom · element · reference · importing · text · api · bezier · command · comparison · curves

Interested in this course?

Contact us to book it or get a custom training plan for your team.