Home About Me

More Canvas Basics: Arcs, Stroke vs Fill, Line Edges, and a Simple Sine Wave

After putting together some earlier notes on Canvas, here is another batch of basic examples. They stay at the same introductory level, but cover a few very practical drawing details.

One small side note before the examples: when recording visual results in Markdown, it turns out that many parsers can render raw HTML directly. That makes Canvas demos much easier to include inline. In practice, all you need is a <canvas> tag plus a <script> tag, and then do the drawing work inside window.onload.

That said, this approach does have a downside: compatibility is weaker. Some online Markdown editors do not parse this kind of HTML at all. In local editing and static-site compilation it may work fine, but unless the article specifically needs live web-based visual demos, it is still better not to mix in too much HTML.

Basic HTML structure

<!DOCTYPE html>
<html>
<head>
  <meta charset="utf-8">
  <title>html5 canvas</title>
</head>
<body>
<canvas id="canvas" width="320" height="200"  style="border: 1px dashed;">
</canvas>
</body>
</html>

This is the minimal setup used in the following snippets: a canvas with a fixed size and a visible dashed border so the drawing area is easy to see.

Circles and semicircles

The first example uses arc() to draw several shapes in different positions. Some are open arcs, some are full circles, some are stroked, and one is filled. It also shows how changing the last argument can reverse the drawing direction.

window.onload = function() {
  var canvas = document.getElementById("canvas");
      context = canvas.getContext("2d");

  // 左上
  context.beginPath();
  context.arc(80, 50, 30, 0, Math.PI);
  context.stroke();

  // 右上
  context.beginPath();
  context.arc(240, 50, 30, 0, Math.PI * 2);
  context.closePath();
  context.strokeStyle = "green";
  context.stroke();

  // 中
  context.beginPath();
  context.arc(160, 80, 30, 0, Math.PI);
  context.strokeStyle = "#8C7676";
  context.closePath();
  context.stroke();

  // 左下
  context.beginPath();
  context.arc(80, 150, 30, 0, Math.PI * 2);
  context.closePath();
  context.fillStyle = "#99B0C2";
  context.fill();

  // 右下
  context.beginPath();
  context.arc(240, 150, 30, 0, Math.PI, true);
  context.strokeStyle = "#aa0000";
  context.stroke();
}

A few details are worth noticing here:

  • Math.PI gives a half circle.
  • Math.PI * 2 gives a full circle.
  • strokeStyle and fillStyle can be changed between shapes.
  • closePath() affects how the path is closed before drawing or filling.
  • The final true argument in arc() draws counterclockwise.

Drawing a sine curve

Canvas is also convenient for plotting a simple function point by point. In the following example, the first path draws a horizontal reference line at y = 100, and the second path uses a loop to generate a sine wave.

window.onload = function() {
  var canvas = document.getElementById("canvas");
      context = canvas.getContext("2d");

  // 横线
  context.beginPath();
  context.moveTo(0, 100);
  context.lineTo(320, 100);
  context.stroke();

  context.beginPath();
  context.moveTo(0, 100);

  for(var x = 0; x <320; x++) {
    var y = 100 + Math.sin(x * 0.02) * 100;
    console.log(x);
    console.log(y);
    context.lineTo(x, y);
  }
  context.stroke();
}

The expression

100 + Math.sin(x * 0.02) * 100

sets the vertical center at 100 and scales the amplitude by 100, so the curve oscillates around the middle horizontal line. The for loop moves across the canvas one pixel at a time, connecting each calculated point with lineTo().

Stroke and fill

The next snippet compares four square-drawing combinations: stroke only, fill only, stroke then fill, and fill then stroke. In the demonstration, the border width was increased on purpose so the visual difference would be more obvious.

// 设置边框宽度
context.lineWidth = lw;

// 左上
context.beginPath();
context.rect(55, 25, 50, 50);
context.stroke();

// 右上
context.beginPath();
context.rect(215, 25, 50, 50);
context.fill();

// 左下
context.beginPath();
context.rect(55, 125, 50, 50);
context.stroke();
context.fill();

// 右下
context.beginPath();
context.rect(215, 125, 50, 50);
context.fill();
context.stroke();

This is a straightforward way to see that stroke() and fill() are independent operations, and that their order can affect the final appearance when line width is more noticeable.

Line edge and corner behavior

When drawing lines, the shape of the endpoints and the way corners connect can change the look quite a bit. The following example demonstrates lineCap, lineJoin, and optionally miterLimit.

window.onload = function () {
  var canvas = document.getElementById("canvas"),
      context = canvas.getContext("2d");

  // 边缘形状
  context.lineCap = "round";
  // context.lineCap = "square";

  // 链接处形状
  // context.lineJoin = "bevel";
  context.lineJoin = "miter";
  // context.lineJoin = "round";
  // context.miterLimit = "3";


  context.lineWidth = 10;
  context.strokeStyle = "#FFDD83";
  draw();

  context.lineWidth = 1;
  context.strokeStyle = "#405983";
  draw();

  function draw() {
    context.beginPath();
    context.moveTo(50, 50);
    context.lineTo(100, 100);
    context.stroke();

    context.beginPath();
    context.moveTo(180, 50);
    context.lineTo(200, 100);
    context.lineTo(220, 50);
    context.stroke();

    context.beginPath();
    context.strokeRect(100, 130, 30, 30);
  }
}

Here the same shapes are drawn twice: first with a thick yellow stroke, then again with a thin dark stroke. That overlay makes the cap and join behavior easier to inspect. Switching between round, square, bevel, and miter gives a quick visual sense of how Canvas handles edges and corners.

Radial gradient inside a circle

Canvas gradients are also easy to test with a small self-contained example. This one creates a radial gradient that transitions from white at the center to black at the edge, then fills a circular path.

window.onload = function () {
  var canvas = document.getElementById("canvas"),
      context = canvas.getContext("2d");

  var gradient = context.createRadialGradient(120, 80, 0, 110, 90, 70);
  gradient.addColorStop(0, "white");
  gradient.addColorStop(1, "black");

  context.fillStyle = gradient;
  context.beginPath();
  context.arc(100, 100, 50, 0, Math.PI * 2, false);
  context.fill();
}

The key part is createRadialGradient(), which defines two circles for the gradient calculation. After that, addColorStop() sets the transition points, and the gradient can be assigned directly to fillStyle before filling the shape.