Perfect Masonry

UPDATE
I have created a jQuery plugin called mason.js that is a completely refined script that allows for many options and configurations.
Fork it on Github!

It’s been a while since my last post, crazy busy right now – but I wanted to share something that I recently came across that left me scratching my head for far too long. Perfect Masonry.



Currently there are a few plugins out there that allow you to arrange items into a nice grid – Masonry and its smarter brother Isotope. The issue with these plugins is that sometimes there are gaps in your grid.

A project I am working on required me to create a perfect grid of randomly sized elements with no gaps. I started off by trying Masonry and Isotope the first results seemed promising but I quickly found out that no matter what, I would wind up with gaps ( FAIL ). So how to overcome this issue?

First I had to step back and think about the problem – here is the setup:

I have a grid that has to be responsive, so the number of columns and size of those columns will vary.

Next each element will be a random size, meaning I have no idea what size elements will fall where.

I started by floating the elements left positioned relative, which gave me a nice grid, but would bump rows down and leave these nasty holes. I needed a way to solve that – this is where you need to really think.

In my example my grid consists of an endless number of elements, these elements have a ratio to them, 1×1, 1×2, 2×2 – somehow I need to pack those elements into a perfect grid. Now because my grid is limitless I have the opportunity to reuse certain elements to fill these gaps, in other applications of this you could set aside special elements to be used.

Theory -
I spent way too much time thinking about rows and columns – what you need to do is think about your grid as a matrix. There are a number of mathematical equations and problems out there, mainly Bin-Packing problem and Knapsack Packing Problem.

After spending some time looking at both of those problems as well as doing some searching around the interwebs I started to come up with a theory of how to make this work.

The basic idea for what we are wanting to do is to detect where missing spaces are and fill those in with new elements also position them in those gaps.

Say we have a page that is 1200px wide. We have defined break points for columns to fit in there by saying that on a 1200px wide screen we will allow 4 columns. Now we need to do some math.

We need to start by creating our grid object, this will contain some basic options to get things started, such as selectors and elements we want to be positioning.

(function(){
  MASON = function(par,el,sel,ratio,sizes){
  var self = this;
  self.options = {
    parent: par,
    el: el,
    sel: sel,
    ratio: ratio,
    block: {
      height: 0,
      width: 0
    },
    matrix: []
  }
  return self;
}
})();

What we’ve done here is make an object that we can create at any time and feed a few options:

  • par – parent
  • el – container element
  • sel – object selector
  • ratio – Our block ratio
  • sizes – array of sizes / ratios

Now that we’ve made our object lets start to do some math!

The next step is to create “blocks” these blocks live in your head and in the computers memory. In my case my blocks had to have a ratio height of 1.5 – what does 1.5 mean?

I had previously done calculations on my ratios to determine that the smallest ratio I had was 1.5 x 2 – this was based on the creative given to me and also based on the area I needed each block to have at the very minimum.

we need to create two primary functions, a setup function which will give us our first calculations, as well as column finder.

Because I do not know my final height I need to use my windows width to determine the height and width of each “block”

(function(){
  MASON = function(par,el,sel,ratio,sizes){
  var self = this;
  self.options = {
    parent: par,
    el: el,
    sel: sel,
    ratio: ratio,
    block: {
      height: 0,
      width: 0
    },
    matrix: []
  }
 
  /*
   * Get sizes
  */
  self.sizes = [];
  for(var i = 0; i < sizes.length; i ++){
    self.sizes[i] = sizes[i];
  }
 
  /*
   * Setup our options and do some basic math
  */
  self.setup = function(){
    self.options.block.height = (( window.innerWidth / self.cols() ) / self.options.ratio ).toFixed(0);
    self.options.block.width = ( window.innerWidth / self.cols() );
  }
  self.cols = function(){
    var w = Math.floor(window.innerWidth);
    var cols = 0;
 
    if(w < 480){
      cols = 1;
    }
    else if (w > 480 && w < 780){
      cols = 2;
    }
    else if (w > 780 && w < 1080){
      cols = 3;
    }
    else if( w > 1080 && w < 1320){
      cols = 4;
    }
    else if( w > 1320 && w < 1680){
      cols = 5
    }
    else {
      cols = 6;
    }
    return cols;
  }
  return self;
}
})();

we set our block to be considered the smallest block allowed on screen. On a 1200px wide screen that should be around 200h x 300w.

Great so now we know our block size meaning if we were to set this into a for loop at 100 we would wind up with a perfect grid of 200×300 rectangles.

Now that we know what our block size is we can easily size our elements based around that… meaning a 1×1 would = 200×300, a 1×2 would be 200×600, and a 2×2 would be 600×600.

Do you see where this is going?

A note – You may notice the call for toFixed(0) on the height, this takes our number for height and brings what could be a crazy decimal number down to 0 – this makes for faster calculations later.

Next step is to randomly place our current elements – I have on my html side a page that has a bunch of divs with the class of “box” which in this case is our selector, they are contained in an element called “gallery”.

<html>
  <head>
    <title>Perfect Masonry</title>
    <link rel='stylesheet' href='css/style.css' />
  </head>
  <body>
    <div id='gallery'>
      <div class='box'></div>
      <div class='box'></div>
      <div class='box'></div>
      <div class='box'></div>
      <div class='box'></div>
      <div class='box'></div>
      <div class='box'></div>
      <div class='box'></div>
      <div class='box'></div>
      <div class='box'></div>
      <div class='box'></div>
      <div class='box'></div>
      <div class='box'></div>
      <div class='box'></div>
      <div class='box'></div>
      <div class='box'></div>
      <div class='box'></div>
      <div class='box'></div>
      <div class='box'></div>
      <div class='clear'></div>
    </div>
  </body>
</html>

Take note of the clear at the bottom of our boxes! This is there to help define the height of our container!

Cool. Now we have our HTML set as well as our basic javascript! Now lets set up a little css..

* {
  margin: 0px;
}
body {
  height: 100%;
  width: 100%;
}
#gallery {
  width: 100%;
  height: 100%;
  position: relative;
}
.box {
  background-color: #ff0000;
  float: left;
  position: relative;
}
.clear {
  clear: both;
  position: relative;
}

rock and roll – now all of our box elements are floating left – now lets give them sizes. This is where the good stuff happens. Before we jump into this a few things. FORGET ABOUT COLUMNS AND ROWS while these do play an part in what we’re about to do they don’t have any real meaning in this case – this is totally abstract and should be thought of in that way. That said lets do this!

I am going to make a new function all size_boxes() size boxes is going to size my current elements and create a matrix of my grid so I can detect the missing spaces and fill in the gaps.

self.size_boxes = function(){
  /*
   * Create Columns 
  */
  var cols = self.cols();
  var matrix = []; // define this for later use
 
  /*
   * size our element
   * if column count is 1 set size else make sizes
  */
  if( cols = 1 ){
    self.options.sel.height(self.options.block.height);
    self.options.sel.width(self.options.block.width);
  }
  else {
    self.options.sel.each(function(){
      $this = $(this);
 
      var ran = Math.floor(Math.random().self.sizes.length);
      var ranSize = self.sizes[ran];
 
      $this.data('size',ran); // we will use this later
 
      var h = (self.options.block.height * self.sizes[ran][1]).toFixed(2); // avoid trailing decimals
      var w = self.options.block.width * self.sizes[ran][0];
 
      $this.height(h);
      $this.width(w);
    });
  }
}

Okay so this code should be setting elements on our page with random sizes and since they are floated left we will see a page that has gaps between our elements.

Now for the fun stuff – Matrix time. I had a hard time at first attempting to think of this grid as a matrix because I kept thinking of rows and columns. I made a post to stackoverflow the day before this post and JoshNaro suggested this matrix idea again. So I started thinking…

Okay say we have a matrix of a four column grid. I have a large item ( 2×2 ) and tall item ( 1×2 ) and one small items ( 1×1 )… how does this play.. well if we think about it like this…

[true,true,true,true]
[true,true,true,false]

I know that the false statement means there is an empty space… but how do I know for sure? this is where you need to start thinking abstract thoughts…

First step is back to the “block” thinking… say we have a grid you dont know how tall it is, but you know how many items are in in. also you’ve just placed those items thus giving its container a solid height meaning you now know the total area of your grid via ( h x w = a )! so we need to calculate how many “rows” we could potentially make with our grid using our “blocks”

var h = self.options.el.height();
var bh = h / self.options.block.height;

Now we know how many rows we will have and thanks to our nifty self.cols() function we can determine the number of columns.

Next we need to construct a blank matrix that represents an imaginary “grid” of our “blocks” – remember this doesn’t actually exist anywhere other than in an array.

for(var i = 0; i < bh; i ++ ){
  matrix[i] = [];
  for(var m = 0; m < cols; m ++){
    matrix[i][m] = false;
  }
}

So what we just did was create a matrix that represents our grid based on our columns and our rows, this matrix is full of booleans set to false. We do this because at this point the matrix has nothing in it.

Now lets add values and do some logic…

self.options.sel.each(function(){
  var size = parseFloat($(this).data('size')); // Told you we'd use this!
 
  /*
   * Lets determine in "blocks" where the elements are positioned.
  */
  var c = Math.round($(this).position().left / self.options.block.width);
  var r = Math.round($(this).position().top / self.options.block.height);
 
  var h = self.sizes[size][1];
  var w = self.sizes[size][0];
  var a = h*w;
 
  for(var i = 0; i < a; i++){
    for(var bh = 0; bh < h; bh++){
      matrix[r+bh][c] = true;
      for(var bw = 0; bw < w; bw++){
        matrix[r+bh][c+bw] = true;
      }
    }					
  }
});

Okay so now we have created a matrix based around our sized objects… again this is more theory than practice but I will be open sourcing this code so if you have a good solution for this portion please feel free to fork and issue a pull request ;)

I have updated the script to be completely dynamic now it will do all of these calculations for you :)

Now we should have a matrix that is holding a representation of our current elements on our “grid” based on blocks.

keeping up? lets do more…

Now we need to determine missing spaces – we have already outlined those missing spaces via our matrix and booleans. We will detect these false booleans and determine their position based on a for loop and their index inside of the matrix. We will take that number times our block dimensions which should give us an X and Y to place the item. Then we append it to our container element.

for(var i = 0; i < matrix.length; i ++ ) {
  for(var c = 0; c < matrix[i].length; i ++ ){
    if( matrix[i][c] == false ){
      var h = self.options.block.height;
      var w = self.options.block.width;
 
      var x = ( i * h ).toFixed(2);
      var y = ( c * w );
 
      self.options.el.append("<div class='box filler' style='height:"+h+"px; width:"+w+"px; top:"+x+" top:"+y+"px;'></div>");
      }
    }
  }
}

SO there you have it – a perfect grid!

Check out the DEMO

Fork It on Github

A note – there appears to be slight bug, I believe due to rounding, that may add a few extra pixels to our placed elements causing them to be off by 1 – 6px this only happens at certain resolutions. Still working to perfect the code but the theory is all there.

This entry was posted in CSS, javascript. Bookmark the permalink.

6 Responses to Perfect Masonry

  1. Paul McClean says:

    I’m seeing a consistent 8-10 pixels of horizontal scrolling overflow.

  2. vivek says:

    U r my new Hero … on layout redesign…. Tons of Thanks … for an awesome plugin …… :)

  3. Nadia says:

    Thanks a lot!! It’s very-veeeeery helpfull in my current project.
    You are my new Hero too))

  4. Eva says:

    Hello, im looking for this one but with horizontal scroll, it is possible?
    thank you very much!

Leave a Reply

Your email address will not be published. Required fields are marked *

You may use these HTML tags and attributes: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <strike> <strong>