DEV Community

toolzip
toolzip

Posted on Originally published at toolzip.app

Everything You Can Do With Canvas API (That You Probably Didn't Know)

Most developers use Canvas API for one thing: drawing shapes or rendering charts. That's maybe 10% of what it can do.

Here's what else is possible — and how to use it for real-world file processing.

1. Image Compression

Canvas gives you direct control over JPEG quality when exporting.

function compressImage(file, quality = 0.85) {
  return new Promise((resolve) => {
    const img = new Image();
    const url = URL.createObjectURL(file);

    img.onload = () => {
      const canvas = document.createElement("canvas");
      canvas.width = img.naturalWidth;
      canvas.height = img.naturalHeight;

      const ctx = canvas.getContext("2d");

      // Fill white background for JPEG (no transparency)
      ctx.fillStyle = "#ffffff";
      ctx.fillRect(0, 0, canvas.width, canvas.height);

      ctx.drawImage(img, 0, 0);
      URL.revokeObjectURL(url);

      canvas.toBlob(resolve, "image/jpeg", quality);
    };

    img.src = url;
  });
}

// Usage
const blob = await compressImage(file, 0.8); // 80% quality
Enter fullscreen mode Exit fullscreen mode

The quality parameter (0-1) is only meaningful for JPEG and WebP. PNG is lossless — quality has no effect.

2. Format Conversion

async function convertFormat(file, targetFormat) {
  const mimeTypes = {
    jpg: "image/jpeg",
    png: "image/png",
    webp: "image/webp",
  };

  const img = await loadImage(file);
  const canvas = document.createElement("canvas");
  canvas.width = img.naturalWidth;
  canvas.height = img.naturalHeight;

  const ctx = canvas.getContext("2d");

  // White background needed when converting PNG with alpha to JPEG
  if (targetFormat === "jpg") {
    ctx.fillStyle = "#ffffff";
    ctx.fillRect(0, 0, canvas.width, canvas.height);
  }

  ctx.drawImage(img, 0, 0);

  return new Promise(resolve => {
    canvas.toBlob(resolve, mimeTypes[targetFormat], 0.92);
  });
}
Enter fullscreen mode Exit fullscreen mode

HEIC conversion is trickier — you need a library like heic2any to decode HEIC first, then Canvas handles the re-encoding.

3. Pixel-Level Manipulation

getImageData() gives you direct access to every pixel as RGBA values.

function applyGrayscale(canvas) {
  const ctx = canvas.getContext("2d");
  const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);
  const data = imageData.data; // Uint8ClampedArray: [R,G,B,A, R,G,B,A, ...]

  for (let i = 0; i < data.length; i += 4) {
    const r = data[i];
    const g = data[i + 1];
    const b = data[i + 2];
    // Luminance formula
    const gray = 0.299 * r + 0.587 * g + 0.114 * b;
    data[i] = data[i + 1] = data[i + 2] = gray;
    // data[i + 3] is alpha — leave it alone
  }

  ctx.putImageData(imageData, 0, 0);
}
Enter fullscreen mode Exit fullscreen mode

The same pattern works for:

  • Brightness/contrast adjustment
  • Channel separation (R, G, B, Alpha channels separately)
  • Color inversion
  • Threshold / posterization
  • Custom filters

4. Extracting Color Palettes

Sample pixels at regular intervals to find dominant colors.

function extractPalette(canvas, sampleRate = 10) {
  const ctx = canvas.getContext("2d");
  const { width, height } = canvas;
  const imageData = ctx.getImageData(0, 0, width, height);
  const data = imageData.data;

  const colorMap = new Map();

  for (let y = 0; y < height; y += sampleRate) {
    for (let x = 0; x < width; x += sampleRate) {
      const i = (y * width + x) * 4;
      const a = data[i + 3];
      if (a < 128) continue; // Skip transparent pixels

      // Quantize to reduce color count
      const r = Math.round(data[i] / 32) * 32;
      const g = Math.round(data[i + 1] / 32) * 32;
      const b = Math.round(data[i + 2] / 32) * 32;
      const key = `${r},${g},${b}`;

      colorMap.set(key, (colorMap.get(key) || 0) + 1);
    }
  }

  // Sort by frequency
  return [...colorMap.entries()]
    .sort((a, b) => b[1] - a[1])
    .slice(0, 10)
    .map(([key]) => {
      const [r, g, b] = key.split(",").map(Number);
      return { r, g, b, hex: `#${r.toString(16).padStart(2,"0")}${g.toString(16).padStart(2,"0")}${b.toString(16).padStart(2,"0")}` };
    });
}
Enter fullscreen mode Exit fullscreen mode

5. Reading Pixel Values on Click

canvas.addEventListener("click", (e) => {
  const rect = canvas.getBoundingClientRect();

  // Account for CSS scaling
  const scaleX = canvas.width / rect.width;
  const scaleY = canvas.height / rect.height;

  const x = Math.round((e.clientX - rect.left) * scaleX);
  const y = Math.round((e.clientY - rect.top) * scaleY);

  const ctx = canvas.getContext("2d");
  const pixel = ctx.getImageData(x, y, 1, 1).data;

  const r = pixel[0], g = pixel[1], b = pixel[2], a = pixel[3];
  const hex = `#${r.toString(16).padStart(2,"0")}${g.toString(16).padStart(2,"0")}${b.toString(16).padStart(2,"0")}`;

  console.log(`RGBA: ${r}, ${g}, ${b}, ${a}`);
  console.log(`HEX: ${hex}`);
});
Enter fullscreen mode Exit fullscreen mode

Critical gotcha: Always account for CSS scaling. If your canvas is 800px wide but displayed at 400px via CSS, clicking at pixel 200 in screen coordinates maps to pixel 400 in canvas coordinates. Forgetting this causes infuriating off-by-factor-of-two bugs.

6. Transparent Background Visualization

To show transparency like Photoshop's checkerboard:

function drawCheckerboard(ctx, width, height, size = 10) {
  for (let y = 0; y < height; y += size) {
    for (let x = 0; x < width; x += size) {
      ctx.fillStyle = ((x / size + y / size) % 2 === 0) ? "#cccccc" : "#ffffff";
      ctx.fillRect(x, y, size, size);
    }
  }
}

// Draw checkerboard first, then image on top
drawCheckerboard(ctx, canvas.width, canvas.height);
ctx.drawImage(img, 0, 0);
Enter fullscreen mode Exit fullscreen mode

7. Histogram Generation

function generateHistogram(canvas) {
  const ctx = canvas.getContext("2d");
  const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);
  const data = imageData.data;

  const histogram = {
    r: new Array(256).fill(0),
    g: new Array(256).fill(0),
    b: new Array(256).fill(0),
    luma: new Array(256).fill(0),
  };

  for (let i = 0; i < data.length; i += 4) {
    if (data[i + 3] < 128) continue; // Skip transparent
    histogram.r[data[i]]++;
    histogram.g[data[i + 1]]++;
    histogram.b[data[i + 2]]++;
    const luma = Math.round(0.299 * data[i] + 0.587 * data[i+1] + 0.114 * data[i+2]);
    histogram.luma[luma]++;
  }

  return histogram;
}
Enter fullscreen mode Exit fullscreen mode

Performance Considerations

getImageData is slow. It triggers a GPU-to-CPU transfer. For large images, do pixel operations in batches or use OffscreenCanvas in a Web Worker.

createImageBitmap is faster than new Image() for loading. Use it when processing multiple images.

willReadFrequently hint:

const ctx = canvas.getContext("2d", { willReadFrequently: true });
Enter fullscreen mode Exit fullscreen mode

This tells the browser to optimize for frequent getImageData calls. Makes a meaningful difference on large images.


Canvas API is the most underestimated browser API. For image processing that doesn't require AI or server resources, it handles most use cases.

These techniques power the image tools at ToolZip — browser-based file utilities.

Top comments (0)