RGB Splitting Effect with HTML5 Canvas and JavaScript
Tags: javascript, 100daysofcode, codenewbie, tutorial
Published: 2020-06-27 11:07:41

Recently, I followed [Honeypot](https://twitter.com/honeypotio) 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.

<figure>
  <img src="https://dev-to-uploads.s3.amazonaws.com/i/aoj5o6ayf4ue6q1zjeed.jpg" alt="Honeypot Screencapture">
  <figcaption>Honeypot likes to use this RGB splitting effect on their page.</figcaption>
</figure>

## Walk-through 🚶‍♀️🚶‍♂️ 

[Live demo](https://hangindev.com/demo/rgb-splitting.html)

Open [this CodeSandbox](https://codesandbox.io/s/focused-bogdan-y68xc?fontsize=14&hidenavigation=1&theme=dark) 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.

```html
<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`.

```javascript
// 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](#time-to-tear-them-apart).

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.)

```javascript
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.

```javascript
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)

```javascript
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.

```javascript
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*.**

![Images with and without RGB splitting effect applied](https://dev-to-uploads.s3.amazonaws.com/i/pn5domhhsjhhlmlrns05.png)

![Captain America Lumber](https://dev-to-uploads.s3.amazonaws.com/i/1e8xa9ghm2k9iup5a4vd.gif)

## 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.

```javascript
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! 👋*

{% twitter 1276887792644231169 %}

![React Summit](https://dev-to-uploads.s3.amazonaws.com/i/2ub10uyfjmw681ixyaae.png)

> React Summit is coming back on October 15-16. There'll be speakers like [Kent C. Dodds](https://twitter.com/kentcdodds), [Max Stoiber](https://twitter.com/mxstbr), [Sara Vieira](https://twitter.com/nikkitaftw), [Sharif Shameem](https://twitter.com/sharifshameem).
> 
> Register for free before September 20: [https://ti.to/gitnation/react-summit?source=REFURCR-1](https://ti.to/gitnation/react-summit?source=REFURCR-1).