Table of Contents
- Module 1: Course Overview
- Module 2: Getting Started with Canvas and SVG Graphics
- Module 3: Drawing Basic Shapes
- Module 4: Creating Arbitrary Shapes with Paths
- Module 5: Adding Text to Custom Graphics
- Module 6: Importing Images into Custom Graphics
- Module 7: Exploring the Unique Strengths of Canvas and SVG Graphics
- Reference: SVG vs Canvas Comparison
- Reference: SVG Path Command Cheat Sheet
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:
| Part | Purpose |
|---|---|
<svg> tag | Wraps the entire SVG image; identifies the section of the document the browser will treat as an image |
xmlns attribute | The 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 / width | Sets 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
idattribute 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:
- Basic Shapes — rectangles, circles, arcs, lines
- Paths — arbitrary shapes using path commands
- Text — adding and styling text in graphics
- Images — embedding pre-rendered images
- 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>
| Attribute | Description |
|---|---|
x, y | Position of the upper-left corner |
width, height | Dimensions of the rectangle |
stroke | Outline/border color (accepts named colors or RGB) |
stroke-width | Width of the outline in pixels |
fill | Fill/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>
| Attribute | Description |
|---|---|
cx, cy | Center point coordinates |
r | Radius |
fill | Fill color |
stroke | Border 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
getElementByIdjust 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
renderfunction 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 therenderfunction 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:
| Command | Type | Description |
|---|---|---|
M x,y | Move To (absolute) | Move the pen to a point without drawing |
m dx,dy | Move To (relative) | Move relative to current position |
L x,y | Line To (absolute) | Draw a straight line to a point |
l dx,dy | Line To (relative) | Draw a line using relative coordinates |
H x | Horizontal Line (absolute) | Draw a horizontal line to x |
h dx | Horizontal Line (relative) | Draw a horizontal line by dx pixels |
V y | Vertical Line (absolute) | Draw a vertical line to y |
v dy | Vertical Line (relative) | Draw a vertical line by dy pixels |
Q cx,cy x,y | Quadratic Bezier (absolute) | Draw a quadratic curve |
q dcx,dcy dx,dy | Quadratic Bezier (relative) | Draw a quadratic curve (relative) |
C cx1,cy1 cx2,cy2 x,y | Cubic Bezier | Draw a cubic Bezier curve |
A rx,ry angle large-arc sweep x,y | Arc | Draw an elliptical arc |
Z or z | Close Path | Draw 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:
M 100 75— Move pen to (100, 75) — the top of the trianglel 50 100— Draw line 50px right, 100px down — bottom-right cornerh -100— Draw horizontal line 100px to the left — bottom-left cornerZ— 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 — togglingdisplayon the group shows/hides all children - CSS
transform-originandrotatedistribute identical path shapes evenly around the center - SVG attributes like
fillandstrokeaccept 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 withlineTo(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:
| Value | Effect |
|---|---|
start | Left-align at the x position (default) |
middle | Center the text at the x position |
end | Right-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 / Property | Description |
|---|---|
ctx.font | CSS font string: 'bold 25px sans-serif' |
ctx.fillStyle | Text fill color |
ctx.strokeStyle | Text 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:
- Add a CSS class (
.animated) with atransitionrule - Apply that class to the SVG elements you want to animate
- 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:
-
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).
-
Basic Shapes — Drew squares, rectangles, circles, arcs, and lines onto custom graphics. Explored the key attributes and API methods for each technology.
-
Paths — Learned the path definition language and how it gives extreme flexibility to draw just about any shape. The SVG
dattribute mini-language and the CanvasbeginPath/moveTo/lineTo/quadraticCurveToAPI. -
Text — Added styled, positioned text to graphics. Compared SVG’s declarative
text-anchor="middle"against Canvas’s manual measurement-and-calculation approach. -
Images — Embedded external pre-rendered images using SVG’s
<image>element with thexlinknamespace, and Canvas’s asynchronousnew Image()/drawImage()pattern. -
Unique Strengths — Explored CSS-driven animation with SVG (effortless with just a
transitionclass) 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
| Feature | SVG | Canvas |
|---|---|---|
| Rendering model | Vector (resolution-independent) | Raster (bitmap pixels) |
| API style | Declarative (XML elements) | Imperative (JavaScript calls) |
| DOM integration | Full — elements are DOM nodes | None — canvas is a single opaque element |
| CSS styling | Yes — style attributes, classes, transitions | No — properties set via JavaScript API |
| Animation | CSS transitions/keyframes work natively | Must redraw manually each frame |
| Event handling | Per-element events (click, hover, etc.) | Canvas-level only — must detect hit zones manually |
| Performance | Degrades with many elements | Handles large numbers of drawn objects well |
| Image export | <svg> can be serialized to string | canvas.toDataURL() / canvas.toBlob() |
| Accessibility | Elements visible to screen readers | Inherently inaccessible (pixels only) |
| Best for | Icons, diagrams, interactive charts, illustrations | Games, 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