Principles of 10 Basic Canvas Filters
While learning Canvas, you'll likely encounter filters, which are quite fascinating. Developing Canvas filters requires a foundational understanding of geometry, mathematics, and color theory. However, don't close this page just yet—if you lack the basics, you can always copy and paste the code (much like dealing with regular expressions). I've tried writing and collecting various simple filters. Since it's been a while, I've lost the original sources. If I've offended any original authors, please contact me to make corrections.
The cat image used in this article comes from The Cat API.
Development Environment
The examples listed in this article are written in .html files. Note that although the methods used are native Canvas pixel manipulation APIs, they must be run on a local server to work properly, otherwise they will not take effect due to CORS restrictions.
You can set up a local server in various ways. For instance, projects scaffolded with Vue or React will work out of the box, or you can use http-server. I use a simpler tool: the VS Code Live Server extension, which starts a local service with hot-reloading capabilities.
Setting up the environment is not the focus of this article, so I'll leave it at that.
Filter Principles
As is well known, a bitmap is composed of pixels, which are the smallest units of information in a bitmap. You can understand most everyday images as graphics made up of tiny dots.
Filters work by using specific rules to modify the pixels in an image according to a calculated formula and then re-rendering them.
For instance, inverting the colors of a black cat photo turns originally white pixels black and originally black pixels white. This color inversion is the specific rule.
Filters essentially manipulate pixels. In Canvas, there are 3 commonly used APIs for pixel manipulation:
getImageData()putImageData()createImageData()
getImageData()
The getImageData() method retrieves image data, such as the pixel set composing the image and its dimensions.
Syntax:
javascript context.getImageData(x, y, width, height)
x: The x-coordinate of the top-left corner to start copying.y: The y-coordinate of the top-left corner to start copying.width: The width of the rectangular area to copy.height: The height of the rectangular area to copy.
For example, using a cat image rendered on a Canvas, we can use getImageData() to retrieve its data.
javascript
Looking at the printed information:
data: The pixel data set, stored as an array. This is the key focus of this article!colorSpace: The color standard used by the image (browser dependent).height: The image height.width: The image width.
The data property is a one-dimensional array where values range from 0 to 255. It records the rgba values for each pixel:
r: Redg: Greenb: Bluea: Alpha (Opacity)
In the data array, 4 elements represent 1 pixel (r, g, b, a). Each element is an integer between 0 and 255.
javascript data: [r1, g1, b1, a1, r2, g2, b2, a2, ...]
| Pixel | Value | Color Channel |
|---|---|---|
imageData.data[0] |
49 | Red (r) |
imageData.data[1] |
47 | Green (g) |
imageData.data[2] |
51 | Blue (b) |
imageData.data[3] |
255 | Alpha (a) |
| ... | ... | ... |
imageData.data[n-4] |
206 | Red (r) |
imageData.data[n-3] |
200 | Green (g) |
imageData.data[n-2] |
200 | Blue (b) |
imageData.data[n-1] |
255 | Alpha (a) |
If an image has only 10 pixels, its data array will contain 40 elements.
Therefore, developing Canvas filters means manipulating pixels, which means manipulating the data array. For example, to set an image's opacity to 50%, you iterate through data, change the value of every 4th element (where (index + 1) % 4 === 0) to 128, and then use putImageData() to render the modified data back to the canvas.
putImageData()
The putImageData() method draws the data from an ImageData object onto the canvas.
Syntax:
javascript context.putImageData(imageData, x, y) // or context.putImageData(imageData, x, y, dirtyX, dirtyY, dirtyWidth, dirtyHeight)
imageData: TheImageDataobject to place back on the canvas.x: The x-coordinate of the top-left corner of theImageDataobject.y: The y-coordinate of the top-left corner of theImageDataobject.dirtyX,dirtyY,dirtyWidth,dirtyHeight: Optional parameters defining the dirty rectangle.
javascript
putImageData() is typically used in conjunction with getImageData().
createImageData()
The createImageData() method creates a new, blank ImageData object.
Syntax:
javascript // Creates a new ImageData object with the specified dimensions context.createImageData(width, height) // Creates a new ImageData object with the same dimensions as another ImageData object context.createImageData(imageData)
The APIs above might seem abstract on their own, so let's look at some practical examples, starting with the simplest ones.
Invert Colors
A pixel consists of 4 elements (rgba). For an invert effect, you don't need to modify a (opacity). If rgb are all 255, the color is white; if all 0, it's black. The principle of color inversion is subtracting the original value from 255 for each of the red, green, and blue channels.
For instance, the inverse of rgb(10, 200, 100) is rgb(245, 55, 155).
javascript
By only operating on indices idx, idx + 1, and idx + 2, we modify the rgb channels without affecting the alpha channel, preserving the original opacity.
Grayscale
Using a weighted average approach can produce a visually pleasing grayscale photo.
javascript
Black and White
Sum the rgb values of each pixel and divide by 3. Check if the result is greater than 128; if so, set it to white (255), otherwise set it to black (0). Since the channel range is 0-255, the midpoint 128 serves as the threshold.
javascript
Brightness Adjustment
To brighten an image, increase the rgb values; to darken it, decrease them.
Brighten
javascript
Darken
javascript
Adjust RGB Channels
Similar to the previous example, define an adjustment parameter. If you only want to adjust the red channel, add or subtract this parameter from the red channel values.
javascript
Adjust Opacity
With the previous examples, adjusting opacity is straightforward. Simply subtract an adjustment value from the a (alpha) channel.
javascript
RGB Mask
The logic of a mask is somewhat similar to adjusting RGB channels. The formula used here for a red mask is: find the average of the rgb channels, assign this average to the red channel, and set the green and blue channels to 0.
javascript
Vintage Photo Filter
A vintage photo effect is essentially a grayscale image with a yellowish tint (Red + Green = Yellow). By adjusting the weights, we get the following algorithm:
javascript
Blur
The method to blur an image involves setting the current pixel's value to the average of its surrounding 8 pixels (fewer if the pixel is on the edge). Assigning this average creates a smooth transition color between adjacent pixels. The underlying principle is complex, so I will cover it in a separate article.
javascript
Mosaic
The principle of a mosaic effect is: define a block size parameter (the larger it is, the bigger the mosaic tiles). For the current pixel, find its surrounding pixels within the block, calculate their average color value, and then apply this average color to all pixels within that block. The underlying principle is also quite complex, so I will write another article specifically about it.
javascript
Summary
Developing Canvas filters is essentially about defining filter rules and deriving algorithms to manipulate pixels. In the examples above, the blur and mosaic effects involve many nested loops. Processing large images might cause the application to lag. To resolve this, you can either optimize the algorithm to reduce traversals or offload image processing tasks to Web Workers. Since optimizing algorithms can be quite challenging, I prefer using Web Workers, which I will cover in a future article.