In this lesson, you'll learn how to read and write individual pixel color values from a canvas.
Let's look at pixel level manipulation of canvas. Up to now we've concentrated on drawing content into a canvas. This video will cover both writing and reading of individual pixels in a canvas. You can do pixel level manipulation via a special object called ImageData. You can get an ImageData object by calling getimageData on a 2D rendering context.
You paste in an x, y, width, and height. This specifies the rectangular area of the canvas that you want to get pixel values for. This ImageData object has three important properties, width, height, and data. Width and height are the width and height of the rectangular area you specified. Having them available on the ImageData object will be useful, as you'll soon see.
The data property is an array of pixel color channel values. This is a single, one dimensional array. Each element is a single red, green, blue, or alpha channel of a single pixel. If you specify a 100 by 100 pixel rectangle, the data array will have a length of 40,000. That's 100 times 100, or 10,000 pixels times 4 color channels each.
This is an efficient way of storing pixel data, but it takes a bit of calculation to get at the exact pixel and channel you want to examine. Of course, storing grids of data in a one dimensional array is not a new invention, and there's a standard algorithm for getting the index to represent, say, a particular xy position in the grid.
That algorithm is y times grid width plus x. Say we have a 10 by 10 grid stored in an array and we want to know the array index that points to x equals 2, y equals 4. It's x times width plus x or 4 times 10 plus 2, index 42.
Of course with image data each pixel takes up four elements in the array so the algorithm becomes y times width plus x, all times four. The resulting index will point to the first element representing that pixel. It's red channel. You can then add to that index to get the other channels. Let's try it out here.
I know that this canvas is 600 by 600, so I'll get all the pixels by pasting in 0, 0, 600, 600. Let's set an x of 100 and a y of 160. Then we can calculate an index by saying, "x times ImageData width plus x, all times four." Again, this will point to the red channel. Let's log it.
Now let's log the green channel which will be at index plus one. Blue will be index plus two, and alpha at index plus three. We open the console and run that, and we see that all of the values are zero. This makes sense because we haven't drawn anything into the canvas yet. All channels of all pixels have been initialized to zero.
Let's set the fillStyle to red and fill a big rectangle before getting the ImageData. Now the red channel and the alpha channel are both 255. That makes sense. If we set the fill color to yellow, then we also set the green channel to 255. You know how to read pixel values, but we'll come back to this later with some optimizations.
How about writing pixel values? We have a data array of color channel values. Can we put data in there? Sure, why not? I'll set up a nested loop here. X will go from 100 to 200. Inside that y will go from 100 to 200 as well.
Within the inner loop I'll calculate an index based on the xy and ImageData width. Then I'll set ImageData index to 255. This sets the red channel to be fully on. I'll do the same thing for index plus three, turning opacity fully on. Nothing happens.
This is because the ImageData object you have is really just a snapshot of the state of the canvas at the time you called getimageData. There is no dynamic connection between the canvas and the ImageData after that point.
If you draw more shapes to your canvas, your ImageData will not be updated. If you want your ImageData to be up to date, you have to call ImageData again. Likewise, if you change values in the ImageData data, it doesn't affect the canvas at all.
If you want your canvas to reflect the current ImageData values, you have to put that ImageData back into the canvas. You do this with putImageData. This is another method of the 2D rendering context. This method is a bit like draw image in that it accepts different numbers of parameters to control what portions of the ImageData is drawn and where it will be drawn on the canvas.
The simplest version takes an ImageData and an xy position. That xy is the origin point of the canvas where the ImageData will be drawn. In this version, the entire ImageData will be put to the canvas. I can say context.putImageData (imageData, 0,0). There's the red square we drew with the double four loops.
If I change the x or y, you can see that it changes where the image data is drawn. The other version of this method takes an additional four parameters. These define a rectangle within the ImageData. Only this portion of the ImageData will be put to the canvas.
For example, I know that the red square is at position 100, 100, and it's 100 by 100 in size. If I add 100, 100, 100, 100 to this call, then I'll only be putting those pixels that contain the red square. This may look like nothing different happened, so let me do something else.
I'll go back to the original putImageData with just the xy position. Just before that I'll do a fillRect over the entire canvas. This should fill the entire canvas with black. When I do the putImageData, it overrides all the black pixels with the values that are in the ImageData, which all default to zeros so we don't see any black.
Now if I go back to the 100, 100, 100, 100 version, you can clearly see that it only replaced the pixels in that exact rectangle. That's the basics of reading and writing pixels and canvas, but there are a couple of important things to realize.
First is that when you put ImageData to a canvas, it's not the same as a graphical drawing operation. You're directly transferring pixel color channel values. There's no blending, global alpha, or transformation functionality available during this operation.
If you have empty or transparent values in your ImageData, they won't be just blended in like drawing an image with an alpha channel. Those transparent pixels will be directly transferred to your canvas, wiping out whatever was there before.
The next thing that's important to realize is that getting or putting ImageData are very expensive operations. The more data you get or put, the longer the operations take. This is, most likely, something you do not want to do with animation. In general you want to get or put the absolute minimum amount of pixel data you need to.
In the first example we were getting the ImageData for the entire 600 by 600 canvas. That's 360,000 pixels resulting in a data array containing 1,440,000 elements, all so you could examine the color value of a single pixel. A bit wasteful, eh?
If all you want to look at is one pixel, just get the ImageData for that single pixel. You can create a getPixel method. This will take a 2D rendering context and an xy position. Now we can get ImageData pasting in xy 1, 1. We're getting a one by one rectangle located at xy, a single pixel.
This will result in an array with just four elements, so no fancy math is needed. Let's just return an object with R, G, B, and A properties that we get from elements zero through three. Now let's try a colored rectangle so we have some pixels to read. Then we can call this getPixel method pasting in the context and an xy of, say, 100, 100. Finally we can log the R, G, B, and A properties of the return object.
Check the console, and, sure enough, that's the color we set. That's about 360,000 times more efficient than what we were doing earlier. That's a decent optimization in my book. We can do the same thing to set an individual pixel. This will be a setPixel method. This will need to get the context x and y like getPixel but will also need the red, green, blue, and alpha properties to set.
You could create something fancier that maybe parses the color string, but we'll set this up to get all the parameters for now. In here we could use getimageData to get a single pixel, but since we're going to be overriding it anyway, let's try something different.
The other way to get an ImageData object is createImageData. This is also a method of the contexts, but it just takes a width and height. We'll paste 1, 1. This will give us back an ImageData object of the specified size with all the pixel values set to zero despite any content that might be in the canvas.
We'll simply set the four elements of data array with the values pasted in and then call putImageData using x and y as the origin point. To test this we'll fill the canvas with black and then run a loop 1,000 times. We'll generate a random x and y and fill that pixel with white, which is all 255s.
Realize that the x and y need to be rounded to whole numbers so that they can be used to calculate an integer to use an index to the data array. Then we have a star field. You might have noticed that the star field was showing up before I finished typing all the parameters. What's happening there is that the null parameter for alpha was being converted to a zero, and you can see through to the underlying white HTML document beneath the canvas.
Just to make sure it's all working well, I'll set the blue channel to zero, and we have yellow stars. ImageData is a very powerful object giving you some new advanced techniques to use. However, use it sparingly.
It's usually more efficient to draw content to a canvas with other methods than putImageData. As for getimageData, there's no substitute for reading pixel data from a canvas. Just remember to be smart about how much data you get.