Session 12: Canvas — drawing in JavaScript
Phase 7 — Web · Session 12 of 17
What we’re learning today
You can draw in Pygame. Today you draw in the
browser — using the <canvas> element and JavaScript.
The drawing API looks very similar to Pygame’s: same
shapes, same coordinate system (top-left = 0,0, y goes
down), same logic. The mental model from Phase 6
transfers directly. By the end of class you’ll have
a small scene drawn in the browser. Next session: a
mini-game.
You’ll need to remember from last time
- JavaScript syntax (Session 8).
document.querySelector(Session 9).- Phase 6 Sessions 1-2 — Pygame’s
pygame.draw.rect,circle,line, the coordinate system. All of that mental model applies today.
Part A: The canvas element
What’s a canvas?
The <canvas> element is a rectangular drawing
surface in HTML. It looks like nothing on its own —
just a blank rectangle. You use JavaScript to draw
on it.
<canvas id="myCanvas" width="600" height="400"></canvas>
The width and height set the canvas’s pixel
dimensions. (Always set them in HTML, not CSS — CSS
just scales the canvas, which can blur it.)
Get the drawing context
To draw, you need the 2D drawing context:
const canvas = document.querySelector("#myCanvas");
const ctx = canvas.getContext("2d");
ctx (short for “context”) is your drawing handle.
Almost every canvas method is called on ctx.
(There’s also getContext("webgl") for 3D — way out
of scope.)
Try it — your first canvas
Open Thonny. Save a new file as canvas-test.html:
<!DOCTYPE html>
<html>
<head>
<title>Canvas test</title>
<style>
canvas {
border: 1px solid #999;
display: block;
margin: 20px auto;
}
</style>
</head>
<body>
<canvas id="myCanvas" width="600" height="400"></canvas>
<script>
const canvas = document.querySelector("#myCanvas");
const ctx = canvas.getContext("2d");
// Background
ctx.fillStyle = "#87ceeb";
ctx.fillRect(0, 0, 600, 400);
// A red box
ctx.fillStyle = "red";
ctx.fillRect(50, 50, 100, 100);
</script>
</body>
</html>
Save. Open in browser. A canvas with a sky-blue background and a red square in the corner.
What’s happening:
ctx.fillStyle = "..."sets the color for any shape we draw next.ctx.fillRect(x, y, width, height)draws a filled rectangle.
Compare to Pygame:
# Pygame
screen.fill((135, 206, 235))
pygame.draw.rect(screen, (255, 0, 0), (50, 50, 100, 100))
// Canvas
ctx.fillStyle = "#87ceeb";
ctx.fillRect(0, 0, 600, 400);
ctx.fillStyle = "red";
ctx.fillRect(50, 50, 100, 100);
Almost identical. Different language, same model.
The coordinate system
Same as Pygame. Same as Phase 7’s HTML positions.
(0, 0)is the top-left corner.xincreases to the right.yincreases downward.
If you remember Phase 6, this is automatic.
Filled vs outlined shapes
// Filled — interior color
ctx.fillStyle = "blue";
ctx.fillRect(200, 50, 80, 80);
// Outlined — border color
ctx.strokeStyle = "blue";
ctx.lineWidth = 4;
ctx.strokeRect(300, 50, 80, 80);
fillRect and strokeRect (filled and outlined,
respectively).
fillStyle and strokeStyle (the two color
properties).
lineWidth (border thickness).
Circles (with arc)
Canvas doesn’t have drawCircle. It has arc,
which draws part of a circle (or all of it, if you
go full 360°).
The pattern for a full circle:
ctx.beginPath();
ctx.arc(300, 200, 50, 0, Math.PI * 2); // x, y, radius, startAngle, endAngle
ctx.fillStyle = "yellow";
ctx.fill();
arc(x, y, radius, startAngle, endAngle) draws an
arc:
(x, y)— the center.radius— size.startAngle— where to start (in radians).endAngle— where to end (in radians).
Math.PI * 2 is a full 360° circle.
The beginPath() / fill() pair is required for
arcs. Frame:
“Arcs are drawn as a path. Tell canvas you’re starting a new path (
beginPath), describe the shape (arc(...)), then eitherfill()orstroke()to actually draw it.”
Lines work the same way (we’ll see in a moment).
A complete scene
Replace your script with:
const canvas = document.querySelector("#myCanvas");
const ctx = canvas.getContext("2d");
// Sky background
ctx.fillStyle = "#87ceeb";
ctx.fillRect(0, 0, 600, 400);
// Ground
ctx.fillStyle = "#3a7d3a";
ctx.fillRect(0, 300, 600, 100);
// Sun
ctx.fillStyle = "#ffeb3b";
ctx.beginPath();
ctx.arc(500, 80, 40, 0, Math.PI * 2);
ctx.fill();
// House body
ctx.fillStyle = "#c69876";
ctx.fillRect(220, 200, 160, 100);
// Roof — using lineTo
ctx.fillStyle = "#8b3030";
ctx.beginPath();
ctx.moveTo(210, 200);
ctx.lineTo(390, 200);
ctx.lineTo(300, 130);
ctx.closePath();
ctx.fill();
// Door
ctx.fillStyle = "#5a3a1a";
ctx.fillRect(280, 250, 40, 50);
Save. Reload. A scene — sky, ground, sun, house, door.
What’s new:
moveTo(x, y)— move the “pen” to that point without drawing.lineTo(x, y)— draw a line from the pen’s position to that point.closePath()— close the path back to where it started.
This is how you draw triangles, polygons, and any
custom shape. Phase 6 had pygame.draw.polygon —
this is the canvas equivalent.
Text
ctx.font = "30px Arial";
ctx.fillStyle = "white";
ctx.fillText("Hello!", 100, 100);
fillText(text, x, y) draws text at a position.
The font property is a CSS-style font string.
For text alignment:
ctx.textAlign = "center"; // or "left" (default), "right"
ctx.fillText("Centered", 300, 200);
Where else does this look like CSS?
A few canvas properties take CSS-like values:
ctx.fillStyle = "red"or"#ff0000"or"rgba(255, 0, 0, 0.5)"ctx.font = "30px Arial"ctx.lineWidth = 4
Familiar territory. The web is unified.
Checkpoint: Your canvas has at least 4 different shapes (rectangle, circle, polygon, text), in different colors. This is the natural stop point if class is cut short.
Part B: Build a scene
Time to make something yours.
Pick a subject
Same kind of exercise as Phase 6 Session 2 (the shape-drawing scene). Pick:
- A face — circles for head and eyes, lines for smile.
- A landscape — mountains as triangles, sky, sun, trees.
- A rocket ship — rectangles + triangles + circle windows.
- A city skyline — rectangles of varying heights.
- A bouncing logo screensaver (like the old DVD player).
- An abstract pattern — circles in a grid, color gradients.
Use loops
JavaScript loops work in canvas too. A row of stars:
for (let i = 0; i < 50; i++) {
const x = Math.random() * 600;
const y = Math.random() * 200;
const r = Math.random() * 3 + 1;
ctx.beginPath();
ctx.arc(x, y, r, 0, Math.PI * 2);
ctx.fillStyle = "white";
ctx.fill();
}
50 random stars at random positions and sizes. The power of loops + drawing.
Stretch — gradients
const gradient = ctx.createLinearGradient(0, 0, 0, 400);
gradient.addColorStop(0, "#ff7e5f");
gradient.addColorStop(1, "#feb47b");
ctx.fillStyle = gradient;
ctx.fillRect(0, 0, 600, 400);
A vertical sunset gradient (orange to peach).
createLinearGradient(x1, y1, x2, y2) makes a
gradient from one point to another. addColorStop(t, color) places a color at position t (0 to 1).
Then use the gradient as a fillStyle.
Stretch — drawing with the mouse
You can draw with mouse events. Add to your script:
let drawing = false;
canvas.addEventListener("mousedown", () => drawing = true);
canvas.addEventListener("mouseup", () => drawing = false);
canvas.addEventListener("mousemove", (event) => {
if (!drawing) return;
const rect = canvas.getBoundingClientRect();
const x = event.clientX - rect.left;
const y = event.clientY - rect.top;
ctx.beginPath();
ctx.arc(x, y, 5, 0, Math.PI * 2);
ctx.fillStyle = "black";
ctx.fill();
});
Click and drag on the canvas. You’re drawing. A tiny paint program in 10 lines.
The getBoundingClientRect() business is needed
because the mouse position is relative to the page,
not the canvas. Subtract the canvas’s left/top to get
the canvas-relative position.
Stretch — clear the canvas
ctx.clearRect(0, 0, canvas.width, canvas.height);
Erases everything. Useful for animations (next session).
Add a button to clear:
<button id="clearBtn">Clear</button>
<script>
document.querySelector("#clearBtn").addEventListener("click", () => {
ctx.clearRect(0, 0, canvas.width, canvas.height);
});
</script>
Extension — save as image
const dataURL = canvas.toDataURL("image/png");
// Open in new tab:
window.open(dataURL);
toDataURL() returns the canvas as an encoded image.
Visit the URL in a new tab to download. Real “save
my drawing” feature.
Wrap-up
Before we leave, share with the room:
- For everyone — show your scene. What did you build?
- For the kids who used loops — what did you generate?
- Anyone try the mouse drawing? Did it feel like a real paint program?
- Did the Pygame mental model transfer cleanly?
Today you learned:
<canvas>— drawing surface in HTML.getContext("2d")— your drawing handle (ctx).ctx.fillRect,strokeRect— rectangles.ctx.arc— circles (and other arcs).ctx.moveTo+lineTo— lines and polygons.ctx.beginPath/fill/stroke— the path pattern.ctx.fillStyle,strokeStyle,lineWidth,font— visual properties.ctx.fillText— text on canvas.- Mouse coordinates with
getBoundingClientRect.
The Pygame mental model directly transfers. Different language, same drawing primitives, same coordinates. If you survived Phase 6, you can canvas.
Next week: canvas + animation + input = a real game in the browser.
If you missed this session
Open Thonny.
-
Build the canvas-test.html with the basic rectangle and circle.
-
Build the scene from Part A — sky, ground, sun, house.
-
Build your own scene — pick a subject from the list, use at least 6 shapes.
-
(Stretch) Try the mouse-draw feature.
About 30-45 minutes. By the end you should have a canvas scene of your own.
Stretch and extension ideas
- Gradients — linear and radial.
- Mouse drawing — paint program.
- Fill style with patterns or images.
- Shadow effects —
ctx.shadowBlur,shadowColor. - Save as image with
toDataURL. - Clear button to wipe and start over.
- Color picker — let user choose color.
- Brush size — slider for the pen size.
- Read MDN’s Canvas tutorial for the full API.
What’s next
Next week: animation + input = a real
mini-game in the browser. We use
requestAnimationFrame for the frame loop, listen
for keyboard events, and build something playable.
The Pygame patterns from Phase 6 transfer directly.