import justifiedLayout from "justified-layout"; /** * Generate the layout matrix. * * If we are in square mode, do this manually to get non-uniformity. * Otherwise, use flickr/justified-layout (at least for now). */ export function getLayout( input: { width: number; height: number; forceSquare: boolean; }[], opts: { rowWidth: number; rowHeight: number; squareMode: boolean; numCols: number; allowBreakout: boolean; seed: number; } ): { top: number; left: number; width: number; height: number; rowHeight?: number; }[] { if (input.length === 0) return []; if (!opts.squareMode) { return justifiedLayout(input, { containerPadding: 0, boxSpacing: 0, containerWidth: opts.rowWidth, targetRowHeight: opts.rowHeight, targetRowHeightTolerance: 0.1, }).boxes; } // RNG const rand = mulberry32(opts.seed); // Binary flags const FLAG_USE = 1 << 0; const FLAG_USED = 1 << 1; const FLAG_USE4 = 1 << 2; const FLAG_USE6 = 1 << 3; const FLAG_BREAKOUT = 1 << 4; // Create 2d matrix to work in const matrix: number[][] = []; // Fill in the matrix let row = 0; let col = 0; let photoId = 0; while (photoId < input.length) { // Check if we reached the end of row if (col >= opts.numCols) { row++; col = 0; } // Make sure we have this and the next few rows while (row + 3 >= matrix.length) { matrix.push(new Array(opts.numCols).fill(0)); } // Check if already used if (matrix[row][col] & FLAG_USED) { col++; continue; } // Use this slot matrix[row][col] |= FLAG_USE; // Check if previous row has something used // or something beside this is used // We don't do these one after another if ( !opts.allowBreakout || (row > 0 && matrix[row - 1].some((v) => v & FLAG_USED)) || (col > 0 && matrix[row][col - 1] & FLAG_USED) ) { photoId++; col++; continue; } // Number of photos left const numLeft = input.length - photoId - 1; // Number of photos needed for perfect fill after using n const needFill = (n: number) => opts.numCols - col - 2 + (n / 2 - 1) * (opts.numCols - 2); let canUse4 = // We have enough space row + 1 < matrix.length && col + 1 < opts.numCols && // This cannot end up being a widow (conservative) // Also make sure the next row gets fully filled, otherwise looks weird (numLeft === needFill(4) || numLeft >= needFill(4) + opts.numCols); let canUse6 = // Image is portrait input[photoId].height > input[photoId].width && // We have enough space row + 2 < matrix.length && col + 1 < opts.numCols && // This cannot end up being a widow (conservative) // Also make sure the next row gets fully filled, otherwise looks weird (numLeft === needFill(6) || numLeft >= needFill(6) + 2 * opts.numCols); let canBreakout = // First column only col === 0 && // Image is landscape input[photoId].width > input[photoId].height && // The next row gets filled (numLeft === 0 || numLeft >= opts.numCols); // Probably folders or tags or faces if (input[photoId].forceSquare) { // We are square already. Everything below is else-if. } // Full width breakout else if (canBreakout && rand() < (input.length > 0 ? 0.25 : 0.1)) { matrix[row][col] |= FLAG_BREAKOUT; for (let i = 1; i < opts.numCols; i++) { matrix[row][i] |= FLAG_USED; } } // Use 6 vertically else if (canUse6 && rand() < 0.2) { matrix[row][col] |= FLAG_USE6; matrix[row + 1][col] |= FLAG_USED; matrix[row + 2][col] |= FLAG_USED; matrix[row][col + 1] |= FLAG_USED; matrix[row + 1][col + 1] |= FLAG_USED; matrix[row + 2][col + 1] |= FLAG_USED; } // Use 4 box else if (canUse4 && rand() < 0.35) { matrix[row][col] |= FLAG_USE4; matrix[row + 1][col] |= FLAG_USED; matrix[row][col + 1] |= FLAG_USED; matrix[row + 1][col + 1] |= FLAG_USED; } // Go ahead photoId++; col++; } // Square layout matrix const absMatrix: { top: number; left: number; width: number; height: number; }[] = []; let currTop = 0; row = 0; col = 0; photoId = 0; while (photoId < input.length) { // Check if we reached the end of row if (col >= opts.numCols) { row++; col = 0; currTop += opts.rowHeight; continue; } // Skip if used if (!(matrix[row][col] & FLAG_USE)) { col++; continue; } // Create basic object const sqsize = opts.rowHeight; const p = { top: currTop, left: col * sqsize, width: sqsize, height: sqsize, rowHeight: opts.rowHeight, }; // Use twice the space const v = matrix[row][col]; if (v & FLAG_USE4) { p.width *= 2; p.height *= 2; col += 2; } else if (v & FLAG_USE6) { p.width *= 2; p.height *= 3; col += 2; } else if (v & FLAG_BREAKOUT) { p.width *= opts.numCols; p.height = (input[photoId].height * p.width) / input[photoId].width; p.rowHeight = p.height; col += opts.numCols; } else { col++; } absMatrix.push(p); photoId++; } return absMatrix; } function flagMatrixStr(matrix: number[][], numFlag: number) { let str = ""; for (let i = 0; i < matrix.length; i++) { const rstr = matrix[i] .map((v) => v.toString(2).padStart(numFlag, "0")) .join(" "); str += i.toString().padStart(2) + " | " + rstr + "\n"; } return str; } function mulberry32(a: number) { return function () { var t = (a += 0x6d2b79f5); t = Math.imul(t ^ (t >>> 15), t | 1); t ^= t + Math.imul(t ^ (t >>> 7), t | 61); return ((t ^ (t >>> 14)) >>> 0) / 4294967296; }; }