grelf Posted January 24, 2021 Share Posted January 24, 2021 (edited) When using the 2D context of a canvas each pixel has 4 components: red, green, blue and alpha (transparency). Each has a range from 0 to 255 (unsigned byte). Access to the data is via an object of type ImageData which has 3 properties: width, height and data. Data has the pixel values sequentially scanning horizontally and then downwards from top left. The data array therefore has a length equal to 4 times the number of pixels. It is possible to add getPixel() and setPixel() methods to the ImageData prototype but I will not call them exactly that in case the standard someday adds these anyway. So I could simply write /** Returns an array with 4 elements: RGBA. */ ImageData.prototype.grGetPixel = function (x, y) { var i = (this.width * Math.round (y) + Math.round (x)) * 4; // NB: JavaScript will try to index arrays with non-integers! return [this.data [ i ] , this.data [i + 1], this.data [i + 2], this.data [i + 3]]; }; /** rgba is an array with 4 elements: RGBA. * Ignores transparent pixels (A = 0). */ ImageData.prototype.grSetPixel = function (x, y, rgba) { var i = (this.wd * Math.round (y) + Math.round (x)) * 4; if (0 === rgba [3]) return; this.data [ i ] = rgba [0]; this.data [i + 1] = rgba [1]; this.data [i + 2] = rgba [2]; this.data [i + 3] = rgba [3]; }; BUT what about using the alpha transparency value? How is it supposed to be applied? Trying to find an algorithm in the working HTML5 standard, or in the W3 CSS standards or on the excellent MDN site all eventually lead to a short paragraph in the W3C standard for SVG: https://www.w3.org/TR/SVG11/masking.html#SimpleAlphaBlending .That says that it assumes the alpha to be premultiplied. To find out what that means see https://en.wikipedia.org/wiki/Alpha_compositing .Thanks to a section on another Wikipedia page (https://en.wikipedia.org/wiki/Alpha_compositing#Alpha_blending) I was able eventually to work out what I wanted to do. The next difficulty was that the alpha part of a canvas pixel has integer values in the range 0..255 but the compositing formulae need decimal multipliers in the range 0..1. Did I want to do a division, a / 255, every time I want to set a pixel? Certainly not: division is slow. Fortunately there are only 256 cases to consider so the solution is to set up a look-up table (LUT) when my program starts: var alphaLUT = new Array (256); // Converts index 0..255 to 0..1 for (var i = 0; i < 256; i++) alphaLUT [ i ] = i / 255; So my grSetPixel finally became /** rgba is an array with 4 elements: RGBA. * Ignores completely transparent pixels (A = 0). */ ImageData.prototype.grSetPixel = function (x, y, rgba) { var i = (this.width * Math.round (y) + Math.round (x)) * 4; if (0 === rgba [3]) return; var r = this.data [ i ] ; // Existing value, to be composited var g = this.data [i + 1]; var b = this.data [i + 2]; var a01 = alphaLUT [rgba [3]]; var a10 = 1 - a01; this.data [ i ] = (a10 * r + a01 * rgba [0]) & 0xff; this.data [i + 1] = (a10 * g + a01 * rgba [1]) & 0xff; this.data [i + 2] = (a10 * b + a01 * rgba [2]) & 0xff; this.data [i + 3] = 255; }; and it works a treat. I wanted to put glass signs in my forest. (Yes, I am oldfashioned - I see nothing wrong with var.) Edited January 24, 2021 by grelf My code sections keep being altered when I submit Quote Link to comment Share on other sites More sharing options...
Recommended Posts
Join the conversation
You can post now and register later. If you have an account, sign in now to post with your account.
Note: Your post will require moderator approval before it will be visible.