@s2ui/justified-gallery
Version:
A justifed gallery by s2ui.
106 lines (101 loc) • 4.29 kB
JavaScript
// Determines last row alignment (if not justified)
function calculateLastRowOffset(alignment, containerWidth, row, adjustedRowHeight, gap) {
const rowWidth = row.reduce((sum, photo) => sum + Math.round(adjustedRowHeight * photo.ratio), 0) +
(row.length - 1) * gap;
return alignment === "center"
? (containerWidth - rowWidth) / 2
: alignment === "right"
? containerWidth - rowWidth
: 0;
}
// Efficiently splits images into justified rows
function splitIntoRows(photos, containerWidth, targetRowHeight, gap) {
let rowWidth = 0, start = 0;
const rowLen = [];
photos.forEach((photo, i) => {
rowWidth += photo.ratio * targetRowHeight;
if (rowWidth + (i - start) * gap > containerWidth ||
i === photos.length - 1) {
rowLen.push([start, i, rowWidth]);
start = i + 1;
rowWidth = 0;
}
});
return rowLen;
}
// Optimized Image Aspect Ratio Calculation
async function loadImageAspectRatio(img) {
if (!img.complete || img.naturalWidth === 0 || img.naturalHeight === 0) {
await img.decode().catch(() => { }); // Avoids blocking on error
}
return { img, ratio: img.naturalWidth / img.naturalHeight };
}
async function computeLayout(options) {
const { container, rowHeight: targetRowHeight, gap, lastRow = "left", } = options;
const images = Array.from(container.querySelectorAll("img"));
if (!images.length)
return [];
// Load images with aspect ratios
const photos = await Promise.all(images.map(loadImageAspectRatio));
const containerWidth = container.clientWidth;
const rows = splitIntoRows(photos, containerWidth, targetRowHeight, gap);
const layout = [];
let currentTop = 0;
rows.forEach(([start, end, rowWidth], rowIndex) => {
const row = photos.slice(start, end + 1);
const isLastRow = rowIndex === rows.length - 1;
const canJustify = !(isLastRow && lastRow !== "justify");
// Calculate row height
const adjustedRowHeight = canJustify
? ((containerWidth - (row.length - 1) * gap) / rowWidth) * targetRowHeight
: Math.min(targetRowHeight, ((containerWidth - (row.length - 1) * gap) / rowWidth) *
targetRowHeight);
let leftOffset = isLastRow && lastRow !== "justify"
? calculateLastRowOffset(lastRow, containerWidth, row, adjustedRowHeight, gap)
: 0;
row.forEach((photo) => {
const width = Math.round(adjustedRowHeight * photo.ratio);
layout.push({
left: leftOffset,
top: currentTop,
width,
height: Math.round(adjustedRowHeight),
img: photo.img,
});
leftOffset += width + gap;
});
currentTop += Math.round(adjustedRowHeight) + gap;
});
return layout;
}
function renderGallery(layout, container) {
container.style.position = "relative";
container.style.height = `${layout[layout.length - 1].top + layout[layout.length - 1].height}px`; // Set container height
layout.forEach(({ img, left, top, width, height }) => {
img.style.position = "absolute";
img.style.transform = `translateX(${left}px) translateY(${top}px) translateZ(0)`;
img.style.width = `${width}px`;
img.style.height = `${height}px`;
// img.style.objectFit = "cover";
});
}
function JustifiedGallery(options) {
async function updateLayout() {
const layout = await computeLayout(options);
renderGallery(layout, options.container);
}
// Run layout initially
updateLayout();
// Debounced resize event listener
let resizeTimeout;
const onResize = () => {
clearTimeout(resizeTimeout);
resizeTimeout = window.setTimeout(updateLayout, 200); // Debounce for better performance
};
// Attach event listener for window resize
// window.addEventListener("resize", onResize);
window.addEventListener("resize", updateLayout); // to get the effect
// Return a cleanup function in case we need to remove the gallery
return () => window.removeEventListener("resize", onResize);
}
export { JustifiedGallery as default };