<div><img class="draggable" src="https://source.unsplash.com/daily" /></div>
<div><img class="draggable" src="https://source.unsplash.com/daily?architecture" /></div>
<div><img class="draggable" src="https://source.unsplash.com/daily?dancing" /></div>
<div><img class="draggable" src="https://source.unsplash.com/daily?music" /></div>
<div><img class="draggable" src="https://source.unsplash.com/daily?nature" /></div>
<div><img class="draggable" src="https://source.unsplash.com/daily?fashion" /></div>
<div><img class="draggable" src="https://source.unsplash.com/daily?business" /></div>
<div><img class="draggable" src="https://source.unsplash.com/daily?cat" /></div>
<div><img class="draggable" src="https://source.unsplash.com/daily?portrait" /></div>
<div><img class="draggable" src="https://source.unsplash.com/daily?model" /></div>
<div><img class="draggable" src="https://source.unsplash.com/daily?music" /></div>
<div><img class="draggable" src="https://source.unsplash.com/daily?nature" /></div>
<div><img class="draggable" src="https://source.unsplash.com/daily?fashion" /></div>
<div><img class="draggable" src="https://source.unsplash.com/daily?flower" /></div>
<div><img class="draggable" src="https://source.unsplash.com/daily?dancing" /></div>
<div><img class="draggable" src="https://source.unsplash.com/daily?dog" /></div>
<div><img class="draggable" src="https://source.unsplash.com/daily?boat" /></div>
<div><img class="draggable" src="https://source.unsplash.com/daily?people" /></div>
html, body {
  height: 100%;
}
body {
  margin: 0px;
  touch-action: none;
  overflow: hidden;
}

div {
  margin: 2px;
  float: left;
  position: relative;
  
  &.dragging {
    z-index: 1;
    img {
      object-fit: contain;
    }
  }
}

img {
  display: block;
  width: 100%;
  height: 100%;
  object-fit: cover;
  will-change: transform;
}
View Compiled
const MAX_ROW_NUM = 5; // the maximum amount of images per row
const MIN_ROW_NUM = 2; // the minimum amount of images per row
const GAP = 2; // the gap inbetween images in pixels. Has to match the CSS.
const ROW_HEIGHTS = [33, 66, 100, 133, 150]; // want even rows? np! remove all but one here.

// these are all of our images
const imgs = Array.from(document.querySelectorAll('img'));

// main masonry function
function masonry() {
  console.time('recalc masonry..');
  
  // in production code you'd use loops instead of all these
  // array functions... but they explain the code a little better.
  const images = Array.from(imgs);
  while(images.length) {
    
    // Array that stores our currently best candidate.
    // 0: offset from viewport width
    // 1: array of images for that candidate
    // 2: the row height of that candidate
    const bestCandidate = [1000, null, null];
     
    // test 3-5 cols
    for(let i=MIN_ROW_NUM; i <= MAX_ROW_NUM; i++) {
      if(images.length >= i) {
        let candidates = images.slice(0, i);
        
        // test ~10 different row heights
        ROW_HEIGHTS.forEach(rowHeight => {
          let width = candidates.reduce((a, c) => a + (rowHeight * (c.naturalWidth / c.naturalHeight)), 0);
          let delta = (window.innerWidth - GAP * i * 2) - width;
          if(Math.abs(delta) < bestCandidate[0]) {
            bestCandidate[0] = delta;
            bestCandidate[1] = candidates;
            bestCandidate[2] = rowHeight;
          }          
        });

      }
    }

    if(bestCandidate[1]) {
      bestCandidate[1].forEach(img => {
        let crop = bestCandidate[0] / bestCandidate[1].length;
        img.parentNode.style.height = bestCandidate[2] + 'px';
        img.parentNode.style.width = Math.floor((bestCandidate[2] * (img.naturalWidth / img.naturalHeight) + crop)) + 'px';
      });
    } else {
      // means there's not even enough left for one row, so might as well
      // remove previous styles to make sure the image displays fully
      images[0].parentNode.style.width = '';
      images[0].parentNode.style.height = '';
    }
 
    images.splice(0, bestCandidate[1] ? bestCandidate[1].length : 1);
  }
  
  console.timeEnd('recalc masonry..');
  
}

// run masonry on load
// (when we have all image dimensions...luckily we'll have them cached usually)
window.onload = () => {
  masonry();
};

// run masonry debounced on resize
let resizeTimer;
window.onresize = () => {
  clearTimeout(resizeTimer);
  resizeTimer = setTimeout(function() {
    console.log('run debounce');
    masonry();  
  }, 32);
}

// draggy drag..
for(let img of imgs) {
  img.addEventListener('customdragstart', function(e) {
    this.parentNode.classList.add('dragging');
  }, false);
  img.addEventListener('customdragstop', function(e) {
    this.parentNode.classList.remove('dragging');
  }, false);
}
Run Pen

External CSS

This Pen doesn't use any external CSS resources.

External JavaScript

  1. https://codepen.io/pbakaus/pen/NWqemQG.js