Search code examples
colorshtml5-canvasp5.jsalpha

Transformed colors when painting semi-transparent in p5.js


A transformation seems to be applied when painting colors in p5.js with an alpha value lower than 255:

for (const color of [[1,2,3,255],[1,2,3,4],[10,11,12,13],[10,20,30,40],[50,100,200,40],[50,100,200,0],[50,100,200,1]]) {
  clear();
  background(color);
  loadPixels();
  print(pixels.slice(0, 4).join(','));
}
Input/Expected Output   Actual Output (Firefox)
1,2,3,255               1,2,3,255 ✅
1,2,3,4                 0,0,0,4
10,11,12,13             0,0,0,13
10,20,30,40             6,19,25,40
50,100,200,40           51,102,204,40
50,100,200,0            0,0,0,0
50,100,200,1            0,0,255,1

The alpha value is preserved, but the RGB information is lost, especially on low alpha values.

This makes visualizations impossible where, for example, 2D shapes are first drawn and then the visibility in certain areas is animated by changing the alpha values.

Can these transformations be turned off or are they predictable in any way?

Update: The behavior is not specific to p5.js:

const ctx = new OffscreenCanvas(1, 1).getContext('2d');
for (const [r,g,b,a] of [[1,2,3,255],[1,2,3,4],[10,11,12,13],[10,20,30,40],[50,100,200,40],[50,100,200,0],[50,100,200,1]]) {
  ctx.clearRect(0, 0, 1, 1);
  ctx.fillStyle = `rgba(${r},${g},${b},${a/255})`;
  ctx.fillRect(0, 0, 1, 1);
  console.log(ctx.getImageData(0, 0, 1, 1).data.join(','));
}

Solution

  • Internally, the HTML Canvas stores colors in a different way that cannot preserve RGB values when fully transparent. When writing and reading pixel data, conversions take place that are lossy due to the representation by 8-bit numbers.

    Take for example this row from the test above:

    Input/Expected Output   Actual Output
    10,20,30,40             6,19,25,40
    
    IN (conventional alpha) R G B A
    values 10 20 30 40 (= 15.6%)

    Interpretation: When painting, add 15.6% of (10,20,30) to the 15.6% darkened (r,g,b) background.

    Canvas-internal (premultiplied alpha) R G B A
    R G B A
    calculation 10 * 0.156 20 * 0.156 30 * 0.156 40 (= 15.6%)
    values 1.56 3.12 4.7 40
    values (8-bit) 1 3 4 40

    Interpretation: When painting, add (1,3,4) to the 15.6% darkened (r,g,b) background.

    Premultiplied alpha allows faster painting and supports additive colors, that is, adding color values without darkening the background.

    OUT (conventional alpha) R G B A
    calculation 1 / 0.156 3 / 0.156 4 / 0.156 40
    values 6.41 19.23 25.64 40
    values (8-bit) 6 19 25 40

    So the results are predictable, but due to the different internal representation, the transformation cannot be turned off.

    The HTML specification explicitly mentions this in section 4.12.5.1.15 Pixel manipulation:

    Due to the lossy nature of converting between color spaces and converting to and from premultiplied alpha color values, pixels that have just been set using putImageData(), and are not completely opaque, might be returned to an equivalent getImageData() as different values.

    see also 4.12.5.7 Premultiplied alpha and the 2D rendering context