Skip to main content
Technology & EngineeringDesign Tool Services220 lines

Canvas API

Draw graphics and generate images with the HTML Canvas 2D API and node-canvas.

Quick Summary32 lines
You are an expert in the Canvas 2D rendering context, both in the browser and on the server via `@napi-rs/canvas` or `canvas` (node-canvas). You create dynamic image generators, chart renderers, and pixel-manipulation utilities with precise control over drawing operations.

## Key Points

- **Forgetting `beginPath()` before new shapes** -- without it, `stroke()` redraws every previous path segment, causing visual artifacts and performance degradation.
- **Using Canvas for static SVG-like graphics in the browser** -- SVG is resolution-independent and accessible; Canvas is for dynamic or pixel-level work.
- **Reading `getImageData` in a hot loop** -- each call copies pixel data from GPU to CPU; batch reads and minimize calls.
- **Not handling DPI scaling** -- on HiDPI displays, set `canvas.width = displayWidth * devicePixelRatio` and scale the context, or output looks blurry.
- Generating dynamic OG images or social cards on the server at request time.
- Building custom chart or data visualization components beyond what charting libraries offer.
- Applying real-time image filters or overlays in the browser (e.g., photo editor).
- Creating certificate, badge, or label generators in a Node.js service.
- Rendering game graphics or interactive visualizations that need per-pixel control.

## Quick Example

```typescript
ctx.fillStyle = "red";
ctx.globalAlpha = 0.5;
drawHeader(ctx);   // inherits red + 0.5 alpha -- unintended
drawFooter(ctx);   // also inherits -- bugs compound
```

```typescript
function draw(ctx: CanvasRenderingContext2D, canvas: HTMLCanvasElement) {
  ctx.clearRect(0, 0, canvas.width, canvas.height);
  // ... draw frame
  requestAnimationFrame(() => draw(ctx, canvas));
}
```
skilldb get design-tool-services-skills/Canvas APIFull skill: 220 lines
Paste into your CLAUDE.md or agent config

HTML Canvas API & node-canvas

You are an expert in the Canvas 2D rendering context, both in the browser and on the server via @napi-rs/canvas or canvas (node-canvas). You create dynamic image generators, chart renderers, and pixel-manipulation utilities with precise control over drawing operations.

Core Philosophy

Immediate Mode Rendering

Canvas is an immediate-mode API: draw calls paint pixels onto a bitmap and are not retained as objects. Plan your rendering order carefully -- there is no layer reordering after the fact.

State Machine Discipline

The 2D context is a state machine. Every fillStyle, transform, or globalAlpha change persists until reset. Use save()/restore() religiously to avoid state leakage between drawing operations.

Server-Side Generation

Use @napi-rs/canvas (faster, no native deps on most platforms) or canvas (node-canvas, Cairo-based) to generate images in Node.js -- OG images, charts, certificates -- without a browser.

Setup

# Server-side (pick one)
npm install @napi-rs/canvas    # Rust-based, prebuilt binaries
npm install canvas              # C++-based, needs build tools

# Types for browser Canvas API
npm install -D @types/dom-webcodecs
// Server-side with @napi-rs/canvas
import { createCanvas, GlobalFonts } from "@napi-rs/canvas";

GlobalFonts.registerFromPath("./fonts/Inter-Bold.ttf", "Inter");

const canvas = createCanvas(1200, 630);
const ctx = canvas.getContext("2d");

Key Patterns

Do: Use save()/restore() around state changes

ctx.save();
ctx.fillStyle = "#6366f1";
ctx.globalAlpha = 0.8;
ctx.fillRect(0, 0, 200, 200);
ctx.restore();
// fillStyle and globalAlpha are back to previous values

Not: Mutate state without restoring

ctx.fillStyle = "red";
ctx.globalAlpha = 0.5;
drawHeader(ctx);   // inherits red + 0.5 alpha -- unintended
drawFooter(ctx);   // also inherits -- bugs compound

Do: Clear before redraw in animation loops

function draw(ctx: CanvasRenderingContext2D, canvas: HTMLCanvasElement) {
  ctx.clearRect(0, 0, canvas.width, canvas.height);
  // ... draw frame
  requestAnimationFrame(() => draw(ctx, canvas));
}

Common Patterns

Generate OG image on the server

import { createCanvas, GlobalFonts, loadImage } from "@napi-rs/canvas";

async function generateOGImage(title: string, author: string): Promise<Buffer> {
  const canvas = createCanvas(1200, 630);
  const ctx = canvas.getContext("2d");

  // Background gradient
  const grad = ctx.createLinearGradient(0, 0, 1200, 630);
  grad.addColorStop(0, "#1e1b4b");
  grad.addColorStop(1, "#312e81");
  ctx.fillStyle = grad;
  ctx.fillRect(0, 0, 1200, 630);

  // Title
  ctx.fillStyle = "#ffffff";
  ctx.font = 'bold 56px "Inter"';
  wrapText(ctx, title, 80, 200, 1040, 68);

  // Author
  ctx.fillStyle = "#a5b4fc";
  ctx.font = '28px "Inter"';
  ctx.fillText(author, 80, 520);

  return canvas.toBuffer("image/png");
}

function wrapText(
  ctx: CanvasRenderingContext2D,
  text: string,
  x: number, y: number,
  maxWidth: number, lineHeight: number
) {
  const words = text.split(" ");
  let line = "";
  let currentY = y;
  for (const word of words) {
    const test = line + word + " ";
    if (ctx.measureText(test).width > maxWidth && line) {
      ctx.fillText(line.trim(), x, currentY);
      line = word + " ";
      currentY += lineHeight;
    } else {
      line = test;
    }
  }
  ctx.fillText(line.trim(), x, currentY);
}

Draw a bar chart

interface BarData { label: string; value: number; color: string }

function drawBarChart(
  ctx: CanvasRenderingContext2D,
  data: BarData[],
  x: number, y: number,
  width: number, height: number
) {
  const maxVal = Math.max(...data.map((d) => d.value));
  const barWidth = width / data.length - 10;

  data.forEach((d, i) => {
    const barHeight = (d.value / maxVal) * height;
    const bx = x + i * (barWidth + 10);
    const by = y + height - barHeight;

    ctx.save();
    ctx.fillStyle = d.color;
    ctx.beginPath();
    ctx.roundRect(bx, by, barWidth, barHeight, [4, 4, 0, 0]);
    ctx.fill();

    ctx.fillStyle = "#64748b";
    ctx.font = "14px sans-serif";
    ctx.textAlign = "center";
    ctx.fillText(d.label, bx + barWidth / 2, y + height + 18);
    ctx.restore();
  });
}

Pixel manipulation for image filters

function grayscale(ctx: CanvasRenderingContext2D, w: number, h: number) {
  const imageData = ctx.getImageData(0, 0, w, h);
  const { data } = imageData;
  for (let i = 0; i < data.length; i += 4) {
    const avg = data[i] * 0.299 + data[i + 1] * 0.587 + data[i + 2] * 0.114;
    data[i] = data[i + 1] = data[i + 2] = avg;
  }
  ctx.putImageData(imageData, 0, 0);
}

Offscreen canvas for web workers

// main.ts
const offscreen = document.querySelector("canvas")!.transferControlToOffscreen();
const worker = new Worker("render-worker.ts", { type: "module" });
worker.postMessage({ canvas: offscreen }, [offscreen]);

// render-worker.ts
self.onmessage = (e: MessageEvent<{ canvas: OffscreenCanvas }>) => {
  const ctx = e.data.canvas.getContext("2d")!;
  ctx.fillStyle = "#6366f1";
  ctx.fillRect(0, 0, 400, 400);
};

Composite multiple images

import { createCanvas, loadImage } from "@napi-rs/canvas";

async function compositeImages(paths: string[], outputWidth: number) {
  const images = await Promise.all(paths.map(loadImage));
  const tileSize = Math.floor(outputWidth / images.length);
  const canvas = createCanvas(outputWidth, tileSize);
  const ctx = canvas.getContext("2d");

  images.forEach((img, i) => {
    ctx.drawImage(img, i * tileSize, 0, tileSize, tileSize);
  });

  return canvas.toBuffer("image/png");
}

Anti-Patterns

  • Forgetting beginPath() before new shapes -- without it, stroke() redraws every previous path segment, causing visual artifacts and performance degradation.
  • Using Canvas for static SVG-like graphics in the browser -- SVG is resolution-independent and accessible; Canvas is for dynamic or pixel-level work.
  • Reading getImageData in a hot loop -- each call copies pixel data from GPU to CPU; batch reads and minimize calls.
  • Not handling DPI scaling -- on HiDPI displays, set canvas.width = displayWidth * devicePixelRatio and scale the context, or output looks blurry.

When to Use

  • Generating dynamic OG images or social cards on the server at request time.
  • Building custom chart or data visualization components beyond what charting libraries offer.
  • Applying real-time image filters or overlays in the browser (e.g., photo editor).
  • Creating certificate, badge, or label generators in a Node.js service.
  • Rendering game graphics or interactive visualizations that need per-pixel control.

Install this skill directly: skilldb add design-tool-services-skills

Get CLI access →