A Blog by Hangindev

RGB Splitting Effect with HTML5 Canvas and JavaScript

Image with RGB Splitting Effect Applied

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.

Screenshot of Honeypot page
Honeypot likes to use this RGB splitting effect on their page.

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.

Live demo

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>
The crossorigin="anonymous" attribute on the img 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 a canvas, 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 variables
const 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 0
const { rOffset = 0, gOffset = 0, bOffset = 0 } = options;
// clone the pixel array from original imageData
const originalArray = imageData.data;
const newArray = new Uint8ClampedArray(originalArray);
// loop through every pixel and assign values to the offseted position
for (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 object
return 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 the ImageData object containing the array of pixel values and dx, dy are the x and y coordinate at which to place the image data in the destination canvas.
Images with and without RGB splitting effect applied

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 imageData
function updateCanvas() {
const updatedImageData = rgbSplit(imageData, {
// turn string value into integer
rOffset: 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. πŸ˜‰