Paths & Shapes
Draw any shape you can imagine using paths, arcs, and curves.
Rectangles are useful for prototyping, but real graphics need triangles, circles, stars, and freeform shapes. This lesson introduces the path system: Canvas’s way of describing any shape you can imagine. By the end, you will draw polygons, smooth curves, and combine them into complex figures.
The “Pen” Metaphor
In Lesson 1, we drew rectangles, but the real world isn’t made of rectangles. To draw triangles, circles, arrows, speech bubbles, or any custom shape, we need paths.
Imagine you’re holding an invisible pen over a piece of paper:
Drawing with an invisible pen:
1. Pick up the pen and place it somewhere
--> moveTo(x, y) "Move without drawing"
2. Drag the pen to draw a line
--> lineTo(x, y) "Draw a straight line to here"
3. Draw a curve
--> arc(x, y, r, startAngle, endAngle)
--> bezierCurveTo(cp1x, cp1y, cp2x, cp2y, x, y)
4. Go back to where you started
--> closePath() "Connect back to start"
5. Fill in the shape or trace its outline
--> fill() or stroke() "Apply the ink"
The pen REMEMBERS where it is after each command.
Every new command starts from where the last one ended. The key insight: path commands don’t draw anything visible. They build up an invisible outline. Only when you call fill() or stroke() does the shape actually appear on the canvas.
Why beginPath() is Critical
beginPath() starts a brand new path, throwing away whatever path was being built before. This is the single most common source of canvas bugs for beginners: forgetting beginPath().
Try this: Delete the second ctx.beginPath() call and hit Run. Both triangles turn cyan because stroke() redraws the entire accumulated path. Add it back to see the fix.
Rule of thumb: Call beginPath() every time you start a new shape. Without it, every shape you’ve drawn since the last beginPath() is still part of the current path, and fill() or stroke() will apply to ALL of them.
moveTo and lineTo: Straight Lines
moveTo(x, y) picks up the pen and places it at a new position without drawing. lineTo(x, y) draws a straight line from wherever the pen currently is to the new point.
closePath: Connecting Back to Start
closePath() draws a straight line from the current pen position back to the very first point of the current path (where you called moveTo() initially). It’s how you close a shape cleanly.
Important note: fill() automatically closes the path for you (it has to, since you can’t fill an open shape). But stroke() does NOT auto-close. So if you’re stroking a shape and you want it closed, always call closePath() before stroke().
fill() vs stroke(): Filling vs Outlining
After building a path, you have two choices for making it visible:
fill()colors in the entire interior of the shapestroke()draws just the outline
You can use both on the same path!
Arcs and Circles
arc(x, y, radius, startAngle, endAngle, counterclockwise) draws a circular arc (a portion of a circle, or the whole circle). Angles are in radians, not degrees.
Radians cheat sheet:
Degrees Radians Position on clock
─────── ───────────── ──────────────────
0 0 3 o'clock (right)
90 Math.PI / 2 6 o'clock (bottom)
180 Math.PI 9 o'clock (left)
270 Math.PI * 1.5 12 o'clock (top)
360 Math.PI * 2 full circle
Formula: radians = degrees * Math.PI / 180
270 / PI*1.5
│
│
180 / PI ───────────●─────────── 0 (start)
│
│
90 / PI/2
REMEMBER: 0 radians points RIGHT, not up!
And angles go CLOCKWISE (Y is flipped in canvas). Arc Direction: Clockwise vs Counterclockwise
The last parameter of arc() is counterclockwise (default: false = clockwise). This controls which way the arc sweeps from the start angle to the end angle.
When does direction matter? For a full circle (0 to PI*2), it doesn’t matter. But for partial arcs (pie charts, gauges, pac-man), the direction determines which “slice” of the circle you get. Same start and end angle, but clockwise gives you the big slice while counterclockwise gives the small slice (or vice versa).
rect(): Rectangle as a Path
We used fillRect() and strokeRect() in Lesson 1, but there’s also rect(x, y, width, height) which adds a rectangle to the current path without drawing it immediately. This lets you combine rectangles with other path operations.
ellipse(): Ovals and Rotated Circles
ellipse(x, y, radiusX, radiusY, rotation, startAngle, endAngle) is like arc() but allows different horizontal and vertical radii, plus rotation. The egg example below also uses save(), translate(), scale(), and restore() to stretch a circle into an oval; these transform methods will be covered in a later lesson.
Quadratic Curves: One Control Point
quadraticCurveTo(cpx, cpy, endX, endY) draws a curve from the current pen position, bending toward a control point, and ending at the endpoint.
The rubber band analogy:
Imagine a rubber band pinned at START and END.
The control point is a finger pulling the band sideways.
control point (cpx, cpy)
●
/ \
/ \ The curve bends TOWARD
/ \ the control point but
/ \ never actually touches it
●─────────● (usually).
START END
The further the control point is from
the straight line, the more the curve bends. Want the actual math? The quadratic Bezier formula is B(t) = (1-t)^2*P0 + 2*(1-t)*t*P1 + t^2*P2. It’s just two levels of lerp (linear interpolation) stacked together. See the Math Appendix for the full derivation, interactive visualizations, and why it works.
Bezier Curves: Two Control Points
bezierCurveTo(cp1x, cp1y, cp2x, cp2y, endX, endY) is the more powerful cousin of quadratic curves. With two control points, you can make S-curves, loops, and the smooth curves used in fonts and vector graphics.
How two control points shape a curve:
CP1 controls the CP2 controls the
START of the curve END of the curve
● ●
╱ ╲
╱ The curve leaves ╲
╱ START heading ╲
● toward CP1, then ────●
START bends to arrive END
from CP2's direction
Think of it as: CP1 says "leave this way"
CP2 says "arrive this way"
S-curve (control points on opposite sides):
● CP1
╲
╲ ╱────╲
●───╲──╱ ╲──●
START END
╱
╱
● CP2 The cubic formula: B(t) = (1-t)^3*P0 + 3*(1-t)^2*t*P1 + 3*(1-t)*t^2*P2 + t^3*P3. Three levels of lerp. The Math Appendix derives this step by step and shows why the coefficients 1, 3, 3, 1 come from Pascal’s triangle.
Fill Rules: nonzero vs evenodd
When paths overlap or have holes, how does Canvas decide what’s “inside” the shape? That’s the fill rule. You pass it to fill().
The 5 points sit on a circle like a pentagon. But instead of connecting 0→1→2→3→4 (which would draw a pentagon), the star connects 0→2→4→1→3 (skipping one each time). The lines cross over each other, creating a center region where the path overlaps. That overlap is what makes the fill rule matter:
When to use each:
nonzero (default): - Fills everything enclosed by the path - Use for simple shapes, most of the time evenodd: - Counts how many times a ray crosses the path - Odd crossings = inside (filled) - Even crossings = outside (not filled) - Use for shapes with holes (donuts, stars, etc.)
Practical Shape: Rounded Rectangle
Rounded rectangles are everywhere in UI design (buttons, cards, tooltips). Modern browsers support ctx.roundRect(x, y, width, height, radii) (since 2023), but building one yourself from lines and arcs is great practice for learning path primitives.
Practical Shape: Speech Bubble
A speech bubble is a rounded rectangle with a triangular tail, combining lines, arcs, and curves in one path.
Practical Shape: Arrows
Arrows are useful for diagrams, flowcharts, and UI. Here’s how to build them from paths.
Practical Shape: Regular Polygons
With a simple loop, you can draw any regular polygon (triangle, pentagon, hexagon, octagon…) by computing points around a circle.
Notice: As the number of sides increases, the polygon starts looking like a circle! In fact, arc() is just a polygon with so many sides you can’t see the edges. This is how computers draw circles; they approximate them with many tiny straight lines.
Practical Shape: Gear
A gear combines the polygon idea with alternating inner and outer radii, the same approach as the star in the fill rules demo, but with more teeth.
How does alternating between two radii create teeth? The idea: place 2 x teeth points around a circle. Even points sit on the outer radius (tooth tips), odd points sit on the inner radius (tooth valleys). Connect them in order and you get a gear.
The gear algorithm:
teeth = 12 --> points = 24 Point 0 (even) --> outer radius --> tooth TIP Point 1 (odd) --> inner radius --> tooth VALLEY Point 2 (even) --> outer radius --> tooth TIP Point 3 (odd) --> inner radius --> tooth VALLEY ...repeat... Each pair (tip + valley) = one tooth. Connect all points in order --> gear shape. Fewer teeth = bigger teeth (8-tooth vs 12-tooth). Gap between radii = tooth height.
Preview: isPointInPath() for Hit Testing
Canvas doesn’t have built-in “click on a shape” like DOM elements. But isPointInPath(x, y) tells you whether a point is inside the current path. This is the foundation for making shapes interactive (covered fully in Lesson 7).
How it works: After building a path (but before or after drawing it), call ctx.isPointInPath(x, y). It returns true if the point is inside the path, false otherwise. This works for ANY shape: circles, polygons, custom paths, even bezier curves!
Putting It All Together: A Complete Scene
Let’s combine everything from this lesson into a single drawing that uses lines, arcs, curves, and custom shapes.
Challenges
Challenge 1: Draw a House with Paths
Use paths (not fillRect) to draw: - A triangular roof (moveTo + lineTo + closePath + fill) - A rectangular wall (rect or 4 lineTo calls) - A circular window (arc) - An arched doorway (lineTo + arc) This practices combining different path types.
Challenge 2: Draw a Pie Chart
Given data like: var data = [30, 20, 25, 15, 10];
Draw a pie chart:
- Each slice is an arc (use moveTo to center, then arc)
- Calculate angles: slice / total * Math.PI * 2
- Use closePath() + fill() for each slice
- Different color per slice
Hint:
var total = 100;
var startAngle = 0;
data.forEach(function(value) {
var sliceAngle = (value / total) * Math.PI * 2;
ctx.beginPath();
ctx.moveTo(cx, cy);
ctx.arc(cx, cy, radius, startAngle, startAngle + sliceAngle);
ctx.closePath();
ctx.fill();
startAngle += sliceAngle;
}); Challenge 3: Draw Your Initials
Use lineTo and bezierCurveTo to draw your initials
as custom letter shapes (not ctx.fillText).
For example, the letter "S" could be two arcs:
ctx.beginPath();
ctx.arc(x, y1, r, Math.PI, 0); // top curve
ctx.arc(x, y2, r, 0, Math.PI); // bottom curve (reversed)
ctx.stroke();
For straight letters (L, T, H), use lineTo.
For curved letters (S, C, O), use arc or bezierCurveTo. What you learned in this lesson:
Concept How to do it
────────────────────── ─────────────────────────────────────
Start a new path ctx.beginPath()
Move pen (no drawing) ctx.moveTo(x, y)
Straight line ctx.lineTo(x, y)
Close the shape ctx.closePath()
Fill the shape ctx.fill() or ctx.fill('evenodd')
Outline the shape ctx.stroke()
Circle/arc ctx.arc(x, y, r, start, end, ccw?)
Oval ctx.ellipse(x, y, rx, ry, rot, s, e)
Rectangle path ctx.rect(x, y, w, h)
Simple curve ctx.quadraticCurveTo(cpx, cpy, x, y)
Complex curve ctx.bezierCurveTo(c1x,c1y,c2x,c2y,x,y)
Hit testing ctx.isPointInPath(x, y)
Key ideas:
- ALWAYS call beginPath() before a new shape
- Path commands are invisible: fill()/stroke() makes them real
- fill() auto-closes paths; stroke() does not
- Angles are in RADIANS (PI = 180 degrees)
- Combine line, arc, and curve calls in one path for complex shapes