JavaScript: RGBA pixel arrays and Uint8ClampedArray

Convert pixels stored the way a canvas stores them to grayscale and see how clamping works.

JavaScript
const pixels = new Uint8ClampedArray([
  255, 0, 0, 255,
  0, 255, 0, 255,
  0, 0, 255, 255,
  255, 255, 255, 255,
]);

for (let i = 0; i < pixels.length; i += 4) {
  const gray = Math.round(0.299 * pixels[i] + 0.587 * pixels[i + 1] + 0.114 * pixels[i + 2]);
  pixels[i] = gray;
  pixels[i + 1] = gray;
  pixels[i + 2] = gray;
}
console.log(pixels.join(","));

const clamped = new Uint8ClampedArray([300, -20, 127.6]);
console.log(clamped.join(","));
Output
76,76,76,255,150,150,150,255,29,29,29,255,255,255,255,255
255,0,128

Explanation

In the browser, canvas ImageData stores an image as one flat Uint8ClampedArray with four bytes per pixel in the order red, green, blue, alpha. Pixel number p starts at index 4·p. Image-processing code walks this array in steps of four.

A Uint8ClampedArray forces every stored value into 0 to 255: values above 255 become 255, values below 0 become 0, and fractions are rounded to the nearest integer. That is convenient for image math, because overshoot from a filter cannot wrap around. The program converts four pixels to grayscale with luma weights and shows the clamping.

Explore the dry run
Guided walkthroughReady
1const pixels = new Uint8ClampedArray([
2 255, 0, 0, 255,
3 0, 255, 0, 255,
4 0, 0, 255, 255,
5 255, 255, 255, 255,
6]);
7
8for (let i = 0; i < pixels.length; i += 4) {
9 const gray = Math.round(0.299 * pixels[i] + 0.587 * pixels[i + 1] + 0.114 * pixels[i + 2]);
10 pixels[i] = gray;
11 pixels[i + 1] = gray;
12 pixels[i + 2] = gray;
13}
14console.log(pixels.join(","));
15
16const clamped = new Uint8ClampedArray([300, -20, 127.6]);
17console.log(clamped.join(","));

Follow the example

Start to inspect the source, values, and output. These prepared steps explain the published sample; grouped loop steps describe multiple iterations.

View the complete dry-run table
Prepared trace of the published program. Some rows group repeated iterations.
Step / lineWhat happensState after stepOutput so far
Step through the array four bytes (one pixel) at a time.—
—
Luma weights: the red pixel gives 76, green 150, blue 29, and white 255.gray = 76, 150, 29, 255
—
Write the same gray value to red, green, and blue. Alpha stays unchanged.gray = 76, 150, 29, 255
—
Four gray pixels, each with alpha 255.gray = 76, 150, 29, 255
76,76,76,255,150,150,150,255,29,29,29,255,255,255,255,255
300 becomes 255, −20 becomes 0, and 127.6 rounds to 128.gray = 76, 150, 29, 255
76,76,76,255,150,150,150,255,29,29,29,255,255,255,255,255
255,0,128

Cost and common mistakes

O(p) for p pixels.

  • A plain Array or Uint8Array does not clamp. In a Uint8Array, 300 wraps around to 44.
  • Remember the stride of four. Treating the array as one value per pixel mixes the channels.
  • Alpha is separate from color. Premultiplied alpha, used in some canvases, changes the arithmetic.

Try it yourself

At which index does the third pixel start?

Reveal answer and reasoning

At index 8, because pixel number 2 (counting from 0) starts at 4 × 2.

Further reading