Text & Images

Everything about rendering text, loading and drawing images, sprite sheets, pixel manipulation, and saving your canvas.

Lesson 4 of 10

Most real Canvas projects need more than shapes. Labels on a chart, scores in a game, watermarks on an image: text is everywhere. And when you need to display photos, sprites, or user uploads, the drawImage API handles it. This lesson covers both, plus pixel-level manipulation for when you need full control.

Drawing Text on Canvas

Canvas gives you two ways to render text. Think of it like writing on a whiteboard: you can write in solid marker (fill) or trace just the outlines (stroke).

fillText(text, x, y) draws solid, filled text. This is what you’ll use 90% of the time.

strokeText(text, x, y) draws only the outlines of each letter. Great for decorative effects.

Both methods place the text starting at (x, y), where y refers to the text baseline, the invisible line that letters “sit” on (like ruled paper).

                     x
                     │
y (baseline) ────────┼─── H e l l o ───
                     │        │
                Ascenders go up (h, l)
                Descenders go down (g, p, y)

fillText("Hello", x, y)
     The x,y is where the baseline starts
fillText vs strokeText paused

Try this: Change the font size in the ctx.font strings. Try '48px sans-serif' for large text, or 'bold 24px monospace' for a different family. Notice how Canvas text does not word-wrap; it just keeps going off the edge.

How Fonts Work in Canvas

The ctx.font property uses CSS font syntax, the same format you’d use in a stylesheet. It’s a string that combines style, weight, size, and family.

Font string format: “[style] [weight] size family”

ctx.font = "italic bold 24px serif"
            │      │    │    │
            │      │    │    └── family: serif, sans-serif,
            │      │    │        monospace, system-ui, or
            │      │    │        any loaded font name
            │      │    └─────── size: required! px, em, pt
            │      └──────────── weight: normal, bold, 100-900
            └─────────────────── style: normal, italic, oblique

Minimum required: size + family
ctx.font = "24px sans-serif"   // this is enough!

Important: Canvas can only use fonts that are already loaded by the browser. If you use a custom web font, it must be loaded via CSS @font-face before you try to draw with it. If the font isn’t loaded yet, Canvas silently falls back to a default font.

Font Styles, Weights, and Families paused

Text Alignment and Baseline

When you call fillText(“Hello”, 250, 100), where exactly does “Hello” appear relative to (250, 100)? That depends on two properties.

textAlign: horizontal position relative to the x coordinate:

"left"       "center"       "right"
 x│Hello      He│llo         Hello│x
 Text         Text is        Text ends
 starts       centered       at x
 at x         on x

textBaseline: vertical position relative to the y coordinate:

"top"          Text hangs below y  ────
"middle"       Text centered on y  ────
"alphabetic"   Baseline sits on y  ──── (default, most common)
"bottom"       Text sits above y   ────
textAlign and textBaseline Visualized paused

Measuring Text

ctx.measureText(text) returns a TextMetrics object that tells you how wide the text will be when drawn. This is essential for centering text, creating UI layouts, and wrapping text.

measureText: Width Calculation paused

Auto-Shrinking Text with maxWidth

Both fillText and strokeText accept an optional fourth argument: maxWidth. If the text would be wider than this value, the browser automatically squishes it to fit. This is like “shrink to fit” in a spreadsheet cell.

fillText maxWidth: Auto-Shrink paused

Text Wrapping: Canvas Doesn’t Do It!

Here’s a surprise for beginners: Canvas has no built-in text wrapping. If your text is longer than the canvas, it just keeps going off the right edge. To wrap text, you have to do it yourself using measureText().

The word-wrap algorithm:

1. Split text into words
2. Start with an empty line
3. For each word:
   ├─ Measure (current line + word) width
   ├─ If it fits within maxWidth: add word to current line
   └─ If it doesn't: draw current line, start new line with this word
4. Don't forget to draw the last line!

"The quick brown fox jumps over the lazy dog"
 maxWidth = 200px

 Line 1: "The quick brown"     (fits in 200px)
 Line 2: "fox jumps over"      (fits in 200px)
 Line 3: "the lazy dog"        (fits in 200px)
Manual Word Wrapping paused

Drawing Multiline Text

If your text contains newline characters (\n), Canvas ignores them completely. It draws everything on one line. You need to split by \n and draw each line separately.

Handling Newlines in Text paused

Practical: Game HUD / Scoreboard

One of the most common uses of Canvas text is building a heads-up display (HUD) for games. Let’s combine text alignment, measurement, and styling to build a polished scoreboard. This demo also uses createLinearGradient from Lesson 3 for the bar backgrounds.

Game HUD with Score, Health, Level paused

Drawing Images on Canvas

drawImage() is one of the most powerful Canvas methods. It can draw images, videos, and even other canvases onto your canvas. It has three forms with 3, 5, or 9 arguments.

The three forms of drawImage:

3-arg: drawImage(img, dx, dy)
       Draw at position, original size

5-arg: drawImage(img, dx, dy, dw, dh)
       Draw at position AND scale to dw x dh

9-arg: drawImage(img, sx, sy, sw, sh, dx, dy, dw, dh)
       Crop a region from source, then draw scaled

9-arg breakdown:
+──SOURCE IMAGE──+        +──YOUR CANVAS──+
│                │        │               │
│  (sx,sy)       │        │  (dx,dy)      │
│   +──────+     │  ───>  │   +──────+    │
│   │ sw   │     │        │   │ dw   │    │
│   │ x sh │     │        │   │ x dh │    │
│   +──────+     │        │   +──────+    │
│                │        │               │
+────────────────+        +───────────────+
 Crop this area            Paste it here
drawImage: 3, 5, and 9 Argument Forms paused

Loading Images the Right Way

In real projects, you load images from files or URLs. The critical thing to understand: images load asynchronously. If you try to draw an image before it’s loaded, nothing appears. There is no error, just silence.

The correct pattern:

var img = new Image();

// STEP 1: Set up onload BEFORE setting src
img.onload = function() {
  // STEP 3: Image is ready: NOW you can draw it
  ctx.drawImage(img, 0, 0);
};

img.onerror = function() {
  // Handle failures gracefully
  console.error('Image failed to load!');
};

// STEP 2: Setting src starts the download
img.src = 'path/to/image.png';

// WARNING: drawImage(img, 0, 0) here would draw NOTHING
// because the image hasn't loaded yet!

CORS (Cross-Origin Resource Sharing): If you load an image from a different domain, you may need to set img.crossOrigin = “anonymous” before setting src. Without this, getImageData() and toDataURL() will throw a security error (the canvas becomes “tainted”).

Image Loading Pattern with Error Handling paused

Sprite Sheets: How Games Use Images

A sprite sheet is a single image file containing many smaller images (sprites) arranged in a grid. Games use them because loading one big file is faster than loading hundreds of small files. The 9-argument drawImage is how you slice out individual sprites.

How sprite sheets work:

Sprite Sheet (one PNG file):
+────+────+────+────+
│ W1 │ W2 │ W3 │ W4 │  Row 0: Walking animation
+────+────+────+────+
│ R1 │ R2 │ R3 │ R4 │  Row 1: Running animation
+────+────+────+────+
│ J1 │ J2 │idle│hurt│  Row 2: Jump, idle, hurt
+────+────+────+────+
Each cell is 32x32 pixels

To get "R2" (running frame 2):
  row = 1, col = 1
  sx  = col * 32 = 32
  sy  = row * 32 = 32
  drawImage(sheet, 32, 32, 32, 32, dx, dy, 32, 32)
Sprite Sheet: Grid Extraction and Animation paused

Pixel Manipulation: Reading and Writing Pixels

Canvas lets you access individual pixels through ImageData objects. Each pixel is stored as four consecutive numbers (Red, Green, Blue, Alpha), each ranging from 0 to 255.

How pixel data is stored:

ImageData.data is a flat Uint8ClampedArray:
[ R, G, B, A,  R, G, B, A,  R, G, B, A, ... ]
  pixel 0       pixel 1       pixel 2

For a 3x2 image (3 wide, 2 tall):
  Row 0: [pix0] [pix1] [pix2]
  Row 1: [pix3] [pix4] [pix5]

To find pixel at (x, y):
  index = (y * width + x) * 4

  data[index]     = Red    (0-255)
  data[index + 1] = Green  (0-255)
  data[index + 2] = Blue   (0-255)
  data[index + 3] = Alpha  (0-255)  ← 255 = opaque, 0 = transparent

createImageData(w, h) creates blank (all zero/transparent) pixel data.

getImageData(x, y, w, h) reads pixel data from the canvas.

putImageData(data, x, y) writes pixel data back to the canvas.

Creating Pixel Art from Scratch paused

Practical Pixel Filters

By reading pixels with getImageData, modifying them, and writing them back with putImageData, you can implement image filters like grayscale, inversion, and brightness adjustment.

Image Filters: Grayscale, Invert, Brightness paused

Image Smoothing for Pixel Art

When you scale up a small image, the browser applies smoothing (anti-aliasing) by default. This makes photos look great but ruins pixel art, turning crisp pixels into blurry blobs. Toggle it off with imageSmoothingEnabled = false.

imageSmoothingEnabled: Pixel Art Scaling paused

Saving Your Canvas as an Image

canvas.toDataURL() converts the entire canvas into a data URL, a long string that represents the image. You can use this to create a download link, display the result in an <img> tag, or send it to a server.

toDataURL(type, quality)

canvas.toDataURL()                   // PNG (default)
canvas.toDataURL('image/png')        // PNG explicitly
canvas.toDataURL('image/jpeg', 0.8)  // JPEG at 80% quality
canvas.toDataURL('image/webp', 0.9)  // WebP at 90% quality

Returns a string like:
  "data:image/png;base64,iVBORw0KGgo..."
  (very long base64-encoded image data)

To create a download:
  var link = document.createElement('a');
  link.download = 'my-canvas.png';
  link.href = canvas.toDataURL();
  link.click();  // triggers download!

Warning: If you’ve drawn a cross-origin image (from another domain) without CORS headers, toDataURL() will throw a security error. The canvas is “tainted” and you can no longer export it.

toDataURL: Export and Preview paused

Putting It All Together: Mini Image Editor

Let’s combine text rendering, image creation, pixel manipulation, and filtering into an interactive mini image editor. Click the buttons below the canvas to apply different filters to the generated image. The addListener helper used below is provided by the demo framework to attach event listeners that are automatically cleaned up when the demo is removed.

Interactive Mini Image Editor paused

Lesson 4 Recap: What you learned.

Text: fillText/strokeText for rendering. ctx.font uses CSS syntax. textAlign and textBaseline for positioning. measureText() for width. maxWidth parameter for auto-shrinking.

Text challenges: Canvas has no built-in word wrap (implement manually with measureText). Newlines are ignored (split and draw line by line). Fonts must be pre-loaded.

Images: drawImage with 3, 5, or 9 arguments. Always use img.onload callback. Set crossOrigin for external images. Sprite sheets use the 9-arg form to extract frames.

Pixels: getImageData/putImageData/createImageData for pixel-level access. Each pixel = 4 bytes (RGBA). Index formula: (y * width + x) * 4.

Filters: Grayscale (luminance weighted), invert (255 - value), brightness (add), sepia (matrix), pixelate (block sampling).

Pixel art: imageSmoothingEnabled = false for crisp scaling.

Export: canvas.toDataURL() converts canvas to a downloadable image string.