Academic Block

HTML Canvas with JavaScript Get Certified

The HTML <canvas> element is a blank drawing surface that JavaScript controls entirely through code. On a real page, you select it and call canvas.getContext("2d") to start drawing shapes, lines, and text onto it.

This lesson uses the standards-based OffscreenCanvas API, which offers the same 2D drawing methods without a canvas element on the page, so every example performs genuine drawing operations. After drawing, we read the actual pixel data back with getImageData() and print the real colour values, proving exactly what was drawn. Below the text output, each example also shows the canvas itself with the sampled pixels marked, so you can match every number to a place in the picture. OffscreenCanvas needs a reasonably recent browser (for example Safari 16.4 or newer).

Creating a Canvas and Context

On a real page you would write const canvas = document.querySelector("canvas"); const ctx = canvas.getContext("2d");. Here, OffscreenCanvas gives us the same 2D context without needing a canvas element on the page. Every example then displays a copy of its canvas below the text output, with the sampled pixels marked.

What this code does: Creates a 200×100 canvas, gets its 2D rendering context (the object that holds all the drawing methods) and prints the canvas size, the type of the context and the number of pixels. Nothing has been drawn, so the picture below the text output is a checkerboard: that is how transparent pixels are shown. A new canvas always starts fully transparent.

// On a real page: const canvas = document.querySelector("canvas");
const canvas = new OffscreenCanvas(200, 100);

// The 2D context holds every drawing method (fillRect, arc, stroke ...)
const ctx = canvas.getContext("2d");

console.log("Canvas size:", canvas.width + "x" + canvas.height);
console.log("Context type:", ctx.constructor.name);
console.log("Total pixels:", canvas.width * canvas.height);

// A new canvas is blank (fully transparent), so it shows as a checkerboard
showCanvas("A new, empty 200x100 canvas", canvas);

// ===== Helpers below are not part of the lesson =====
function showPreview(label, node) {
    setTimeout(() => {
        const box = document.createElement("div");
        box.className = "demo-preview-box";
        box.innerHTML = "<p class='demo-preview-label'>" + label + "</p>";
        box.appendChild(node);
        document.getElementById("previewContent").appendChild(box);
    }, 0);
}

function showCanvas(label, source, options) {
    const scale = (options && options.scale) || 1;
    const marks = (options && options.marks) || [];

    const copy = document.createElement("canvas");
    copy.width = source.width;
    copy.height = source.height;
    copy.getContext("2d").drawImage(source, 0, 0);
    copy.style.cssText = "display:block;width:" + source.width * scale + "px;height:" + source.height * scale + "px;image-rendering:pixelated;background:repeating-conic-gradient(#e5e7eb 0% 25%, #ffffff 0% 50%) 50% / 16px 16px;";

    const wrap = document.createElement("div");
    wrap.className = "demo-canvas-wrap";
    wrap.style.cssText = "position:relative;display:inline-block;";
    wrap.appendChild(copy);

    marks.forEach((m) => {
        const ring = document.createElement("span");
        ring.className = "demo-canvas-mark";
        ring.style.cssText = "position:absolute;box-sizing:border-box;width:12px;height:12px;border:2px solid #d946ef;border-radius:50%;left:" + ((m.x + 0.5) * scale - 6) + "px;top:" + ((m.y + 0.5) * scale - 6) + "px;";
        ring.innerHTML = "<b style='position:absolute;left:12px;top:-4px;font:bold 12px sans-serif;color:#a21caf'>" + m.name + "</b>";
        wrap.appendChild(ring);
    });

    showPreview(label, wrap);
}

Drawing a Rectangle — and Proving It

After filling a rectangle, getImageData(x, y, 1, 1) reads the real color of a single pixel at that position, as an [r, g, b, a] array with values from 0–255.

What this code does: Draws a filled red rectangle, then reads one pixel from the middle of it with getImageData() and prints its [r, g, b, a] values, which should be [255, 0, 0, 255]: full red, no green or blue, fully opaque. The picture shows the rectangle with a ring marking exactly which pixel was read, and a colour box shows the value that came back. This proves the colour was really written to the canvas.

const canvas = new OffscreenCanvas(200, 100);
const ctx = canvas.getContext("2d");

ctx.fillStyle = "red";
ctx.fillRect(10, 10, 50, 50);

const pixel = ctx.getImageData(30, 30, 1, 1).data;

console.log("Pixel A at (30, 30), inside the red rectangle:", Array.from(pixel));

showCanvas("The rectangle and the pixel we read", canvas, { marks: [{ x: 30, y: 30, name: "A" }] });
showSwatches("Colour read back from pixel A", [{ name: "A", rgba: pixel }]);

// ===== Helpers below are not part of the lesson =====
function showPreview(label, node) {
    setTimeout(() => {
        const box = document.createElement("div");
        box.className = "demo-preview-box";
        box.innerHTML = "<p class='demo-preview-label'>" + label + "</p>";
        box.appendChild(node);
        document.getElementById("previewContent").appendChild(box);
    }, 0);
}

function showCanvas(label, source, options) {
    const scale = (options && options.scale) || 1;
    const marks = (options && options.marks) || [];

    const copy = document.createElement("canvas");
    copy.width = source.width;
    copy.height = source.height;
    copy.getContext("2d").drawImage(source, 0, 0);
    copy.style.cssText = "display:block;width:" + source.width * scale + "px;height:" + source.height * scale + "px;image-rendering:pixelated;background:repeating-conic-gradient(#e5e7eb 0% 25%, #ffffff 0% 50%) 50% / 16px 16px;";

    const wrap = document.createElement("div");
    wrap.className = "demo-canvas-wrap";
    wrap.style.cssText = "position:relative;display:inline-block;";
    wrap.appendChild(copy);

    marks.forEach((m) => {
        const ring = document.createElement("span");
        ring.className = "demo-canvas-mark";
        ring.style.cssText = "position:absolute;box-sizing:border-box;width:12px;height:12px;border:2px solid #d946ef;border-radius:50%;left:" + ((m.x + 0.5) * scale - 6) + "px;top:" + ((m.y + 0.5) * scale - 6) + "px;";
        ring.innerHTML = "<b style='position:absolute;left:12px;top:-4px;font:bold 12px sans-serif;color:#a21caf'>" + m.name + "</b>";
        wrap.appendChild(ring);
    });

    showPreview(label, wrap);
}

function showSwatches(label, samples) {
    const row = document.createElement("div");
    row.className = "demo-swatch-row";
    row.style.cssText = "display:flex;flex-wrap:wrap;gap:8px;font:12px monospace;color:#0f172a;";

    samples.forEach((s) => {
        const item = document.createElement("div");
        item.className = "demo-swatch";
        const color = "rgba(" + s.rgba[0] + "," + s.rgba[1] + "," + s.rgba[2] + "," + s.rgba[3] / 255 + ")";
        item.innerHTML = "<div style='width:96px;height:36px;border:1px solid #94a3b8;background:repeating-conic-gradient(#e5e7eb 0% 25%, #ffffff 0% 50%) 50% / 12px 12px'>"
            + "<div style='width:100%;height:100%;background:" + color + "'></div></div>"
            + s.name + ": " + Array.from(s.rgba).join(", ");
        row.appendChild(item);
    });

    showPreview(label, row);
}

Comparing a Pixel Inside vs. Outside a Shape

A blank canvas starts fully transparent, so a pixel outside any drawn shape reads back as [0, 0, 0, 0].

What this code does: Draws the same red rectangle, then reads one pixel inside it (A) and one outside it (B). Pixel A comes back as opaque red, [255, 0, 0, 255], and pixel B as [0, 0, 0, 0] because nothing was ever drawn there. The picture marks both pixels and two colour boxes show the two results (the empty one appears as the checkerboard).

const canvas = new OffscreenCanvas(200, 100);
const ctx = canvas.getContext("2d");

ctx.fillStyle = "red";
ctx.fillRect(10, 10, 50, 50);

const insidePixel = ctx.getImageData(30, 30, 1, 1).data;
const outsidePixel = ctx.getImageData(150, 30, 1, 1).data;

console.log("A, inside the rectangle:", Array.from(insidePixel));
console.log("B, outside the rectangle:", Array.from(outsidePixel));

showCanvas("Pixels A and B", canvas, { marks: [{ x: 30, y: 30, name: "A" }, { x: 150, y: 30, name: "B" }] });
showSwatches("Colours read back", [{ name: "A", rgba: insidePixel }, { name: "B", rgba: outsidePixel }]);

// ===== Helpers below are not part of the lesson =====
function showPreview(label, node) {
    setTimeout(() => {
        const box = document.createElement("div");
        box.className = "demo-preview-box";
        box.innerHTML = "<p class='demo-preview-label'>" + label + "</p>";
        box.appendChild(node);
        document.getElementById("previewContent").appendChild(box);
    }, 0);
}

function showCanvas(label, source, options) {
    const scale = (options && options.scale) || 1;
    const marks = (options && options.marks) || [];

    const copy = document.createElement("canvas");
    copy.width = source.width;
    copy.height = source.height;
    copy.getContext("2d").drawImage(source, 0, 0);
    copy.style.cssText = "display:block;width:" + source.width * scale + "px;height:" + source.height * scale + "px;image-rendering:pixelated;background:repeating-conic-gradient(#e5e7eb 0% 25%, #ffffff 0% 50%) 50% / 16px 16px;";

    const wrap = document.createElement("div");
    wrap.className = "demo-canvas-wrap";
    wrap.style.cssText = "position:relative;display:inline-block;";
    wrap.appendChild(copy);

    marks.forEach((m) => {
        const ring = document.createElement("span");
        ring.className = "demo-canvas-mark";
        ring.style.cssText = "position:absolute;box-sizing:border-box;width:12px;height:12px;border:2px solid #d946ef;border-radius:50%;left:" + ((m.x + 0.5) * scale - 6) + "px;top:" + ((m.y + 0.5) * scale - 6) + "px;";
        ring.innerHTML = "<b style='position:absolute;left:12px;top:-4px;font:bold 12px sans-serif;color:#a21caf'>" + m.name + "</b>";
        wrap.appendChild(ring);
    });

    showPreview(label, wrap);
}

function showSwatches(label, samples) {
    const row = document.createElement("div");
    row.className = "demo-swatch-row";
    row.style.cssText = "display:flex;flex-wrap:wrap;gap:8px;font:12px monospace;color:#0f172a;";

    samples.forEach((s) => {
        const item = document.createElement("div");
        item.className = "demo-swatch";
        const color = "rgba(" + s.rgba[0] + "," + s.rgba[1] + "," + s.rgba[2] + "," + s.rgba[3] / 255 + ")";
        item.innerHTML = "<div style='width:96px;height:36px;border:1px solid #94a3b8;background:repeating-conic-gradient(#e5e7eb 0% 25%, #ffffff 0% 50%) 50% / 12px 12px'>"
            + "<div style='width:100%;height:100%;background:" + color + "'></div></div>"
            + s.name + ": " + Array.from(s.rgba).join(", ");
        row.appendChild(item);
    });

    showPreview(label, row);
}

Drawing a Circle

arc() draws circles and arcs. Sampling the exact center confirms the fill color landed where expected.

What this code does: Draws a blue filled circle with arc() and then reads two pixels: the centre of the circle (A), which should be opaque blue, [0, 0, 255, 255], and a pixel near the top-left corner (B), which is outside the circle and should be transparent. The picture shows the circle with both pixels marked.

const canvas = new OffscreenCanvas(200, 200);
const ctx = canvas.getContext("2d");

ctx.beginPath();
ctx.arc(100, 100, 50, 0, Math.PI * 2);
ctx.fillStyle = "blue";
ctx.fill();

const center = ctx.getImageData(100, 100, 1, 1).data;
const corner = ctx.getImageData(5, 5, 1, 1).data;

console.log("A, center of the circle:", Array.from(center));
console.log("B, corner (outside the circle):", Array.from(corner));

showCanvas("Circle with sampled pixels", canvas, { marks: [{ x: 100, y: 100, name: "A" }, { x: 5, y: 5, name: "B" }] });
showSwatches("Colours read back", [{ name: "A", rgba: center }, { name: "B", rgba: corner }]);

// ===== Helpers below are not part of the lesson =====
function showPreview(label, node) {
    setTimeout(() => {
        const box = document.createElement("div");
        box.className = "demo-preview-box";
        box.innerHTML = "<p class='demo-preview-label'>" + label + "</p>";
        box.appendChild(node);
        document.getElementById("previewContent").appendChild(box);
    }, 0);
}

function showCanvas(label, source, options) {
    const scale = (options && options.scale) || 1;
    const marks = (options && options.marks) || [];

    const copy = document.createElement("canvas");
    copy.width = source.width;
    copy.height = source.height;
    copy.getContext("2d").drawImage(source, 0, 0);
    copy.style.cssText = "display:block;width:" + source.width * scale + "px;height:" + source.height * scale + "px;image-rendering:pixelated;background:repeating-conic-gradient(#e5e7eb 0% 25%, #ffffff 0% 50%) 50% / 16px 16px;";

    const wrap = document.createElement("div");
    wrap.className = "demo-canvas-wrap";
    wrap.style.cssText = "position:relative;display:inline-block;";
    wrap.appendChild(copy);

    marks.forEach((m) => {
        const ring = document.createElement("span");
        ring.className = "demo-canvas-mark";
        ring.style.cssText = "position:absolute;box-sizing:border-box;width:12px;height:12px;border:2px solid #d946ef;border-radius:50%;left:" + ((m.x + 0.5) * scale - 6) + "px;top:" + ((m.y + 0.5) * scale - 6) + "px;";
        ring.innerHTML = "<b style='position:absolute;left:12px;top:-4px;font:bold 12px sans-serif;color:#a21caf'>" + m.name + "</b>";
        wrap.appendChild(ring);
    });

    showPreview(label, wrap);
}

function showSwatches(label, samples) {
    const row = document.createElement("div");
    row.className = "demo-swatch-row";
    row.style.cssText = "display:flex;flex-wrap:wrap;gap:8px;font:12px monospace;color:#0f172a;";

    samples.forEach((s) => {
        const item = document.createElement("div");
        item.className = "demo-swatch";
        const color = "rgba(" + s.rgba[0] + "," + s.rgba[1] + "," + s.rgba[2] + "," + s.rgba[3] / 255 + ")";
        item.innerHTML = "<div style='width:96px;height:36px;border:1px solid #94a3b8;background:repeating-conic-gradient(#e5e7eb 0% 25%, #ffffff 0% 50%) 50% / 12px 12px'>"
            + "<div style='width:100%;height:100%;background:" + color + "'></div></div>"
            + s.name + ": " + Array.from(s.rgba).join(", ");
        row.appendChild(item);
    });

    showPreview(label, row);
}

Clearing Part of the Canvas

clearRect() genuinely erases pixels back to transparent. You can confirm this by reading the alpha channel before and after.

What this code does: Fills a 100×100 canvas with green, reads a pixel in the middle (A), then erases a 40×40 square with clearRect() and reads the same pixel again. Before clearing, A is opaque green [0, 128, 0, 255]; after clearing it is [0, 0, 0, 0]. A second pixel (B) outside the erased square stays green. The output shows the canvas before and after, so you can see the hole appear.

const canvas = new OffscreenCanvas(100, 100);
const ctx = canvas.getContext("2d");

ctx.fillStyle = "green";
ctx.fillRect(0, 0, 100, 100);

const marks = [{ x: 50, y: 50, name: "A" }, { x: 5, y: 5, name: "B" }];

console.log("A before clear:", Array.from(ctx.getImageData(50, 50, 1, 1).data));
showCanvas("Before clearRect: all green", canvas, { scale: 3, marks: marks });

ctx.clearRect(20, 20, 40, 40);

console.log("A after clear (inside the cleared area):", Array.from(ctx.getImageData(50, 50, 1, 1).data));
console.log("B after clear (outside the cleared area):", Array.from(ctx.getImageData(5, 5, 1, 1).data));
showCanvas("After clearRect(20, 20, 40, 40)", canvas, { scale: 3, marks: marks });

// ===== Helpers below are not part of the lesson =====
function showPreview(label, node) {
    setTimeout(() => {
        const box = document.createElement("div");
        box.className = "demo-preview-box";
        box.innerHTML = "<p class='demo-preview-label'>" + label + "</p>";
        box.appendChild(node);
        document.getElementById("previewContent").appendChild(box);
    }, 0);
}

function showCanvas(label, source, options) {
    const scale = (options && options.scale) || 1;
    const marks = (options && options.marks) || [];

    const copy = document.createElement("canvas");
    copy.width = source.width;
    copy.height = source.height;
    copy.getContext("2d").drawImage(source, 0, 0);
    copy.style.cssText = "display:block;width:" + source.width * scale + "px;height:" + source.height * scale + "px;image-rendering:pixelated;background:repeating-conic-gradient(#e5e7eb 0% 25%, #ffffff 0% 50%) 50% / 16px 16px;";

    const wrap = document.createElement("div");
    wrap.className = "demo-canvas-wrap";
    wrap.style.cssText = "position:relative;display:inline-block;";
    wrap.appendChild(copy);

    marks.forEach((m) => {
        const ring = document.createElement("span");
        ring.className = "demo-canvas-mark";
        ring.style.cssText = "position:absolute;box-sizing:border-box;width:12px;height:12px;border:2px solid #d946ef;border-radius:50%;left:" + ((m.x + 0.5) * scale - 6) + "px;top:" + ((m.y + 0.5) * scale - 6) + "px;";
        ring.innerHTML = "<b style='position:absolute;left:12px;top:-4px;font:bold 12px sans-serif;color:#a21caf'>" + m.name + "</b>";
        wrap.appendChild(ring);
    });

    showPreview(label, wrap);
}

Drawing a Line

A stroked line only colors the pixels directly along its path. Sampling just above or below it should still be transparent.

What this code does: Draws a 4-pixel-wide black horizontal line from (10, 50) to (90, 50) using moveTo, lineTo and stroke, then reads one pixel on the line (A) and one well above it (B). A is opaque black, [0, 0, 0, 255]; B is transparent. The picture is enlarged three times so the thin line and both sample points are easy to see.

const canvas = new OffscreenCanvas(100, 100);
const ctx = canvas.getContext("2d");

ctx.strokeStyle = "black";
ctx.lineWidth = 4;

ctx.beginPath();
ctx.moveTo(10, 50);
ctx.lineTo(90, 50);
ctx.stroke();

const onLine = ctx.getImageData(50, 50, 1, 1).data;
const awayFromLine = ctx.getImageData(50, 10, 1, 1).data;

console.log("A, on the line:", Array.from(onLine));
console.log("B, away from the line:", Array.from(awayFromLine));

showCanvas("A line and two sampled pixels", canvas, { scale: 3, marks: [{ x: 50, y: 50, name: "A" }, { x: 50, y: 10, name: "B" }] });
showSwatches("Colours read back", [{ name: "A", rgba: onLine }, { name: "B", rgba: awayFromLine }]);

// ===== Helpers below are not part of the lesson =====
function showPreview(label, node) {
    setTimeout(() => {
        const box = document.createElement("div");
        box.className = "demo-preview-box";
        box.innerHTML = "<p class='demo-preview-label'>" + label + "</p>";
        box.appendChild(node);
        document.getElementById("previewContent").appendChild(box);
    }, 0);
}

function showCanvas(label, source, options) {
    const scale = (options && options.scale) || 1;
    const marks = (options && options.marks) || [];

    const copy = document.createElement("canvas");
    copy.width = source.width;
    copy.height = source.height;
    copy.getContext("2d").drawImage(source, 0, 0);
    copy.style.cssText = "display:block;width:" + source.width * scale + "px;height:" + source.height * scale + "px;image-rendering:pixelated;background:repeating-conic-gradient(#e5e7eb 0% 25%, #ffffff 0% 50%) 50% / 16px 16px;";

    const wrap = document.createElement("div");
    wrap.className = "demo-canvas-wrap";
    wrap.style.cssText = "position:relative;display:inline-block;";
    wrap.appendChild(copy);

    marks.forEach((m) => {
        const ring = document.createElement("span");
        ring.className = "demo-canvas-mark";
        ring.style.cssText = "position:absolute;box-sizing:border-box;width:12px;height:12px;border:2px solid #d946ef;border-radius:50%;left:" + ((m.x + 0.5) * scale - 6) + "px;top:" + ((m.y + 0.5) * scale - 6) + "px;";
        ring.innerHTML = "<b style='position:absolute;left:12px;top:-4px;font:bold 12px sans-serif;color:#a21caf'>" + m.name + "</b>";
        wrap.appendChild(ring);
    });

    showPreview(label, wrap);
}

function showSwatches(label, samples) {
    const row = document.createElement("div");
    row.className = "demo-swatch-row";
    row.style.cssText = "display:flex;flex-wrap:wrap;gap:8px;font:12px monospace;color:#0f172a;";

    samples.forEach((s) => {
        const item = document.createElement("div");
        item.className = "demo-swatch";
        const color = "rgba(" + s.rgba[0] + "," + s.rgba[1] + "," + s.rgba[2] + "," + s.rgba[3] / 255 + ")";
        item.innerHTML = "<div style='width:96px;height:36px;border:1px solid #94a3b8;background:repeating-conic-gradient(#e5e7eb 0% 25%, #ffffff 0% 50%) 50% / 12px 12px'>"
            + "<div style='width:100%;height:100%;background:" + color + "'></div></div>"
            + s.name + ": " + Array.from(s.rgba).join(", ");
        row.appendChild(item);
    });

    showPreview(label, row);
}

globalAlpha and Real Color Blending

globalAlpha makes everything drawn afterward partially transparent. Reading the resulting pixel shows the actual blended color, not just an approximation.

What this code does: Fills the canvas with white, then draws a black square at 50% transparency (globalAlpha = 0.5) over the middle. A pixel inside the square (A) is a blend of black and white, around [128, 128, 128, 255], a mid-gray; a pixel outside it (B) is still pure white, [255, 255, 255, 255]. The picture shows the gray square on the white background and the colour boxes show both values.

const canvas = new OffscreenCanvas(100, 100);
const ctx = canvas.getContext("2d");

ctx.fillStyle = "white";
ctx.fillRect(0, 0, 100, 100);

ctx.globalAlpha = 0.5;
ctx.fillStyle = "black";
ctx.fillRect(20, 20, 60, 60);
ctx.globalAlpha = 1; // reset so later drawing is fully opaque again

const blended = ctx.getImageData(50, 50, 1, 1).data;
const plain = ctx.getImageData(5, 5, 1, 1).data;

console.log("A, 50% black over white:", Array.from(blended));
console.log("B, plain white background:", Array.from(plain));

showCanvas("Half-transparent black square over white", canvas, { scale: 3, marks: [{ x: 50, y: 50, name: "A" }, { x: 5, y: 5, name: "B" }] });
showSwatches("Colours read back", [{ name: "A", rgba: blended }, { name: "B", rgba: plain }]);

// ===== Helpers below are not part of the lesson =====
function showPreview(label, node) {
    setTimeout(() => {
        const box = document.createElement("div");
        box.className = "demo-preview-box";
        box.innerHTML = "<p class='demo-preview-label'>" + label + "</p>";
        box.appendChild(node);
        document.getElementById("previewContent").appendChild(box);
    }, 0);
}

function showCanvas(label, source, options) {
    const scale = (options && options.scale) || 1;
    const marks = (options && options.marks) || [];

    const copy = document.createElement("canvas");
    copy.width = source.width;
    copy.height = source.height;
    copy.getContext("2d").drawImage(source, 0, 0);
    copy.style.cssText = "display:block;width:" + source.width * scale + "px;height:" + source.height * scale + "px;image-rendering:pixelated;background:repeating-conic-gradient(#e5e7eb 0% 25%, #ffffff 0% 50%) 50% / 16px 16px;";

    const wrap = document.createElement("div");
    wrap.className = "demo-canvas-wrap";
    wrap.style.cssText = "position:relative;display:inline-block;";
    wrap.appendChild(copy);

    marks.forEach((m) => {
        const ring = document.createElement("span");
        ring.className = "demo-canvas-mark";
        ring.style.cssText = "position:absolute;box-sizing:border-box;width:12px;height:12px;border:2px solid #d946ef;border-radius:50%;left:" + ((m.x + 0.5) * scale - 6) + "px;top:" + ((m.y + 0.5) * scale - 6) + "px;";
        ring.innerHTML = "<b style='position:absolute;left:12px;top:-4px;font:bold 12px sans-serif;color:#a21caf'>" + m.name + "</b>";
        wrap.appendChild(ring);
    });

    showPreview(label, wrap);
}

function showSwatches(label, samples) {
    const row = document.createElement("div");
    row.className = "demo-swatch-row";
    row.style.cssText = "display:flex;flex-wrap:wrap;gap:8px;font:12px monospace;color:#0f172a;";

    samples.forEach((s) => {
        const item = document.createElement("div");
        item.className = "demo-swatch";
        const color = "rgba(" + s.rgba[0] + "," + s.rgba[1] + "," + s.rgba[2] + "," + s.rgba[3] / 255 + ")";
        item.innerHTML = "<div style='width:96px;height:36px;border:1px solid #94a3b8;background:repeating-conic-gradient(#e5e7eb 0% 25%, #ffffff 0% 50%) 50% / 12px 12px'>"
            + "<div style='width:100%;height:100%;background:" + color + "'></div></div>"
            + s.name + ": " + Array.from(s.rgba).join(", ");
        row.appendChild(item);
    });

    showPreview(label, row);
}

The result for pixel A lands around [128, 128, 128, 255], a genuine mid-gray produced by real alpha blending, not a hardcoded guess. The exact value can differ by one between browsers because of rounding.

Draw Order Matters

Shapes drawn later appear on top of shapes drawn earlier, exactly like layers. Overlapping regions take on the most recently drawn color.

What this code does: Draws a red square, then a blue square that overlaps its lower-right corner. Pixel A is in the red-only area and reads [255, 0, 0, 255]; pixel B is in the overlap and reads blue, [0, 0, 255, 255], because the blue square was drawn last and covers the red. Swap the order of the two drawing blocks and run again to see the overlap turn red.

const canvas = new OffscreenCanvas(100, 100);
const ctx = canvas.getContext("2d");

ctx.fillStyle = "red";
ctx.fillRect(0, 0, 60, 60);

ctx.fillStyle = "blue";
ctx.fillRect(40, 40, 60, 60);

const redOnly = ctx.getImageData(10, 10, 1, 1).data;
const overlap = ctx.getImageData(50, 50, 1, 1).data;

console.log("A, non-overlapping area (red only):", Array.from(redOnly));
console.log("B, overlap area (blue was drawn last):", Array.from(overlap));

showCanvas("Blue drawn after red", canvas, { scale: 3, marks: [{ x: 10, y: 10, name: "A" }, { x: 50, y: 50, name: "B" }] });
showSwatches("Colours read back", [{ name: "A", rgba: redOnly }, { name: "B", rgba: overlap }]);

// ===== Helpers below are not part of the lesson =====
function showPreview(label, node) {
    setTimeout(() => {
        const box = document.createElement("div");
        box.className = "demo-preview-box";
        box.innerHTML = "<p class='demo-preview-label'>" + label + "</p>";
        box.appendChild(node);
        document.getElementById("previewContent").appendChild(box);
    }, 0);
}

function showCanvas(label, source, options) {
    const scale = (options && options.scale) || 1;
    const marks = (options && options.marks) || [];

    const copy = document.createElement("canvas");
    copy.width = source.width;
    copy.height = source.height;
    copy.getContext("2d").drawImage(source, 0, 0);
    copy.style.cssText = "display:block;width:" + source.width * scale + "px;height:" + source.height * scale + "px;image-rendering:pixelated;background:repeating-conic-gradient(#e5e7eb 0% 25%, #ffffff 0% 50%) 50% / 16px 16px;";

    const wrap = document.createElement("div");
    wrap.className = "demo-canvas-wrap";
    wrap.style.cssText = "position:relative;display:inline-block;";
    wrap.appendChild(copy);

    marks.forEach((m) => {
        const ring = document.createElement("span");
        ring.className = "demo-canvas-mark";
        ring.style.cssText = "position:absolute;box-sizing:border-box;width:12px;height:12px;border:2px solid #d946ef;border-radius:50%;left:" + ((m.x + 0.5) * scale - 6) + "px;top:" + ((m.y + 0.5) * scale - 6) + "px;";
        ring.innerHTML = "<b style='position:absolute;left:12px;top:-4px;font:bold 12px sans-serif;color:#a21caf'>" + m.name + "</b>";
        wrap.appendChild(ring);
    });

    showPreview(label, wrap);
}

function showSwatches(label, samples) {
    const row = document.createElement("div");
    row.className = "demo-swatch-row";
    row.style.cssText = "display:flex;flex-wrap:wrap;gap:8px;font:12px monospace;color:#0f172a;";

    samples.forEach((s) => {
        const item = document.createElement("div");
        item.className = "demo-swatch";
        const color = "rgba(" + s.rgba[0] + "," + s.rgba[1] + "," + s.rgba[2] + "," + s.rgba[3] / 255 + ")";
        item.innerHTML = "<div style='width:96px;height:36px;border:1px solid #94a3b8;background:repeating-conic-gradient(#e5e7eb 0% 25%, #ffffff 0% 50%) 50% / 12px 12px'>"
            + "<div style='width:100%;height:100%;background:" + color + "'></div></div>"
            + s.name + ": " + Array.from(s.rgba).join(", ");
        row.appendChild(item);
    });

    showPreview(label, row);
}

Reading a Full Row of Pixel Data

getImageData() returns a flat array: 4 numbers (r, g, b, a) per pixel, left to right, top to bottom.

What this code does: Creates a tiny 5×1 canvas (five pixels in one row), paints the first two pixels red and the last three blue, then reads the whole row in one getImageData() call. A loop picks each pixel’s four values out of the flat array using the index x * 4 and prints them. The canvas is enlarged 50 times so you can see all five pixels, with a ring on each, and a colour box for each pixel shows the values read.

const canvas = new OffscreenCanvas(5, 1);
const ctx = canvas.getContext("2d");

ctx.fillStyle = "red";
ctx.fillRect(0, 0, 2, 1);

ctx.fillStyle = "blue";
ctx.fillRect(2, 0, 3, 1);

const row = ctx.getImageData(0, 0, 5, 1).data;
console.log("Array length:", row.length, "(5 pixels x 4 values)");

const marks = [];
const samples = [];

for (let x = 0; x < 5; x++) {
    const r = row[x * 4];
    const g = row[x * 4 + 1];
    const b = row[x * 4 + 2];
    const a = row[x * 4 + 3];

    console.log("Pixel " + x + ":", r, g, b, a);
    marks.push({ x: x, y: 0, name: String(x) });
    samples.push({ name: "Pixel " + x, rgba: [r, g, b, a] });
}

showCanvas("The 5x1 canvas enlarged 50 times", canvas, { scale: 50, marks: marks });
showSwatches("Each pixel read from the array", samples);

// ===== Helpers below are not part of the lesson =====
function showPreview(label, node) {
    setTimeout(() => {
        const box = document.createElement("div");
        box.className = "demo-preview-box";
        box.innerHTML = "<p class='demo-preview-label'>" + label + "</p>";
        box.appendChild(node);
        document.getElementById("previewContent").appendChild(box);
    }, 0);
}

function showCanvas(label, source, options) {
    const scale = (options && options.scale) || 1;
    const marks = (options && options.marks) || [];

    const copy = document.createElement("canvas");
    copy.width = source.width;
    copy.height = source.height;
    copy.getContext("2d").drawImage(source, 0, 0);
    copy.style.cssText = "display:block;width:" + source.width * scale + "px;height:" + source.height * scale + "px;image-rendering:pixelated;background:repeating-conic-gradient(#e5e7eb 0% 25%, #ffffff 0% 50%) 50% / 16px 16px;";

    const wrap = document.createElement("div");
    wrap.className = "demo-canvas-wrap";
    wrap.style.cssText = "position:relative;display:inline-block;";
    wrap.appendChild(copy);

    marks.forEach((m) => {
        const ring = document.createElement("span");
        ring.className = "demo-canvas-mark";
        ring.style.cssText = "position:absolute;box-sizing:border-box;width:12px;height:12px;border:2px solid #d946ef;border-radius:50%;left:" + ((m.x + 0.5) * scale - 6) + "px;top:" + ((m.y + 0.5) * scale - 6) + "px;";
        ring.innerHTML = "<b style='position:absolute;left:12px;top:-4px;font:bold 12px sans-serif;color:#a21caf'>" + m.name + "</b>";
        wrap.appendChild(ring);
    });

    showPreview(label, wrap);
}

function showSwatches(label, samples) {
    const row = document.createElement("div");
    row.className = "demo-swatch-row";
    row.style.cssText = "display:flex;flex-wrap:wrap;gap:8px;font:12px monospace;color:#0f172a;";

    samples.forEach((s) => {
        const item = document.createElement("div");
        item.className = "demo-swatch";
        const color = "rgba(" + s.rgba[0] + "," + s.rgba[1] + "," + s.rgba[2] + "," + s.rgba[3] / 255 + ")";
        item.innerHTML = "<div style='width:96px;height:36px;border:1px solid #94a3b8;background:repeating-conic-gradient(#e5e7eb 0% 25%, #ffffff 0% 50%) 50% / 12px 12px'>"
            + "<div style='width:100%;height:100%;background:" + color + "'></div></div>"
            + s.name + ": " + Array.from(s.rgba).join(", ");
        row.appendChild(item);
    });

    showPreview(label, row);
}

<canvas> vs. OffscreenCanvas

Feature <canvas> Element OffscreenCanvas
Requires the DOM?YesNo
Renders visibly on screen?Yes, automaticallyNo, it stays in memory
Works inside a Web Worker?NoYes
Same drawing methods (fillRect, arc, stroke)?YesYes, identical 2D API
Typical use caseOn-page graphics, games, chartsBackground rendering, image processing, tests

Common Mistakes

  • Forgetting beginPath() before starting a new shape, which can carry over unwanted path segments into the next fill or stroke.
  • Expecting toDataURL() to exist on OffscreenCanvas. It doesn’t; use convertToBlob() or transferToImageBitmap() instead.
  • Forgetting that alpha values in getImageData() run from 0–255, while globalAlpha is set on a 0–1 scale.
  • Drawing coordinates outside the canvas’s bounds and being surprised nothing appears; canvas drawing simply clips silently, with no error.
  • Assuming fillStyle resets automatically between draw calls; it stays set until you explicitly change it again.

Best Practices

  • Use getImageData() to write automated checks that your rendering logic actually draws what you expect.
  • Keep getImageData() calls small and targeted; reading large regions repeatedly is relatively expensive.
  • Always pair beginPath() with each new shape to avoid unintended leftover path segments.
  • Reset globalAlpha back to 1 after you’re done drawing something semi-transparent, so it doesn’t leak into later drawing calls.
  • Reach for OffscreenCanvas when rendering needs to happen off the main thread or without a visible page at all.

Quick Summary

  • canvas.getContext("2d") (or new OffscreenCanvas()) gives you an object with all the drawing methods.
  • fillRect(), arc(), stroke(), and clearRect() all genuinely modify the canvas’s real pixel data.
  • getImageData(x, y, w, h) reads back real [r, g, b, a] values, proving exactly what was drawn.
  • Shapes drawn later appear on top of shapes drawn earlier.
  • OffscreenCanvas supports the same 2D drawing API as a regular <canvas>, but works without a visible page, including inside Web Workers.

Try It Yourself

Modify the code below. Try changing the shapes’ colors and positions, then sample a few different pixel coordinates to see the real values change.

What this code does: Combines a background, a filled circle and a diagonal line on a 150×150 canvas, then samples one pixel in each of the three areas: the background (A), the circle’s centre (B) and a point on the diagonal line (C). The picture shows the finished drawing with the three pixels marked, and the colour boxes show what each one contains. Change the colours or positions, then sample other coordinates.

const canvas = new OffscreenCanvas(150, 150);
const ctx = canvas.getContext("2d");

ctx.fillStyle = "lightyellow";
ctx.fillRect(0, 0, 150, 150);

ctx.beginPath();
ctx.arc(75, 75, 40, 0, Math.PI * 2);
ctx.fillStyle = "orangered";
ctx.fill();

ctx.strokeStyle = "black";
ctx.lineWidth = 3;
ctx.beginPath();
ctx.moveTo(0, 0);
ctx.lineTo(150, 150);
ctx.stroke();

const background = ctx.getImageData(5, 5, 1, 1).data;
const circle = ctx.getImageData(75, 75, 1, 1).data;
const line = ctx.getImageData(20, 20, 1, 1).data;

console.log("A, background corner:", Array.from(background));
console.log("B, center of circle:", Array.from(circle));
console.log("C, on the diagonal line:", Array.from(line));

showCanvas("The finished drawing", canvas, { scale: 2, marks: [{ x: 5, y: 5, name: "A" }, { x: 75, y: 75, name: "B" }, { x: 20, y: 20, name: "C" }] });
showSwatches("Colours read back", [{ name: "A", rgba: background }, { name: "B", rgba: circle }, { name: "C", rgba: line }]);

// ===== Helpers below are not part of the lesson =====
function showPreview(label, node) {
    setTimeout(() => {
        const box = document.createElement("div");
        box.className = "demo-preview-box";
        box.innerHTML = "<p class='demo-preview-label'>" + label + "</p>";
        box.appendChild(node);
        document.getElementById("previewContent").appendChild(box);
    }, 0);
}

function showCanvas(label, source, options) {
    const scale = (options && options.scale) || 1;
    const marks = (options && options.marks) || [];

    const copy = document.createElement("canvas");
    copy.width = source.width;
    copy.height = source.height;
    copy.getContext("2d").drawImage(source, 0, 0);
    copy.style.cssText = "display:block;width:" + source.width * scale + "px;height:" + source.height * scale + "px;image-rendering:pixelated;background:repeating-conic-gradient(#e5e7eb 0% 25%, #ffffff 0% 50%) 50% / 16px 16px;";

    const wrap = document.createElement("div");
    wrap.className = "demo-canvas-wrap";
    wrap.style.cssText = "position:relative;display:inline-block;";
    wrap.appendChild(copy);

    marks.forEach((m) => {
        const ring = document.createElement("span");
        ring.className = "demo-canvas-mark";
        ring.style.cssText = "position:absolute;box-sizing:border-box;width:12px;height:12px;border:2px solid #d946ef;border-radius:50%;left:" + ((m.x + 0.5) * scale - 6) + "px;top:" + ((m.y + 0.5) * scale - 6) + "px;";
        ring.innerHTML = "<b style='position:absolute;left:12px;top:-4px;font:bold 12px sans-serif;color:#a21caf'>" + m.name + "</b>";
        wrap.appendChild(ring);
    });

    showPreview(label, wrap);
}

function showSwatches(label, samples) {
    const row = document.createElement("div");
    row.className = "demo-swatch-row";
    row.style.cssText = "display:flex;flex-wrap:wrap;gap:8px;font:12px monospace;color:#0f172a;";

    samples.forEach((s) => {
        const item = document.createElement("div");
        item.className = "demo-swatch";
        const color = "rgba(" + s.rgba[0] + "," + s.rgba[1] + "," + s.rgba[2] + "," + s.rgba[3] / 255 + ")";
        item.innerHTML = "<div style='width:96px;height:36px;border:1px solid #94a3b8;background:repeating-conic-gradient(#e5e7eb 0% 25%, #ffffff 0% 50%) 50% / 12px 12px'>"
            + "<div style='width:100%;height:100%;background:" + color + "'></div></div>"
            + s.name + ": " + Array.from(s.rgba).join(", ");
        row.appendChild(item);
    });

    showPreview(label, row);
}
Tip: When you want to be certain your canvas code is drawing exactly what you intend, sample real pixels with getImageData() instead of just trusting the code “looks right”. It’s the same technique used in this lesson’s examples.

Understanding the real drawing and pixel-reading side of <canvas> complements the broader API tour in the JavaScript Canvas API lesson, and is a genuinely useful technique for testing rendering logic in real projects.

🧪 Test Your JavaScript Code

Edit the code on the left and click “Run Code” to see the console output on the right.

📝 JavaScript Code
👁 Console Output

Click “Run Code” to see the output here.

Ctrl + Enter to run code Esc to close