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});