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
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);
});
}
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);
}
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")}` };
});
}
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}`);
});
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);
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;
}
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 });
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.