RGB Splitting Effect with HTML5 Canvas and JavaScript
Recently, I followed Honeypot on Twitter. In case you didn't know, Honeypot is a developer-focused job platform that also produces awesome documentaries exploring tech culture and influential technologies. On their page, they like to use this RGB splitting technique in their cover images to create a glitch effect. Neat. So I figured I'd write a post explaining how it can be done with HTML5 canvas and JavaScript to those who are new to image processing on the web.
Walk-through πΆββοΈπΆββοΈ
In the end result, there are an img
element and canvas
element placed side by side to show the before-after comparison. Below there are three sliders which are input
elements with type "range". When you adjust the slider, you will see the splitting effect on the canvas
.
I created this CodeSandbox for you to follow along. Let's walk through the files. First, I scaffolded the structure inside body
of index.html
so that we can focus on writing JavaScript. I also added a stylesheet in the head
which I will not go into here but feel free to have a look.
<body><!-- Before / After --><div class="container"><div><p>Original Image:</p><img id="Source" src="/demo.jpg" crossorigin="anonymous" /></div><div><p>Canvas:</p><canvas id="Canvas"></canvas></div></div><!-- Control Sliders --><div class="control"><div class="red"><label>R:</label><input id="rOffset" type="range" min="-100" max="100" step="5" /></div><div class="green"><label>G:</label><input id="gOffset" type="range" min="-100" max="100" step="5" /></div><div class="blue"><label>B:</label><input id="bOffset" type="range" min="-100" max="100" step="5" /></div></div><!-- Reference the external script --><script src="app.js"></script></body>
Thecrossorigin="anonymous"
attribute on theimg
tag tells the browser to fetch the image with CORS(Cross-Origin Resource Sharing) header. I will write more about CORS in the future. For now, just keep in mind if you fail to do some operations on acanvas
, it may be related to CORS.
Then there are two js files. app.js
contains the minimal code to get you started. If at every time you want to look at the finished code, you can check app-finish.js.
// Find all elements that will be used and assign them to variablesconst image = document.getElementById("Source");const canvas = document.getElementById("Canvas");const rOffsetInput = document.getElementById("rOffset");const gOffsetInput = document.getElementById("gOffset");const bOffsetInput = document.getElementById("bOffset");// If the image is completely loaded before this script executes, call init().if (image.complete) init();// In case it is not loaded yet, we listen to its "load" event and call init() when it fires.image.addEventListener("load", init);function init() {// Where the Magic Happens}
Display the Image on Canvas
For any image processing tasks you'd like to perform, you will most likely need to use the canvas
element. canvas
is a powerful playground for you to play with image data, apply filters and overlays effects. And you are not limited to static images but you can even manipulate video data with canvas
. Here let's first try to draw the image from the img
element to the canvas
.
To draw anything on the canvas, you will need to get a drawing context using getContext
method. "2d" is passed as argument to get a two-dimensional rendering context. Then, we will set the canvas drawing dimensions (as opposed to the display dimensions set by CSS) to the intrinsic width and height of the image. Finally, we will use the drawImage
method to draw the image onto the canvas. (Save the file using ctrl+s/cmd+s after changes to see the update.)
function init() {const ctx = canvas.getContext("2d");const width = image.naturalWidth;const height = image.naturalHeight;canvas.width = width;canvas.height = height;ctx.drawImage(image, 0, 0, width, height);}
Syntax: drawImage(image, dx, dy, dWidth, dHeight) where image is the image elemnet to show, dx, dy are the x and y coordinate in the canvas at which to place the top-left corner of the image, and dWidth, dHeight are the width and height to draw the image in the canvas.
Peek into the ImageData
Now, let's use getImageData
to get the image data out and see what is in it using console.log
. Do not use the console CodeSandbox provides since the ImageData
object is a fairly large object. Instead, open the browser in a new window and use the native console of the browser.
function init() {const ctx = canvas.getContext("2d");const width = image.naturalWidth;const height = image.naturalHeight;canvas.width = width;canvas.height = height;ctx.drawImage(image, 0, 0, width, height);// πconst imageData = ctx.getImageData(0, 0, width, height);console.log(imageData);}
Syntax: ctx.getImageData(sx, sy, sw, sh) where sx, sy are the x and y coordinate of the top-left corner of the rectangle from which the data will be extracted, and sw, sh are the width and height of the rectangle from which the data will be extracted.
The imageData
object has three properties: width
and height
are the actual dimensions of the image data we extracted, which in this case is also the dimensions of our image and canvas. The data
property is an Uint8ClampedArray
which is an array-like object used to store values between 0-255(inclusive). Values smaller than 0 or greater than 255 will be clamped to 0 and 255.
So what is this array representing? If you have used rgb
color in CSS, you may have a sense that it is something related and you are right. This Uint8ClampedArray
is a one-dimensional array representing the color in the RGBA(red, green, blue, alpha) order of every pixel in the image. In other words, every four values in this array represent a pixel in the image.
ImageData {data: Uint8ClampedArray[14, 34, 58, 255, 38, 60, 81, 255, 46, 75, 93, 255β¦], width: 640, height: 427}
Time to Tear Them Apart
Now that we've learned about ImageData
. It's time for the fun part. (finally!) The idea behind the RGB splitting is to shift the whole channel of color(red, green or blue) in different directions. To implement it, we will create a helper function called rgbSplit
. (create it above or below the init
function)
function rgbSplit(imageData, options) {// destructure the offset values from options, default to 0const { rOffset = 0, gOffset = 0, bOffset = 0 } = options;// clone the pixel array from original imageDataconst originalArray = imageData.data;const newArray = new Uint8ClampedArray(originalArray);// loop through every pixel and assign values to the offseted positionfor (let i = 0; i < originalArray.length; i += 4) {newArray[i + 0 + rOffset * 4] = originalArray[i + 0]; // π΄newArray[i + 1 + gOffset * 4] = originalArray[i + 1]; // π’newArray[i + 2 + bOffset * 4] = originalArray[i + 2]; // π΅}// return a new ImageData objectreturn new ImageData(newPixels, imageData.width, imageData.height);}
rgbSplit
takes in ImageData
and an options
object as arguments. The options
object should have three properties: rOffset
, gOffset
, bOffset
which represent the pixel offset of each color channel.
Next, instead of mutating the data values in ImageData
, let's make a copy of it by calling the Uint8ClampedArray
constructor and passing it the original color array. Then, we will loop through every pixel and manipulate the color in each of them. Remember four values in that array represent one pixel? That's why we are setting the increment expression to be i += 4
.
In each iteration, we take each color intensity from the original array and place it to a new position based on the offset value provided. Again, we are multiplying the offset value by 4 since four values represent one pixel.
π΄π’π΅βͺ π΄π’π΅βͺ π΄π’π΅βͺ π΄π’π΅βͺ
To use the rgbSplit
funciton, we go back into the init
function. We call the rgbSplit
funciton with the imageData
we got from the canvas context and also some random offset values. We will then paint the new image data onto the canvas using the putImageData
method.
function init() {const ctx = canvas.getContext("2d");const width = image.naturalWidth;const height = image.naturalHeight;canvas.width = width;canvas.height = height;ctx.drawImage(image, 0, 0, width, height);const imageData = ctx.getImageData(0, 0, width, height);// πconst updatedImageData = rgbSplit(imageData, {rOffset: 20,gOffset: -10,bOffset: 10});ctx.putImageData(updatedImageData, 0, 0);}
Syntax: ctx.putImageData(imageData, dx, dy) where imageData is theImageData
object containing the array of pixel values anddx, dy
are the x and y coordinate at which to place the image data in the destination canvas.
And voila.
Bonus: Implement the Sliders
Lastly, with the help of the rgbSplit
function, the implementation of the slider control will be straightforward. We just have to listen to the slider "change" event and call the rgbSplit
function with the values of the sliders.
function init() {const ctx = canvas.getContext("2d");const width = image.naturalWidth;const height = image.naturalHeight;canvas.width = width;canvas.height = height;ctx.drawImage(image, 0, 0, width, height);const imageData = ctx.getImageData(0, 0, width, height);// const updatedImageData = rgbSplit(imageData, {// rOffset: 30,// gOffset: -10,// bOffset: 10// });// ctx.putImageData(updatedImageData, 0, 0);rOffsetInput.addEventListener("change", updateCanvas);gOffsetInput.addEventListener("change", updateCanvas);bOffsetInput.addEventListener("change", updateCanvas);// Put this function inside init since we have to access imageDatafunction updateCanvas() {const updatedImageData = rgbSplit(imageData, {// turn string value into integerrOffset: Number(rOffsetInput.value),gOffset: Number(gOffsetInput.value),bOffset: Number(bOffsetInput.value)});ctx.putImageData(updatedImageData, 0, 0);}}
Wrap up
Are you still here? What's meant to be a simple article has turned into one of my longest posts. But I hope you have learned something and get to play with the canvas
element. Please let me know your feedback. Do you think if the post is too lengthy? Or did I not explain some concepts well enough? Anyway, thanks a lot for reading. Until next time! π
Discuss on Twitter β’ Discuss on DEV
I am planning to write about an Instagram Effect Clone with actual functionality and I hope to make it beginner-friendly using just plain HTML, CSS, and JavaScript so that more people can try. It will probably be split into 2 parts? If you would like to get the latest updates, please follow me on twitter. π