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. 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 πΆββοΈπΆββοΈ
Open this CodeSandbox if you want 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 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 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 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
If you have used canvas before, feel free to fast forward to the real action.
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. 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() {
// Get a two-dimensional rendering context
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 each channel of color(red, green, or blue) to 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 data 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.
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! π
Honeypot@honeypotioTrade secrets revealed π Great post, @hangindev! twitter.com/hangindev/statβ¦14:39 PM - 27 Jun 2020Jason Leung π§ββοΈπ¨βπ» @hangindevI'm following @honeypotio and noticed they like to use this glitch effect on their images a lot. So I figured I could write about how to implement this RGB splitting technique with HTML5 canvas and JavaScript. https://t.co/kycSIru5xn #javascript #100DaysOfCode #CodeNewbie
React Summit is coming back on October 15-16. There'll be speakers like Kent C. Dodds, Max Stoiber, Sara Vieira, Sharif Shameem.
Register for free before September 20: https://ti.to/gitnation/react-summit?source=REFURCR-1.
Top comments (0)