Drawing Basic Shapes

1

Drawing Basic Shapes

▶

The HTML5 Canvas API is one of the most powerful tools available in a web browser for rendering graphics programmatically. Rather than relying on static images or CSS visuals, the Canvas API lets you draw shapes, paths, text, and images directly onto a pixel-based surface using JavaScript. Before any drawing can happen, you need to understand how to prepare the canvas and obtain the drawing context — the gateway to every visual operation the API offers.

The Canvas API draws everything imperatively: you issue commands that immediately paint pixels onto the surface. Unlike the DOM, where elements persist and can be styled or queried later, canvas drawings are essentially "burned" into a bitmap. This makes it critical to understand the order of operations, how styles are applied, and how paths work, because there are no individual shape objects to select and modify after the fact.

Setting Up the Canvas Context

The first step is placing a <canvas> element in your HTML document. The element accepts width and height attributes that define the resolution of the drawing surface in CSS pixels. These are not the same as the element's CSS display size — the width and height attributes define the actual coordinate space of the canvas buffer.

<canvas id="myCanvas" width="500" height="400">
  Your browser does not support the Canvas API.
</canvas>

The fallback text between the opening and closing tags is shown only in browsers that do not support the <canvas> element at all. For all modern browsers this text will never be visible, but it is a good accessibility practice to include it. A more robust approach is to test for Canvas support in JavaScript before attempting to draw:

const canvas = document.getElementById('myCanvas');

if (!canvas.getContext) {
  console.error('Canvas is not supported in this browser.');
} else {
  const ctx = canvas.getContext('2d');
  // All drawing code goes here
}

The getContext('2d') call returns a CanvasRenderingContext2D object, commonly stored in a variable named ctx. This object exposes every drawing method and style property you will use — rectangles, paths, arcs, colors, line widths, and much more. If the context type passed to getContext() is not supported, the method returns null, so checking for a falsy return value is a safe defensive measure.

It is also worth noting that if you resize a canvas using CSS (for example, setting it to width: 100%), the drawn pixels are stretched to fit, which can make graphics look blurry. To avoid this, always set the width and height attributes to match the intended pixel dimensions, and handle high-DPI (Retina) screens separately if crisp rendering on those displays is required.

Drawing Rectangles

The rectangle is the only shape that the Canvas API can render with a single method call, without the need to build a path first. Three dedicated methods handle rectangles directly:

  • fillRect(x, y, width, height) — Draws a solid, filled rectangle. The top-left corner is positioned at (x, y), and the rectangle extends width pixels to the right and height pixels downward. The fill color is determined by the current fillStyle property.
  • strokeRect(x, y, width, height) — Draws only the border (outline) of a rectangle at the same position and size, without filling the interior. The border color and thickness come from strokeStyle and lineWidth.
  • clearRect(x, y, width, height) — Erases all pixels within the specified rectangular area, making them fully transparent. This is particularly useful when creating animations, where you need to wipe the canvas clean before redrawing the next frame.
const canvas = document.getElementById('myCanvas');
const ctx = canvas.getContext('2d');

// Draw a solid blue rectangle
ctx.fillStyle = 'steelblue';
ctx.fillRect(50, 50, 200, 100);

// Draw an outlined rectangle in dark red
ctx.strokeStyle = 'darkred';
ctx.lineWidth = 4;
ctx.strokeRect(300, 50, 150, 100);

// Clear a small region inside the blue rectangle
ctx.clearRect(100, 75, 60, 50);

In this example, a solid blue rectangle appears at position (50, 50). A dark red outlined rectangle appears to the right. The clearRect call punches a transparent hole inside the blue rectangle, revealing the white canvas background (or whatever is behind the canvas element). This demonstrates how clearRect does not paint a color — it removes pixels entirely.

Drawing Circles and Arcs

The Canvas API does not have a dedicated drawCircle() method. Instead, circles are drawn using the general-purpose arc() method, which is part of the path system. Its full signature is:

ctx.arc(x, y, radius, startAngle, endAngle, anticlockwise)
  • x, y — The coordinates of the center point of the arc.
  • radius — The radius of the arc in pixels.
  • startAngle — The angle at which the arc begins, measured in radians from the positive x-axis (the rightmost point of the circle).
  • endAngle — The angle at which the arc ends, also in radians.
  • anticlockwise — An optional boolean (default false). When true, the arc is drawn counterclockwise from the start angle to the end angle.

Angles in the Canvas API are always expressed in radians, not degrees. A full circle spans 2 * Math.PI radians (approximately 6.283). If you are more comfortable thinking in degrees, use this conversion formula:

radians = degrees * (Math.PI / 180)

For example, 90 degrees equals 90 * (Math.PI / 180), which is Math.PI / 2 radians. To draw a complete circle you start at 0 radians and end at 2 * Math.PI:

const ctx = document.getElementById('myCanvas').getContext('2d');

// Filled circle
ctx.beginPath();
ctx.arc(150, 150, 80, 0, 2 * Math.PI);
ctx.fillStyle = 'tomato';
ctx.fill();

// Outlined circle (same center, different radius)
ctx.beginPath();
ctx.arc(350, 150, 60, 0, 2 * Math.PI);
ctx.strokeStyle = 'navy';
ctx.lineWidth = 5;
ctx.stroke();

Notice that beginPath() is called before each arc. Without it, both arcs would be joined together as a single path, producing unexpected results when fill() or stroke() is called. The anticlockwise parameter matters most when drawing partial arcs (wedges or pie slices). For example, drawing an arc from 0 to Math.PI / 2 clockwise gives you the bottom-right quarter of a circle, while drawing it anticlockwise gives you the remaining three-quarters.

Drawing Polygons and Lines with Paths

Beyond rectangles and circles, almost every other shape — triangles, pentagons, stars, arrows — is built by constructing a path. A path is a sequence of connected points and curves that you define step by step, and then render all at once with fill() or stroke(). The key path methods are:

  • beginPath() — Resets the current path, discarding any previously defined segments. Always call this before starting a new shape to avoid unintentionally connecting it to previous drawings.
  • moveTo(x, y) — Lifts the virtual "pen" and places it at (x, y) without drawing anything. This sets the starting point of the path.
  • lineTo(x, y) — Draws a straight line from the current pen position to (x, y), then moves the pen to the new point.
  • closePath() — Draws a straight line from the current position back to the point defined by the most recent moveTo(), effectively closing the shape.
  • fill() — Fills the enclosed area of the current path with the current fillStyle.
  • stroke() — Draws the outline of the current path using the current strokeStyle and lineWidth.

Here is how to draw a filled triangle:

ctx.beginPath();
ctx.moveTo(250, 50);   // Top vertex
ctx.lineTo(100, 300);  // Bottom-left vertex
ctx.lineTo(400, 300);  // Bottom-right vertex
ctx.closePath();       // Line back to (250, 50)
ctx.fillStyle = 'mediumseagreen';
ctx.fill();
ctx.strokeStyle = 'darkgreen';
ctx.lineWidth = 3;
ctx.stroke();

The same technique scales to any polygon. For a regular pentagon, you can use trigonometry to calculate each vertex's position based on an angle that increments by (2 * Math.PI) / 5 for each of the five sides:

const sides = 5;
const centerX = 250, centerY = 200, radius = 100;

ctx.beginPath();
for (let i = 0; i < sides; i++) {
  const angle = (2 * Math.PI / sides) * i - Math.PI / 2; // Start from the top
  const x = centerX + radius * Math.cos(angle);
  const y = centerY + radius * Math.sin(angle);
  if (i === 0) {
    ctx.moveTo(x, y);
  } else {
    ctx.lineTo(x, y);
  }
}
ctx.closePath();
ctx.fillStyle = 'goldenrod';
ctx.fill();

Subtracting Math.PI / 2 from the angle rotates the starting point to the top of the polygon, which gives it the natural "upright" orientation that most people expect. Without this offset, the first vertex would appear at the rightmost point of the bounding circle.

Applying Fill and Stroke Styles

Colors and line styles in the Canvas API are set as properties on the context object before drawing commands are issued. The most important style properties are:

  • ctx.fillStyle — Accepts any valid CSS color value as a string: named colors ('red'), hex codes ('#ff6347'), rgb(), rgba(), hsl(), and hsla(). This property governs the interior color of any shape rendered with fill() or fillRect().
  • ctx.strokeStyle — Same format as fillStyle, but controls the color of outlines drawn by stroke() or strokeRect().
  • ctx.lineWidth — A number specifying the stroke thickness in pixels. The default is 1. Setting it to 5, for instance, makes all subsequent stroked lines five pixels wide.

A crucial behavior to understand is that these properties are stateful and persistent. Once you set fillStyle to 'red', every subsequent fill call will use red until you explicitly change it. This means you must set the appropriate styles immediately before drawing each shape when you want different shapes to have different appearances:

// Shape 1: orange fill, black stroke
ctx.fillStyle = 'orange';
ctx.strokeStyle = 'black';
ctx.lineWidth = 2;
ctx.fillRect(50, 50, 120, 80);
ctx.strokeRect(50, 50, 120, 80);

// Shape 2: purple fill, no visible stroke (lineWidth still 2)
ctx.fillStyle = 'purple';
ctx.beginPath();
ctx.arc(300, 100, 60, 0, 2 * Math.PI);
ctx.fill();

The rgba() color format is especially useful because it lets you control transparency. An alpha value of 0 is fully transparent, and 1 is fully opaque. For example, 'rgba(255, 0, 0, 0.4)' draws a semi-transparent red that allows shapes underneath to show through, enabling layering effects without extra DOM elements.

Property Default Value Accepted Formats Affects
fillStyle 'black' CSS color string, gradient, pattern fill(), fillRect()
strokeStyle 'black' CSS color string, gradient, pattern stroke(), strokeRect()
lineWidth 1 Positive number (pixels) All stroke operations
globalAlpha 1.0 Number between 0 and 1 All drawing operations

Drawing Rounded Rectangles

Plain sharp-cornered rectangles cover many use cases, but interfaces often call for rectangles with rounded corners — buttons, cards, badges, and tooltips all commonly use them. The Canvas API provides two approaches.

The modern approach uses the native roundRect() method, which was added to the specification and is supported in all current major browsers:

ctx.beginPath();
ctx.roundRect(50, 50, 200, 100, 20); // 20-pixel corner radius
ctx.fillStyle = 'cornflowerblue';
ctx.fill();
ctx.strokeStyle = 'midnightblue';
ctx.lineWidth = 3;
ctx.stroke();

The radii parameter (the fifth argument) can be a single number to apply uniformly to all four corners, an array of two numbers [horizontal, vertical] for elliptical corners, or an array of up to four values to control each corner independently — following the same top-right-bottom-left order as CSS border-radius.

For environments where roundRect() may not be available, or when you need finer control, you can build a rounded rectangle manually using the arcTo() method inside a path. The arcTo(x1, y1, x2, y2, radius) method draws an arc that is tangent to two lines: one from the current point to (x1, y1), and another from (x1, y1) to (x2, y2). It is ideal for creating smooth corners:

function drawRoundedRect(ctx, x, y, width, height, radius) {
  ctx.beginPath();
  ctx.moveTo(x + radius, y);                                      // Top edge start
  ctx.lineTo(x + width - radius, y);                             // Top edge end
  ctx.arcTo(x + width, y, x + width, y + radius, radius);        // Top-right corner
  ctx.lineTo(x + width, y + height - radius);                    // Right edge
  ctx.arcTo(x + width, y + height, x + width - radius, y + height, radius); // Bottom-right
  ctx.lineTo(x + radius, y + height);                            // Bottom edge
  ctx.arcTo(x, y + height, x, y + height - radius, radius);     // Bottom-left corner
  ctx.lineTo(x, y + radius);                                     // Left edge
  ctx.arcTo(x, y, x + radius, y, radius);                       // Top-left corner
  ctx.closePath();
}

drawRoundedRect(ctx, 50, 50, 200, 100, 20);
ctx.fillStyle = 'salmon';
ctx.fill();

Each call to arcTo() handles one corner of the rectangle by drawing a circular arc between two lines. The straight lineTo() calls connect the arcs along each edge. This pattern gives you full control over corner radii and can be adapted to round only specific corners by substituting a lineTo() for the arcTo() at corners you want to keep sharp.

Whether you use roundRect() or the manual arcTo() approach, the rendering step is the same: after defining the path, call fill(), stroke(), or both to make the shape visible on the canvas. Both shapes and styles together form the complete visual output — neither alone is sufficient.

NotesThe roundRect() method has broad modern browser support but was not available in older browsers. When teaching to mixed audiences it is worth noting the arcTo() fallback approach remains important. Also worth mentioning: ctx.save() and ctx.restore() can be introduced as a natural follow-up for managing style state across multiple shapes.