Today, I'm gonna show you all one of my favorite things to do with coding: creating art!. There are a limitless number of artistic ideas you can pursue, with the only limit being your imagination (I know, it sounds super-cheesy but it's true!).

The end product I'll show you how to make in this post can be see on my Codepen. You can use Codepen to follow along, or any other method you prefer. At its core, what is happening is that the level of detail in the image, which I am measuring by the variance in color, is being visualized with circles. Areas with high amounts of detail have smaller circles and areas with low detail have bigger circles. This visualization choice results in the final image bearing a strong resemblance to the original, while still being unique. With the intro out of the way, let's get started shall we.

Loading the Image

The first thing that we have to do to redraw an image, is to load and display it. To accomplish this, we're going to make use of the canvas element. Let's start of by writing our HTML markup first, which will be minimal.


<canvas id='original'></canvas>

<canvas id='circles'></canvas>

We're making two canvas elements, one to display the original image and the other to display the redrawn image. The next thing we have to do is load the image we want to draw, but there's a catch here. If you are following along on Codepen, or another online HTML testing site, you will only be able to load images from sites that have the CORS header set. This is a security setting that only allows images and other content to be loaded on sites with the same origin URL, unless explicitly told otherwise. Lucky for us, imgur has the correct CORS headers set so we can pull an image from there! Go ahead and find an image you want to use (I'll wait) and we'll load that image into our canvas element.


var image = 'http://i.imgur.com/FRK6meX.png';

var imgData;
var imgWidth;
var imgHeight;

function loadImage(url, callback) {
  let img = new Image();
  img.crossOrigin = 'Anonymous';
  img.onload = function() {
    let canvas = document.getElementById('original');
    let ctx = canvas.getContext('2d');
    let h = Math.min(500, this.height);
    let w = Math.round(this.width / this.height * h);
    canvas.height = h;
    canvas.width = w;
    ctx.drawImage(this, 0, 0,w,h);
    imgData = ctx.getImageData(0,0,w,h);
    var dataURI = canvas.toDataURL('png');
    callback(dataURI, h, w);
  }
  img.src = url;
}

var canvas = document.getElementById('canvas');
var ctx = canvas.getContext('2d');

loadImage(image, function(URI, height, width) {
  img.src = URI;
  canvas.height = imgHeight = height;
  canvas.width = imgWidth = width;
  draw();  //We'll implement this function towards the end 
});

There's a lot happening here, most of it is pretty simple, so I'll only give an overview instead of a line-by-line breakdown (you can skip to the next section if you don't care about how we load the image).

To start off, we declare the loadImage, in which we create an image object and we set the crossOrigin property to 'Anonymous' so that we can load images from outside sources. In the onload function for the image, we get a canvas element, set its width and height to a scaled version of the original image, and then draw the image to the canvas. In the line imgData = ctx.getImageData(0,0,w,h) we extract the image's pixel data in to an array for later use. The next line uses the toDataURL method of a canvas element to convert the image into base64 and then issue a callback with that information and the canvas height/width. Our callback will change the image source to base64, set the redrawn canvas height/width, and start the drawing cycle. Finally, we load the image and if all went well, you should see it pop up on your screen!

Writing Some Helper Functions

Before we get too lost with all the circles we're gonna be drawing, let's write a few helper functions to make our lives easier.


function dist(x1, y1, x2, y2) {
   return Math.sqrt(Math.pow(x1 - x2, 2) + Math.pow(y1 - y2, 2));
}
function random(max, min = 0) {
return (Math.random() * (max - min) + min);
}
function getPixelColor(imageData, xPos, yPos, width) {
   var rgb ='rgb(';
   for (var i = 0; i < 3; ++i) {
      rgb += (imageData[(width * yPos * 4) + (xPos * 4) + i]);
      if (i !== 2) rgb += ',';
      else rgb += ')';
   }
   return rgb;

    // A more concise version of this function is:
    // var pixelIndex = (width * yPos * 4) + (xPos * 4);
    // return `rgb(${imageData.slice(pixelIndex, pixelIndex + 3).join(",")}`;
    // but it is easier to see how the color is being built in the long version.
    // Use this shorter one for a speed boost if you want. 
}

The dist function calculates the Euclidean distance between two points, random gives us a random number within the range we specify, and getPixelColor takes a X/Y position from our image and returns the pixel color at that point. To go from a 2-D X/Y position to a 1-D array index (since our pixel color array is flat and lists pixels starting from the top-left and ending at the bottom-right) we use the formula index = (width * yPos * 4) + (xPos * 4). We multiply the Y position by width because when we move down a pixel row, we move move across a width's worth of pixels, and we add the X position since that's how many pixels we have passed in that row. Both are multiplied by 4 because each pixel contains 4 color values, one for red, green, blue, and alpha.

Keep these in the back of your mind because they will all be used in later sections. But don't worry too much because I'll remind you about them when we use them ;)

Creating the Circle Object

Now that we have the image ready, we can start writing the code to put circles on the screen. Since we'll be drawing A LOT of circles, it makes sense to create a Circle object to hold all that logic.


class Circle {
   constructor(x_, y_, r_, color, ctx) {
      this.x = x_;
      this.y = y_;
      this.r = r_;
      this.color = color;
      this.canvas = document.getElementById('canvas');
      this.ctx = canvas.getContext('2d');
   }
   // Note I didn't close the Circle class with another bracket
   // because we will still be adding more code to it

We want our circles to know where they are located, their size, their color, and where they should be drawn, so we pass all that information through the constructor. What we don't want is our circles to overlap eachother when they are drawn so let's add a test to check for that.


   // Remember we're still in the Circle class
   // I'll keep the indenting as a reminder

   outsideCircle(c, _x, _y, _r) {
      return (distance(c.x, c.y, _x, _y) > c.r + _r + 1);
   }

The outsideCircle function takes a circle object and a new circle's position and radius and returns whether or not the new circle would be touching the existing one. If the distance between the two circles' centers is greater than the sum of their radii, they aren't overlapping (draw it out on paper if it you need help conceptualizing this). The +1 is there simply as padding between circles. Now, we just need a way to draw our happy little circles.


   drawCircle() {
      this.ctx.beginPath();
      this.ctx.fillStyle = this.color;
      this.ctx.ellipse(this.x, this.y, this.r, this.r, 0, 0, Math.PI * 2);
      this.ctx.fill();
   }
   // We're done with the circle object now! 
}

This function is pretty straight-forward: it takes location and size from the circle object and draw's a circle with those parameters. As a side note, one interesting thing you can do here to experiment is change this.ctx.fillStyle to this.ctx.strokeStyle and this.ctx.fill to this.ctx.stroke to draw hollow circles. For some pictures, that looks way cooler than a filled circle!

Determining the Variance

The next thing we have to do is determine the variance (remember we're assuming high variance means high level of detail) around a certain point in the image. This will be the most difficult part of this project, so brace yourself! To determine the variance, we first need to map the pixels surrounding a point in our 2-D picture to a 1-D array, which the next function does in a pretty simplistic way.


function findIndices(arr, startInd, width, dist, maxDist, indArr=[]) {
    if (dist > maxDist || indArr.indexOf(startInd) >= 0) {
      return null;
  }
    else {
      indArr.push(startInd);
  }

  //The four recursive calls below will be repeated throughout the function

  //findIndices(arr, startInd-width*4, width, dist+1, maxDist, indArr) | Gets pixel above starting pixel
  //findIndices(arr, startInd-4, width, dist+1, maxDist, indArr) | Gets pixel to the left of starting pixel
  //findIndices(arr, startInd+width*4, width, dist+1, maxDist, indArr) | Gets pixel below starting pixel
  //findIndices(arr, startInd+4, width, dist+1, maxDist, indArr) | Gets pixel to the right of starting pixel

    // Checks if pixel is in first row of image
    if (startInd < width * 4) {
      //Top left corner
      if (startInd === 0) { 
        findIndices(arr, startInd+4, width, dist+1, maxDist, indArr);
        findIndices(arr, startInd+width*4, width, dist+1, maxDist, indArr);
      }
      //Top right corner
      else if (startInd === 4*(width-1)) { 
        findIndices(arr, startInd-4, width, dist+1, maxDist, indArr);
        findIndices(arr, startInd+width*4, width, dist+1, maxDist, indArr);
      }
      else {
        findIndices(arr, startInd-4, width, dist+1, maxDist, indArr);
        findIndices(arr, startInd+width*4, width, dist+1, maxDist, indArr);
        findIndices(arr, startInd+4, width, dist+1, maxDist, indArr);
      }
    }
    // Checks for left edge cases
    else if (startInd % (width*4) === 0) {
      //Bottom left corner
      if (startInd === arr.length - width*4) { 
        findIndices(arr, startInd-width*4, width, dist+1, maxDist, indArr);
        findIndices(arr, startInd+4, width, dist+1, maxDist, indArr);
      }
      else {
        findIndices(arr, startInd-width*4, width, dist+1, maxDist, indArr);
        findIndices(arr, startInd+4, width, dist+1, maxDist, indArr);
        findIndices(arr, startInd+width*4, width, dist+1, maxDist, indArr);
      }
    }
    //Checks is pixel is in last row
    else if (startInd > arr.length - 4*width) {
      //Bottom right corner
      if (startInd === arr.length-4) { 
        findIndices(arr, startInd-width*4, width, dist+1, maxDist, indArr);
        findIndices(arr, startInd-4, width, dist+1, maxDist, indArr);
      }
      else {
        findIndices(arr, startInd-width*4, width, dist+1, maxDist, indArr);
        findIndices(arr, startInd-4, width, dist+1, maxDist, indArr);
        findIndices(arr, startInd+4, width, dist+1, maxDist, indArr);
      }
    }
    //Checks for right edge cases
    else if (startInd % (4*(width-1))) {
      findIndices(arr, startInd-width*4, width, dist+1, maxDist, indArr);
      findIndices(arr, startInd-4, width, dist+1, maxDist, indArr);
      findIndices(arr, startInd+width*4, width, dist+1, maxDist, indArr);
    }
    else {
      findIndices(arr, startInd-width*4, width, dist+1, maxDist, indArr);
      findIndices(arr, startInd-4, width, dist+1, maxDist, indArr);
      findIndices(arr, startInd+width*4, width, dist+1, maxDist, indArr);
      findIndices(arr, startInd+4, width, dist+1, maxDist, indArr);
    }
  return indArr;
}

Alright, I'll admit that function looks big and intimidating (and could probably be written better), but what it's doing is very simple. It finds all the pixels within a certain distance of an initial starting point using recursion. If the current point is outside the max distance or has been found before, it stops checking around that point. Otherwise, it adds the point to an array and checks the other points around it. All the if/else statements inside check for edge cases (literally) because if a pixel is on the edge of an image, there will be less than four pixels in direct contact with it. For example, for a pixel on the left edge, we only need to check the pixels above, below, and to the right of the starting pixel. The function returns an array of indices that, corresponding to pixels in the image, that we'll use to calculate the variance next.


function calcVar(pixelArray, indexArray) {
    var r=[], g=[], b=[];
    indexArray.forEach((e)=>{
      r.push(pixelArray[e]);
      g.push(pixelArray[e+1]);
      b.push(pixelArray[e+2]);
    });
    function singleVar(data) {
      var tot = 0;
      var sqTot =0;
      data.forEach(function(e){
        tot += e;
        sqTot += e*e;
      })
      return sqTot / data.length - ( tot * tot ) / (data.length * data.length);
    }
    return (singleVar(r) + singleVar(g) + singleVar(b));
}

The calcVar function takes in the array of pixel information (obtained from the loadImage function) and the array of indices (obtained from the findIndices function). It goes through each index from indexArray and extracts the corresponding R,G, and B values from pixelArray. Then, it uses the extracted values to calculate the total variance in color as the sum of the individual variances of the RGB values. Keep in mind, we use this function to calculate the color variance around a certain point, so we will need to repeat this calculation every time we choose a new point.

Putting It All Together

Now that we have methods to make circles and calculate the variance in color around points, let's add a method to select where to draw new circles.


var circles = []; // This is a global variable keeping track of all our circles

function newCircle() {
    var x = Math.floor(random(canvas.width));
    var y = Math.floor(random(canvas.height));
    var r = 50/(Math.sqrt(
                  calcVar(
                    imgData.data, findIndices(
                      imgData.data, (x + y * imgWidth) * 4, imgWidth, 0, 10)
                    )
                  )
               );
    var valid = true;
    for (let c of circles) {
      if (Circle.outsideCircle(c, x, y, r) === false) {
        valid = false;
        break;
      }
    }
    if (valid === true) {
      var color = getPixelColor(imgData.data, x, y, canvas.width);
      return new Circle(x, y, r, color, ctx);
    } else return null;
  }

What's happening here is we select a random point within the bounds of our canvas to place our next circle (using the random helper function we wrote earlier). We determine the size of the circle by dividing some number, in this case 50, by the square root of the variance, which is the standard deviation. As a sidenote, the choice of 50 is arbitrary; you can choose any number and all that will change is the average size of new circles.

Once we have a position and size, we loop through every circle we have drawn already and test if the new one is overlapping with any. If it isn't, we return a new Circle object created with the position and size from above and the pixel color at that position. If there is overlap, we return null.

We have just one more function to write now, which controls the drawing of circles on the canvas!


  function draw() {
    var count = 0, max = 10, attempts = 0;
    while (count < max) {
      let newC = newCircle();
      if (newC !== null) {
        circles.push(newC);
        ++count;
      }
      ++attempts;
      if (attempts > 250) {
        console.log("Done");
        return;
      }
    }
    ctx.clearRect(0,0,imgWidth,imgHeight);
    for (let c of circles) {
      c.drawCircle();
    }
    window.requestAnimationFrame(draw);
  }

We start this function off by declaring the variables count, a count for the valid new circles we have found, max, how many new circles we would like to draw with each loop, and attempts, how many times we have tried finding a new valid circle. While we have less circles than we want, we try to create a new circle. If it's valid, we add it to our array of circles and increment count. We always increment our number of attempts, and if it ever is greater than 250, we output 'Done' and end our program. Once we found enough new circles, we exit the while loop, clear the current canvas, draw all the circles we have, and repeat this process.

Voila! There you have it! Your very own circular redrawing of an image. I have included my version below which includes some code, that if you uncomment, will make each circle grow a little though every loop of the draw function.

See the Pen Image Recreation with Circles by Murad Khan (@muradkhan101) on CodePen.

Customizing the Drawing

Think of the code we wrote above as a basis for all the customizations you can implement. For example, you don't have to draw circles, you can use squares, or hexagons, or even triangles! You will just have to create your own shape objects and functions to test valid placement and to draw the shapes.

Another, simpler, customization you can make is to change how the sizing of the shapes is determined. Instead of basing it on the amount of detail at that point, you can base it on the amount of a certain color, or its index in the RGB array. There are tons of possibilities to explore, and I would love to see the ones that you make so send them to me when you come up with a new style!