How to fill shapes with color on canvas in JavaScript

How to fill shapes with color on canvas in JavaScript

Before you paint anything on the screen, you need to tell the browser where it’s going to happen. This is done by grabbing a reference to the canvas element from the DOM, then asking it for its drawing context. The context is essentially your bucket of paint – without it, you can’t begin to draw.

Here’s the basic pattern:

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

That ctx object is your gateway to everything you can draw: lines, shapes, images, text, gradients – basically anything that fits inside a rectangle.

One subtlety here is that if you forget to get the context, or if you try for something other than 2D (say, ‘webgl’ without setting up WebGL properly), you’ll end up with null. This causes errors down the line that aren’t always obvious – so always check:

if (!ctx) {
  throw new Error('Failed to get 2D context');
}

Also, the size of your canvas element matters. The width and height attributes on the canvas tag define the drawing surface size, not the CSS width and height. If you set the CSS size without adjusting the canvas attributes, you’ll get scaling artifacts.

Example:

<canvas id="myCanvas" width="800" height="600"></canvas>

If you only set style="width: 800px; height: 600px;" but leave the attributes at their default (300×150), your drawing surface is still 300 by 150 pixels internally, and the browser stretches it, which looks blurry.

So, always set the canvas size attributes to the actual pixel dimensions you intend to draw at, and then you can scale the canvas element with CSS if you want to handle high-DPI displays manually. This is crucial for sharp-looking graphics.

Now you just have to tell it what to paint

With the context object in hand, you’re ready to draw. The canvas API is built around the idea of paths. You first define a shape or a line by creating a path, and then you tell the browser what to do with that path-either draw its outline (stroke) or fill it in (fill). It’s a two-step process: define, then render.

Let’s start with the simplest thing possible: a straight line. You use moveTo(x, y) to position your virtual pen, and then lineTo(x, y) to trace a line from that starting point to a new one.

// Move the "pen" to the starting coordinates without drawing
ctx.moveTo(50, 50);

// Draw a line to the ending coordinates
ctx.lineTo(250, 150);

// Nothing appears on screen yet. Now, render the path we just defined.
ctx.stroke();

Notice that nothing happens until you call stroke(). You can define a complex path with hundreds of points, but until you call stroke() or fill(), the canvas remains stubbornly blank. This is a common source of confusion; you’ve written all the drawing code, but you forgot the final command to actually put the pixels on the screen.

For shapes, you can draw them line by line, or you can use built-in helper functions. The rect(x, y, width, height) function is a classic. It defines a rectangular path for you. Once defined, you can choose to fill it, stroke it, or both.

// Set the color for the inside of the shape
ctx.fillStyle = 'rgba(0, 0, 255, 0.5)'; // A semi-transparent blue

// Set the style for the border
ctx.strokeStyle = 'black';
ctx.lineWidth = 4;

// Define the rectangle's path
ctx.rect(100, 100, 300, 200);

// Fill the rectangle with the current fillStyle
ctx.fill();

// Draw the border with the current strokeStyle and lineWidth
ctx.stroke();

The context is a state machine. Properties like fillStyle, strokeStyle, and lineWidth are part of its current state. When you set ctx.fillStyle = 'blue', every subsequent call to fill() will use blue paint until you change the state again. This is efficient, but it’s also a trap. If you draw a blue rectangle and then forget to change the fillStyle before drawing your next shape, that shape will also be blue, which is probably not what you intended. You have to meticulously manage the state before each drawing operation.

The one big thing you’ll forget and why it will drive you crazy

So you’ve drawn your first shape, maybe a nice red rectangle. You feel a sense of accomplishment. Now, you want to draw a second shape, a blue line, somewhere else on the canvas. This seems easy enough. You just set the strokeStyle to blue, call moveTo and lineTo, and then stroke. You run the code, and your jaw drops. Not only is your new blue line there, but your original red rectangle now has a blue outline, and there’s a bizarre blue line connecting the corner of the rectangle to the start of your new line.

What on earth is going on? You check your code a dozen times. You set the color for the rectangle, you drew it. You set the color for the line, you drew it. The logic seems flawless. This, my friend, is the trap. The problem isn’t your logic; it’s that you forgot to tell the canvas context that you were finished with the first shape and starting a new one.

The one function you will forget, over and over, is beginPath(). Without it, the canvas context thinks every drawing command you issue is part of one single, gigantic, continuous path. When you call stroke() or fill(), it re-draws *everything* in that ever-growing path with the current styles.

Here’s the broken code. It’s trying to draw a filled red rectangle and then a separate blue line.

// Draw a red rectangle
ctx.fillStyle = 'red';
ctx.rect(10, 10, 100, 100);
ctx.fill();

// Now, try to draw a blue line
ctx.strokeStyle = 'blue';
ctx.moveTo(150, 50);
ctx.lineTo(250, 150);
ctx.stroke(); // This will re-stroke the rectangle too!

When the final stroke() is called, the path contains the rectangle from ctx.rect() AND the new line from moveTo/lineTo. The browser obediently strokes the entire thing in blue, creating a mess. The fix is to explicitly tell the browser to start a new path before you define the new shape.

// Draw a red rectangle
ctx.beginPath(); // Start path for the rectangle
ctx.fillStyle = 'red';
ctx.rect(10, 10, 100, 100);
ctx.fill();

// Now, draw a blue line
ctx.beginPath(); // <-- The crucial, forgotten line. Start a NEW path.
ctx.strokeStyle = 'blue';
ctx.moveTo(150, 50);
ctx.lineTo(250, 150);
ctx.stroke();

By calling beginPath(), you essentially throw away the old path definition (the rectangle is already drawn, so we don't need its path anymore) and start with a clean slate. Now, when you call stroke(), the only thing in the current path is the line you just defined. No more weird phantom shapes. You will forget this. It will drive you crazy. And then you will remember this rule: always call beginPath() before you start drawing a new thing.

This is different from ctx.save() and ctx.restore(). Those functions manage the entire state of the context-colors, line widths, transformations, clipping regions, everything-but they do *not* affect the current path. You use save() and restore() when you want to temporarily change styles, draw something, and then pop back to the previous styles. You use beginPath() to start a new shape. You will often use them together.

Comments

No comments yet. Why don’t you start the discussion?

Leave a Reply

Your email address will not be published. Required fields are marked *