Circles, Arcs & Curves
Straight lines only get you so far. Real interfaces, charts, and games are full of rounded corners, pie slices, dials, and flowing curves. The 2D canvas gives you four path-building methods for this: arc and arcTo for circular shapes, and quadraticCurveTo and bezierCurveTo for free-form curves. All of them add segments to the current path, which you then stroke or fill. This page walks through each one, including the part that trips everyone up: angles in radians.
Drawing arcs with arc()
The workhorse for anything circular is arc():
ctx.arc(x, y, radius, startAngle, endAngle, counterclockwise);
x,y— the center of the circle (not a corner).radius— distance from center to edge, in pixels.startAngle,endAngle— measured in radians, not degrees.counterclockwise— optional boolean (defaultfalse, i.e. clockwise).
Angles start at the positive x-axis (3 o’clock) and increase clockwise on the canvas, because the y-axis points down. A full circle is 2 * Math.PI radians. To convert degrees to radians, multiply by Math.PI / 180.
const toRad = (deg) => (deg * Math.PI) / 180;
console.log(toRad(180)); // half turn
console.log(2 * Math.PI); // full turn
Output:
3.141592653589793
6.283185307179586
A full circle
For a complete circle, sweep from 0 to 2 * Math.PI. The counterclockwise flag is irrelevant for a full sweep.
ctx.beginPath();
ctx.arc(100, 75, 50, 0, 2 * Math.PI);
ctx.fillStyle = "#2563eb";
ctx.fill();
Always call
ctx.beginPath()before starting a new shape. Without it, the new arc joins the previous path, and a stray line connects them when you stroke or fill.
A partial arc (pie slice)
A pie slice is an arc plus two lines back to the center. Start at the center with moveTo, draw the arc, then close the path.
ctx.beginPath();
ctx.moveTo(100, 100); // center
ctx.arc(100, 100, 60, toRad(-30), toRad(60)); // wedge
ctx.closePath();
ctx.fillStyle = "#f59e0b";
ctx.fill();
Rounded corners with arcTo()
arcTo() is easier for rounding corners because it works in terms of tangent lines rather than angles:
ctx.arcTo(x1, y1, x2, y2, radius);
It draws an arc tangent to the line from the current point to (x1, y1) and the line from (x1, y1) to (x2, y2), using the given radius. (x1, y1) acts like the corner point you are rounding off. This is the classic way to build a rounded rectangle.
function roundedRect(ctx, x, y, w, h, r) {
ctx.beginPath();
ctx.moveTo(x + r, y);
ctx.arcTo(x + w, y, x + w, y + h, r); // top-right
ctx.arcTo(x + w, y + h, x, y + h, r); // bottom-right
ctx.arcTo(x, y + h, x, y, r); // bottom-left
ctx.arcTo(x, y, x + w, y, r); // top-left
ctx.closePath();
ctx.stroke();
}
Modern browsers also support
ctx.roundRect(x, y, w, h, radii)natively, which is simpler when you don’t need per-corner tangent control. UsearcTowhen you need finer manual control or wider legacy support.
Quadratic curves with quadraticCurveTo()
A quadratic Bézier curve has a single control point that pulls the line toward it. The curve starts at the current point and ends at the destination, bending toward the control point without touching it.
ctx.quadraticCurveTo(cpx, cpy, x, y);
cpx,cpy— the control point.x,y— the end point.
ctx.beginPath();
ctx.moveTo(20, 100);
ctx.quadraticCurveTo(120, 10, 220, 100); // one control point
ctx.stroke();
Cubic curves with bezierCurveTo()
A cubic Bézier curve uses two control points, giving you S-shapes and more expressive curves. The first control point governs the departure direction from the start; the second governs the arrival direction at the end.
ctx.bezierCurveTo(cp1x, cp1y, cp2x, cp2y, x, y);
ctx.beginPath();
ctx.moveTo(20, 120);
ctx.bezierCurveTo(60, 10, 180, 230, 220, 120);
ctx.stroke();
Comparing the curve methods
| Method | Control points | Best for |
|---|---|---|
arc | none (uses angles) | circles, pie slices, dials |
arcTo | tangent corner | rounded rectangles, corners |
quadraticCurveTo | 1 | simple smooth bends |
bezierCurveTo | 2 | S-curves, expressive paths |
Try it: circles and a curve
This self-contained demo draws several filled circles and a cubic Bézier curve on a single canvas.
<canvas id="c" width="400" height="220" style="border:1px solid #ccc"></canvas>
<script>
const ctx = document.getElementById("c").getContext("2d");
const TAU = Math.PI * 2;
// A row of circles with varying radius and color
const colors = ["#ef4444", "#f59e0b", "#10b981", "#3b82f6", "#8b5cf6"];
colors.forEach((color, i) => {
ctx.beginPath();
ctx.arc(50 + i * 75, 60, 20 + i * 4, 0, TAU);
ctx.fillStyle = color;
ctx.fill();
});
// A smooth cubic Bézier curve underneath
ctx.beginPath();
ctx.moveTo(20, 180);
ctx.bezierCurveTo(120, 100, 280, 260, 380, 160);
ctx.lineWidth = 3;
ctx.strokeStyle = "#1e293b";
ctx.stroke();
</script>
Try it: an animated clock-hand dial
Arcs and angles shine in dials and gauges. This pen sweeps a stroked arc like a progress ring.
<canvas id="dial" width="240" height="240"></canvas>
<script>
const ctx = document.getElementById("dial").getContext("2d");
const cx = 120, cy = 120, r = 90;
let progress = 0;
function frame() {
ctx.clearRect(0, 0, 240, 240);
// Track
ctx.beginPath();
ctx.arc(cx, cy, r, 0, Math.PI * 2);
ctx.lineWidth = 14;
ctx.strokeStyle = "#e2e8f0";
ctx.stroke();
// Progress arc, starting at 12 o'clock (-90deg)
const start = -Math.PI / 2;
const end = start + progress * Math.PI * 2;
ctx.beginPath();
ctx.arc(cx, cy, r, start, end);
ctx.lineWidth = 14;
ctx.lineCap = "round";
ctx.strokeStyle = "#6366f1";
ctx.stroke();
progress = (progress + 0.005) % 1;
requestAnimationFrame(frame);
}
frame();
</script>
Best Practices
- Call
ctx.beginPath()before every distinct shape so paths don’t accidentally connect. - Remember angles are in radians; keep a
toRadhelper orconst TAU = Math.PI * 2handy. - Use
arcwith center coordinates, not corner coordinates — a common off-by-radius mistake. - Prefer
arcToor the nativeroundRectfor rounded corners instead of stitching arcs by angle. - For pie slices,
moveTothe center, draw thearc, thenclosePath()to seal the wedge. - Sketch control points on paper (or render them as dots while developing) to reason about Bézier curves.
- Set
lineCap = "round"on stroked arcs for clean, polished progress rings and gauges.