UNPKG

@s2ui/justified-gallery

Version:
106 lines (101 loc) 4.29 kB
// 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 };