<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);
}
This Pen doesn't use any external CSS resources.