PluginProbe
Photonic Gallery & Lightbox for Flickr, SmugMug & Others / 3.34
Photonic Gallery & Lightbox for Flickr, SmugMug & Others v3.34
3.37 3.36 3.35 3.34 3.33 2.19 2.20 2.21 2.22 2.23 2.24 2.25 2.26 2.27 2.28 2.29 2.30 2.31 2.32 2.33 2.34 2.40 2.41 2.42 2.43 All 142 releases
photonic / include / js / front-end / src / Layouts / Mosaic.js

Mosaic.js in Photonic Gallery & Lightbox for Flickr, SmugMug & Others 3.34, at include/js/front-end/src/Layouts/Mosaic.js

297 lines 10.3 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 /**
2 * Photonic Mosaic Layout
3 * Arranges photos similar to JetPack tiles, but without any of the limitations of JetPack tiles. JS-based, responsive, and images are not affected by resolution issues.
4 *
5 * License: GPL V3.0
6 */
7 import {Core} from "../Core";
8 import * as Util from "../Util";
9
10 const getDistribution = (setSize, max, min) => {
11 const distribution = [];
12 let processed = 0;
13 while (processed < setSize) {
14 if (setSize - processed <= max && processed > 0) {
15 distribution.push(setSize - processed);
16 processed += setSize - processed;
17 }
18 else {
19 let current = Math.max(Math.floor(Math.random() * max + 1), min);
20 current = Math.min(current, setSize - processed);
21 distribution.push(current);
22 processed += current;
23 }
24 }
25 return distribution;
26 };
27
28 const arrayAlternate = (array, remainder) => array.filter((value, index) => index % 2 === remainder);
29
30 const setUniformHeightsForRow = array => {
31 // First, order the array by increasing height
32 array.sort((a, b) => a.height - b.height);
33
34 array[0].new_height = array[0].height;
35 array[0].new_width = array[0].width;
36
37 for (let i = 1; i < array.length; i++) {
38 array[i].new_height = array[0].height;
39 array[i].new_width = array[i].new_height * array[i].aspect_ratio;
40 }
41 const new_width = array.reduce((sum, p) => sum += p.new_width, 0);
42 return { elements: array, height: array[0].new_height, width: new_width, aspect_ratio: new_width / array[0].new_height };
43 };
44
45 const finalizeTiledLayout = (components, containers) => {
46 const cLength = components.length;
47 for (let c = 0; c < cLength; c++) {
48 const component = components[c];
49 let rowY = component.y,
50 otherRowHeight = 0,
51 container;
52 const ceLen = component.elements.length;
53 for (let e = 0; e < ceLen; e++) {
54 const element = component.elements[e];
55 if (element.photo_position !== undefined) {
56 // Component is a single image
57 container = containers[element.photo_position];
58 container.style.width = component.new_width + 'px';
59 container.style.height = component.new_height + 'px';
60 container.style.top = component.y + 'px';
61 container.style.left = component.x + 'px';
62 }
63 else {
64 // Component is a clique (element is a row). Widths and Heights of cliques have been calculated. But the rows in cliques need to be recalculated
65 element.new_width = component.new_width;
66 if (otherRowHeight === 0) {
67 element.new_height = element.new_width / element.aspect_ratio;
68 otherRowHeight = element.new_height;
69 }
70 else {
71 element.new_height = component.new_height - otherRowHeight;
72 }
73 element.x = component.x;
74 element.y = rowY;
75 rowY += element.new_height;
76 const totalWidth = element.elements.reduce((sum, p) => sum += p.new_width, 0);
77
78 let rowX = 0;
79 const eLength = element.elements.length;
80 for (let i = 0; i < eLength; i++) {
81 const image = element.elements[i];
82 image.new_width = element.new_width * image.new_width / totalWidth;
83 image.new_height = element.new_height; //image.new_width / image.aspect_ratio;
84 image.x = rowX;
85
86 rowX += image.new_width;
87
88 container = containers[image.photo_position];
89 container.style.width = Math.floor(image.new_width) + 'px';
90 container.style.height = Math.floor(image.new_height) + 'px';
91 container.style.top = Math.floor(element.y) + 'px';
92 container.style.left = Math.floor(element.x + image.x) + 'px';
93 }
94 }
95 }
96 }
97 };
98
99 const doImageLayout = (grid, needToWait) => {
100 const viewportWidth = Math.floor(grid.getBoundingClientRect().width),
101 triggerWidth = (isNaN(Photonic_JS.mosaic_trigger_width) || parseInt(Photonic_JS.mosaic_trigger_width) <= 0) ? 200 : parseInt(Photonic_JS.mosaic_trigger_width),
102 maxInRow = Math.floor(viewportWidth / triggerWidth),
103 minInRow = viewportWidth >= (triggerWidth * 2) ? 2 : 1,
104 photos = [];
105
106 let setSize;
107
108 const containers = [],
109 images = Array.from(grid.getElementsByTagName('img'));
110
111 images.forEach((image, position) => {
112 if (image.closest('.photonic-panel') != null) {
113 return;
114 }
115
116 const a = image.parentNode;
117 const div = a.parentNode;
118 div.setAttribute('data-photonic-photo-index', position);
119 containers[position] = div;
120
121 const height = needToWait ? image.naturalHeight : image.getAttribute('height'),
122 width = needToWait ? image.naturalWidth : image.getAttribute('width');
123
124 if (!(height === 0 || height === undefined || width === undefined)) {
125 const aspectRatio = (width) / (height);
126 photos.push({src: image.src, width: width, height: height, aspect_ratio: aspectRatio, photo_position: position});
127 }
128 });
129
130 setSize = photos.length;
131 const distribution = getDistribution(setSize, maxInRow, minInRow);
132
133 // We got our random distribution. Let's divide the photos up according to the distribution.
134 let groups = [], startIdx = 0;
135 distribution.forEach((size) => {
136 groups.push(photos.slice(startIdx, startIdx + size));
137 startIdx += size;
138 });
139
140 let groupY = 0;
141
142 // We now have our groups of photos. We need to find the optimal layout for each group.
143 groups.forEach((group) => {
144 // First, order the group by aspect ratio
145 group.sort((a, b) => a.aspect_ratio - b.aspect_ratio);
146
147 // Next, pick a random layout
148 let groupLayout;
149 if (group.length === 1) {
150 groupLayout = [1];
151 }
152 else if (group.length === 2) {
153 groupLayout = [1,1];
154 }
155 else {
156 groupLayout = getDistribution(group.length, group.length - 1, 1);
157 }
158
159 // Now, LAYOUT, BABY!!!
160 let cliqueF = 0,
161 cliqueL = group.length - 1,
162 cliques = [],
163 indices = [];
164
165 for (let i = 2; i <= maxInRow; i++) {
166 let index = groupLayout.indexOf(i);
167 while (-1 < index && cliqueF < cliqueL) {
168 // Ideal Layout: one landscape, one portrait. But we will take any 2 with contrasting aspect ratios
169 let clique = [], j = 0;
170 while (j < i && cliqueF <= cliqueL) {
171 clique.push(group[cliqueF++]); // One with a low aspect ratio
172 j++;
173 if (j < i && cliqueF <= cliqueL) {
174 clique.push(group[cliqueL--]); // One with a high aspect ratio
175 j++;
176 }
177 }
178 // Clique is formed. Add it to the list of cliques.
179 cliques.push(clique);
180 indices.push(index); // Keep track of the position of the clique in the row
181 index = groupLayout.indexOf(i, index + 1);
182 }
183 }
184
185 // The ones that are not in any clique (i.e. the ones in the middle) will be given their own columns in the row.
186 const remainder = group.slice(cliqueF, cliqueL + 1);
187
188 // Now let's layout the cliques individually. Each clique is its own column.
189 let rowLayout = [];
190 cliques.forEach((clique, cliqueIdx) => {
191 const toss = Math.floor(Math.random() * 2); // 0 --> Groups of smallest and largest, or 1 --> Alternating
192 let oneRow, otherRow;
193 if (toss === 0) {
194 // Group the ones with the lowest aspect ratio together, and the ones with the highest aspect ratio together.
195 // Lay one group at the top and the other at the bottom
196 const wide = Math.max(Math.floor(Math.random() * (clique.length / 2 - 1)), 1);
197 oneRow = clique.slice(0, wide);
198 otherRow = clique.slice(wide);
199 }
200 else {
201 // Group alternates together.
202 // Lay one group at the top and the other at the bottom
203 oneRow = arrayAlternate(clique, 0);
204 otherRow = arrayAlternate(clique, 1);
205 }
206
207 // Make heights consistent within rows:
208 oneRow = setUniformHeightsForRow(oneRow);
209 otherRow = setUniformHeightsForRow(otherRow);
210
211 // Now make widths consistent
212 oneRow.new_width = Math.min(oneRow.width, otherRow.width);
213 oneRow.new_height = oneRow.new_width / oneRow.aspect_ratio;
214 otherRow.new_width = oneRow.new_width;
215 otherRow.new_height = otherRow.new_width / otherRow.aspect_ratio;
216
217 rowLayout.push({elements: [oneRow, otherRow], height: oneRow.new_height + otherRow.new_height, width: oneRow.new_width, aspect_ratio: oneRow.new_width / (oneRow.new_height + otherRow.new_height), element_position: indices[cliqueIdx]});
218 });
219
220 rowLayout.sort((a, b) => a.element_position - b.element_position);
221
222 let orderedRowLayout = [];
223 for (let position = 0; position < groupLayout.length; position++) {
224 const cliqueExists = indices.indexOf(position) > -1;
225 if (cliqueExists) {
226 orderedRowLayout.push(rowLayout.shift());
227 }
228 else {
229 const rem = remainder.shift();
230 orderedRowLayout.push({ elements: [rem], height: rem.height, width: rem.width, aspect_ratio: rem.aspect_ratio });
231 }
232 }
233
234 // Main Row layout is fully constructed and ordered. Now we need to balance heights and widths of all cliques with the "remainder"
235 const totalAspect = orderedRowLayout.reduce((sum, p) => sum += p.aspect_ratio, 0);
236
237 let elementX = 0;
238 orderedRowLayout.forEach(component => {
239 component.new_width = component.aspect_ratio / totalAspect * viewportWidth;
240 component.new_height = component.new_width / component.aspect_ratio;
241 component.y = groupY;
242 component.x = elementX;
243 elementX += component.new_width;
244 });
245
246 groupY += orderedRowLayout[0].new_height;
247 finalizeTiledLayout(orderedRowLayout, containers);
248 });
249
250 grid.style.height = groupY + 'px';
251 };
252
253 export const Mosaic = (resized, jsLoaded, selector) => {
254 if (console !== undefined && Photonic_JS.debug_on !== '0' && Photonic_JS.debug_on !== '') console.time('Mosaic');
255 let selection = document.querySelectorAll(selector);
256
257 if (selector == null || selection.length === 0) {
258 selection = document.querySelectorAll('.photonic-mosaic-layout');
259 }
260
261 if (!resized && selection.length > 0) {
262 Core.showSpinner();
263 }
264
265 selection.forEach((grid) => {
266 if (!grid.hasChildNodes()) {
267 return;
268 }
269
270 if (grid.classList.contains('sizes-present')) {
271 Core.watchForImages(grid);
272 doImageLayout(grid, false);
273
274 Core.showSlideupTitle();
275 if (!resized && !jsLoaded) {
276 Core.hideLoading();
277 }
278 }
279 else {
280 Core.waitForImages(grid).then(() => {
281 if (!grid.classList.contains('sizes-present')) {
282 doImageLayout(grid, true);
283 }
284 Array.from(grid.getElementsByTagName('img')).forEach(image => Util.fadeIn(image));
285
286 Core.showSlideupTitle();
287 if (!resized && !jsLoaded) {
288 Core.hideLoading();
289 }
290 });
291 }
292 });
293 if (console !== undefined && Photonic_JS.debug_on !== '0' && Photonic_JS.debug_on !== '') console.timeEnd('Mosaic');
294 };
295
296 //Mosaic(false);
297