Skip to content
JavaScript js canvas 5 min read

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 (default false, 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. Use arcTo when 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

MethodControl pointsBest for
arcnone (uses angles)circles, pie slices, dials
arcTotangent cornerrounded rectangles, corners
quadraticCurveTo1simple smooth bends
bezierCurveTo2S-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 toRad helper or const TAU = Math.PI * 2 handy.
  • Use arc with center coordinates, not corner coordinates — a common off-by-radius mistake.
  • Prefer arcTo or the native roundRect for rounded corners instead of stitching arcs by angle.
  • For pie slices, moveTo the center, draw the arc, then closePath() 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.
Last updated June 1, 2026
Was this helpful?