| 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 |
|