91 lines
3.7 KiB
JavaScript
91 lines
3.7 KiB
JavaScript
// Justified photo grid layout (Google Photos style)
|
|
// Inspired by https://github.com/flickr/justified-layout
|
|
(function () {
|
|
const SPACING = 3;
|
|
const PADDING = 3;
|
|
// Photos that all share the same aspect ratio give rows of identical height,
|
|
// which reads as a contact sheet. Some photos are therefore displayed with a
|
|
// slightly different ratio than their own: `object-fit: cover` trims the
|
|
// edges, and the justified rows become irregular on their own.
|
|
const CROP_STEPS = [0.8, 1, 1, 1.25]; // display ratio multipliers, drawn per photo
|
|
|
|
// id -> multiplier, drawn once per page load and kept on resize (and when a
|
|
// photo is deleted) so the gallery does not reshuffle while it is looked at.
|
|
const cropPlan = new Map();
|
|
|
|
function displayRatio(item, index) {
|
|
const w = parseInt(item.dataset.width) || 1;
|
|
const h = parseInt(item.dataset.height) || 1;
|
|
const id = item.dataset.slideName || 'i' + index;
|
|
if (!cropPlan.has(id)) {
|
|
cropPlan.set(id, CROP_STEPS[Math.floor(Math.random() * CROP_STEPS.length)]);
|
|
}
|
|
return (w / h) * cropPlan.get(id);
|
|
}
|
|
|
|
function applyLayout() {
|
|
const container = document.getElementById('lightgallery');
|
|
if (!container) return;
|
|
|
|
const items = [...container.querySelectorAll('.photo-item')];
|
|
if (!items.length) return;
|
|
|
|
const containerWidth = container.offsetWidth - PADDING * 2;
|
|
const TARGET_HEIGHT = Math.min(220, Math.floor(containerWidth * 0.4));
|
|
const ratios = items.map(displayRatio);
|
|
|
|
// Group items into rows
|
|
const rows = [];
|
|
let rowStart = 0;
|
|
while (rowStart < ratios.length) {
|
|
let sum = 0;
|
|
let end = rowStart;
|
|
while (end < ratios.length) {
|
|
sum += ratios[end];
|
|
const rowWidth = sum * TARGET_HEIGHT + SPACING * (end - rowStart);
|
|
end++;
|
|
if (rowWidth >= containerWidth) break;
|
|
}
|
|
rows.push({ start: rowStart, end });
|
|
rowStart = end;
|
|
}
|
|
|
|
// Position each item
|
|
let top = PADDING;
|
|
rows.forEach(({ start, end }, rowIndex) => {
|
|
const isLastRow = rowIndex === rows.length - 1;
|
|
const count = end - start;
|
|
const sumRatios = ratios.slice(start, end).reduce((a, b) => a + b, 0);
|
|
const totalSpacing = SPACING * (count - 1);
|
|
|
|
// Fill the container width, except for a last row that isn't full:
|
|
// it keeps the target height, but never wider than the container.
|
|
const fitHeight = (containerWidth - totalSpacing) / sumRatios;
|
|
const rowHeight = isLastRow ? Math.min(TARGET_HEIGHT, fitHeight) : fitHeight;
|
|
|
|
let left = PADDING;
|
|
for (let i = start; i < end; i++) {
|
|
const width = Math.round(ratios[i] * rowHeight);
|
|
const height = Math.round(rowHeight);
|
|
const item = items[i];
|
|
item.style.top = top + 'px';
|
|
item.style.left = left + 'px';
|
|
item.style.width = width + 'px';
|
|
item.style.height = height + 'px';
|
|
left += width + SPACING;
|
|
}
|
|
top += Math.round(rowHeight) + SPACING;
|
|
});
|
|
|
|
container.style.height = (top - SPACING + PADDING) + 'px';
|
|
items.forEach(item => {
|
|
item.style.visibility = 'visible';
|
|
const img = item.querySelector('img[data-lazy]');
|
|
if (img) img.src = img.dataset.lazy;
|
|
});
|
|
}
|
|
|
|
window.applyJustifiedLayout = applyLayout;
|
|
document.addEventListener('DOMContentLoaded', applyLayout);
|
|
window.addEventListener('resize', applyLayout);
|
|
})();
|