grain.social is a photo sharing platform built on atproto.
1// deno-lint-ignore-file 2 3let masonryObserverInitialized = false; 4let layoutMode = "justified"; 5 6function computeLayout() { 7 if (layoutMode === "masonry") { 8 computeMasonry(); 9 } else { 10 computeJustified(); 11 } 12} 13 14function toggleLayout(layout = "justified") { 15 layoutMode = layout; 16 computeLayout(); 17} 18 19function computeMasonry() { 20 const container = document.getElementById("masonry-container"); 21 if (!container) return; 22 23 const spacing = 8; 24 const containerWidth = container.offsetWidth; 25 26 if (containerWidth === 0) { 27 requestAnimationFrame(computeMasonry); 28 return; 29 } 30 31 const columns = containerWidth < 640 ? 1 : 3; 32 33 const columnWidth = (containerWidth + spacing) / columns - spacing; 34 const columnHeights = new Array(columns).fill(0); 35 const tiles = container.querySelectorAll(".masonry-tile"); 36 37 tiles.forEach((tile) => { 38 const imgW = parseFloat(tile.dataset.width); 39 const imgH = parseFloat(tile.dataset.height); 40 if (!imgW || !imgH) return; 41 42 const aspectRatio = imgH / imgW; 43 const renderedHeight = aspectRatio * columnWidth; 44 45 let shortestIndex = 0; 46 for (let i = 1; i < columns; i++) { 47 if (columnHeights[i] < columnHeights[shortestIndex]) { 48 shortestIndex = i; 49 } 50 } 51 52 const left = (columnWidth + spacing) * shortestIndex; 53 const top = columnHeights[shortestIndex]; 54 55 Object.assign(tile.style, { 56 position: "absolute", 57 width: `${columnWidth}px`, 58 height: `${renderedHeight}px`, 59 left: `${left}px`, 60 top: `${top}px`, 61 }); 62 63 columnHeights[shortestIndex] = top + renderedHeight + spacing; 64 }); 65 66 container.style.height = `${Math.max(...columnHeights)}px`; 67} 68 69function computeJustified() { 70 const container = document.getElementById("masonry-container"); 71 if (!container) return; 72 73 const spacing = 8; 74 const containerWidth = container.offsetWidth; 75 76 if (containerWidth === 0) { 77 requestAnimationFrame(computeJustified); 78 return; 79 } 80 81 const tiles = Array.from(container.querySelectorAll(".masonry-tile")); 82 let currentRow = []; 83 let rowAspectRatioSum = 0; 84 let yOffset = 0; 85 86 // Clear all styles before layout 87 tiles.forEach((tile) => { 88 Object.assign(tile.style, { 89 position: "absolute", 90 left: "0px", 91 top: "0px", 92 width: "auto", 93 height: "auto", 94 }); 95 }); 96 97 for (let i = 0; i < tiles.length; i++) { 98 const tile = tiles[i]; 99 const imgW = parseFloat(tile.dataset.width); 100 const imgH = parseFloat(tile.dataset.height); 101 if (!imgW || !imgH) continue; 102 103 const aspectRatio = imgW / imgH; 104 currentRow.push({ tile, aspectRatio, imgW, imgH }); 105 rowAspectRatioSum += aspectRatio; 106 107 // Estimate if row is "full" enough 108 const estimatedRowHeight = 109 (containerWidth - (currentRow.length - 1) * spacing) / rowAspectRatioSum; 110 111 // If height is reasonable or we're at the end, render the row 112 if (estimatedRowHeight < 300 || i === tiles.length - 1) { 113 let xOffset = 0; 114 115 for (const item of currentRow) { 116 const width = estimatedRowHeight * item.aspectRatio; 117 Object.assign(item.tile.style, { 118 position: "absolute", 119 top: `${yOffset}px`, 120 left: `${xOffset}px`, 121 width: `${width}px`, 122 height: `${estimatedRowHeight}px`, 123 }); 124 xOffset += width + spacing; 125 } 126 127 yOffset += estimatedRowHeight + spacing; 128 currentRow = []; 129 rowAspectRatioSum = 0; 130 } 131 } 132 133 container.style.position = "relative"; 134 container.style.height = `${yOffset}px`; 135} 136 137function observeMasonry() { 138 if (masonryObserverInitialized) return; 139 masonryObserverInitialized = true; 140 141 const container = document.getElementById("masonry-container"); 142 if (!container) return; 143 144 // Observe parent resize 145 if (typeof ResizeObserver !== "undefined") { 146 const resizeObserver = new ResizeObserver(() => computeLayout()); 147 if (container.parentElement) { 148 resizeObserver.observe(container.parentElement); 149 } 150 } 151 152 // Observe inner content changes (tiles being added/removed) 153 const mutationObserver = new MutationObserver(() => { 154 computeLayout(); 155 }); 156 157 mutationObserver.observe(container, { 158 childList: true, 159 subtree: true, 160 }); 161} 162 163document.addEventListener("DOMContentLoaded", () => { 164 computeMasonry(); 165 observeMasonry(); 166});