Advanced

Drawing with Canvas

How does code become pixels? Learn the <canvas> element, its top-left origin with y growing downward, and paint shapes, colors, and text to build the game's title screen.

What you will see

clean slate 2 ENDLESS RUNNER 3 Best: 0 4 Press SPACE to start

About 5 min of reading

Try It

The canvas lesson shows code turning into game mechanics. I run small snippets and watch the exact output, then copy the same pattern the finished game ships.

1

Every canvas is a single sheet of paper where JavaScript draws one shape at a time. Its coordinate system starts at the origin (0, 0) in the top-left corner, x grows to the right, and y grows downward, the opposite of most math graphs. I print the origin and a corner so the direction of each axis is concrete.

const w = 960 const h = 540 console.log(0, 0) // top-left origin console.log(w, h) // bottom-right corner console.log(100 + 50, 400 + 30) // a rectangle's bottom-right edge

Before you run it, say out loud what it will print. Then open the answer.

What it prints
0 0 960 540 150 430
Because y grows downward, a rectangle at (100, 100, 50, 30) sits near the TOP of the screen, not the bottom. To place something near the bottom, use a large y like 500.
2

I paint to the canvas in the exact order the game's title screen works: set a color, then fillRect a solid rectangle that covers the whole screen. The origin (0, 0) plus the full width and height means "cover everything." I compute width / 2 for the horizontal center line so titles land in the middle.

const width = 960 const height = 540 const centerX = width / 2 console.log(centerX) console.log(Math.round(height / 2 - 80)) console.log(Math.round(height / 2 - 40)) console.log(Math.round(height / 2 + 10))

Before you run it, say out loud what it will print. Then open the answer.

What it prints
480 190 230 280
Every title line uses width / 2 as the x to stay centered. The three y values (190, 230, 280) are stacked vertically: subtract to move a line up, add to move it down, the game does exactly this for its title screen.
3

The game draws with a chain of fillText calls, each picking a color then painting one line. Template interpolation ${high || 0} injects the best score into the text: when the score is 0 the || falls back to 0. I run the interpolation and the vertical layout the game uses so the pattern is obvious.

let high = 0 console.log('ENDLESS RUNNER') console.log('Best: ' + (high || 0)) console.log('Press SPACE / \u2191 / W to start') console.log('center=' + Math.round(960 / 2), 'titleY=' + Math.round(540 / 2 - 80))

Before you run it, say out loud what it will print. Then open the answer.

What it prints
ENDLESS RUNNER Best: 0 Press SPACE / ↑ / W to start center=480 titleY=190
The ${high || 0} means "show high, but if high is 0 or missing show 0". fillText draws with x as the left edge, so centering relies on using width / 2 for every line. The y in fillText is the text baseline, which is why the game offsets it with -80.
4

Before anything is drawn, the game wipes the whole screen dark so the previous frame never lingers. It is a plain fillRect over the full canvas. Each subsequent shape uses whatever fillStyle is currently set, so I change the style first, then draw. I print the clean-slate rectangle and a colored box to see the two-call pattern.

console.log('#101018') // background color console.log(0, 0, 960, 540) // fillRect(0, 0, width, height), clean slate console.log('#ffd23f', 100, 400, 40, 56) // fillStyle, then a 40x56 box

Before you run it, say out loud what it will print. Then open the answer.

What it prints
#101018 0 0 960 540 #ffd23f 100 400 40 56
There is no erase on a canvas, you cover the old picture with a fresh background every frame. That is why the first fillRect each frame is the whole screen. Every shape then uses the current fillStyle, so a missing fillStyle call draws in the last color instead of black.
5

The finished game builds a title screen by stacking draw calls: clean slate, then color-line, color-line, color-line. I write that exact sequence and trace the numbers so the whole screen is clearly just a vertical stack of five calls. Removing the clean-slate step would smudge every frame into the next.

function drawTitle(ctx, width, height, high) { ctx.fillStyle = '#101018' ctx.fillRect(0, 0, width, height) // 1. clean slate ctx.fillStyle = '#f5f6fa' ctx.fillText('ENDLESS RUNNER', width / 2, height / 2 - 80) // 2 ctx.fillStyle = '#ffd23f' ctx.fillText('Best: ' + (high || 0), width / 2, height / 2 - 40) // 3 ctx.fillStyle = '#f5f6fa' ctx.fillText('Press SPACE to start', width / 2, height / 2 + 10) // 4 } drawTitle({}, 960, 540, 0)

Before you run it, say out loud what it will print. Then open the answer.

What it prints
clean slate 2 ENDLESS RUNNER 3 Best: 0 4 Press SPACE to start
The clean-slate fillRect must come first, or every previous frame smudges into the next. Each colored line is a separate fillStyle + fillText pair. The numbers 190, 230, 280 stack the three text lines down the middle of a 540-tall screen.

Put it in order

Before you write anything, put these lines back in the right order. Click a line to move it up one step. This support step is easier than writing code from scratch, and it is here to make the last example stick.

Data Flow

The HTML canvas provides a 2D context; each frame the code cleans the surface then paints shapes and text using the current style.
1 Grab from the HTML by its id
↓
2 Ask for a '2d' drawing context
↓
3 Each frame, fillRect the whole screen to clean it
↓
4 Set a fillStyle or font, then draw a shape or line of text
↓
5 Repeat for every layer until the frame is complete
Why this works
The full canvas drawing model

Think of the canvas as a single sheet of paper you repaint from scratch every frame. There is no "erase", you cover the old picture with a fresh background, then draw the new picture on top. This is why the "paint the whole screen black" step matters: without it, every previous frame would smudge into the next.

ctx.fillStyle = '#101018';
ctx.fillRect(0, 0, width, height);   // clean slate
// ...draw players, coins, hazards here...

The context remembers a handful of "current" settings, fillStyle, font, textAlign: and every draw call uses whatever is currently set. Change a setting, then draw. This is different from most graphics APIs that pass the style into each call.

The finished game also draws with gradients (createLinearGradient + addColorStop) for the sky and ground, but the model is identical: set a paint, then draw.
Text alignment and centering

fillText(text, x, y) places text with x as the left edge and y as the vertical middle. To center a title horizontally, either set ctx.textAlign = 'center' and use width / 2, or, like the real game does for its simple title, just use width / 2 as the x and read the text as anchored there.

ctx.textAlign = 'center';
ctx.font = '40px sans-serif';
ctx.fillText('ENDLESS RUNNER', width / 2, height / 2 - 80);

  • ctx.font sets the size and family, e.g. 28px sans-serif.
  • ctx.textAlign can be 'left', 'center', or 'right'.

The game fakes centering on its original title by centering the x and trusting the baseline, which is why every title line uses width / 2. When you're ready, textAlign = 'center' makes this exact.

Watch out: changing ctx.font affects every later fillText, so reset it between the title font and the HUD font.

Examples

Getting the drawing surface

The answer is the <canvas> element, an HTML box where JavaScript draws shapes, text, and images, one at a time. Our game's entire screen is one canvas, 960 × 540 pixels.

<canvas id="game" width="960" height="540" tabindex="0"></canvas>

JavaScript gets a tool to paint on it, called the context (written ctx):

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

  • document.getElementById('game'): grab the canvas from the HTML.
  • getContext('2d'): ask for the drawing tool. '2d' means we draw flat, 2-D shapes.

ctx is your paintbrush for the rest of this lesson.

The coordinate system (y grows downward)

Before drawing, you must understand where coordinates live. On the canvas:

  • x increases to the right.
  • y increases downward.
  • The origin (0, 0) is the top-left corner.

So to draw something near the bottom of a 540-tall screen, use a large y, like y = 500. A rectangle drawn at (100, 100, 50, 30) has its top-left corner at x=100, y=100.

ctx.fillStyle = '#ff0000';     // color of the fill
ctx.fillRect(100, 100, 50, 30); // x, y, width, height

  • fillStyle: what to paint the shape with (a color, gradient, or pattern).
  • fillRect(x, y, width, height): paint a solid rectangle.

Colors can be names ('red'), hex ('#ffd23f': the game's yellow, the player's color!), or rgba ('rgba(255, 0, 0, 0.5)'). Every fillRect uses the current fillStyle.

A real game screen: the title

This is verbatim the game's title screen, from app1/js/ui/Screens.js:

ctx.fillStyle = COLORS.text;
ctx.fillText('ENDLESS RUNNER', width / 2, height / 2 - 80);
ctx.fillStyle = COLORS.accent;
ctx.fillText(`Best: ${high || 0}`, width / 2, height / 2 - 40);
ctx.fillStyle = COLORS.text;
ctx.fillText('Press SPACE / ↑ / W to start', width / 2, height / 2 + 10);

Let me translate it:

  1. Pick a color, paint a title line, move down, pick another color, paint the next line.
  2. Repeat until the screen is filled.
  3. The ${high || 0} inside the quotes, template interpolation, puts the best score into the text.
  4. Everything is centered by using width / 2 as the x.

So the whole title screen is just: pick a color → write a line → repeat. It's 5 draw calls stacked vertically. And before anything is drawn, the game paints the whole screen dark to "clean" the previous frame so old drawings don't linger:

ctx.fillStyle = '#101018';
ctx.fillRect(0, 0, width, height);

Watch out: the y in fillText is the vertical middle of the text baseline, not the top, that's why the game offsets titles by -80.

Words this lesson uses

Each one is defined here on this site. Open the entry for the full definition and the other words it points at.

← State (State Machines)
Previous Lesson
State (State Machines)
Next Lesson
The Game Loop
The Game Loop →