memories/src/services/Layout.ts

243 lines
5.8 KiB
TypeScript
Raw Normal View History

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(
2022-10-28 19:08:34 +00:00
input: {
width: number;
height: number;
forceSquare: boolean;
}[],
opts: {
rowWidth: number;
rowHeight: number;
squareMode: boolean;
numCols: number;
allowBreakout: boolean;
seed: number;
}
): {
2022-10-28 19:08:34 +00:00
top: number;
left: number;
width: number;
height: number;
rowHeight?: number;
}[] {
2022-10-28 19:08:34 +00:00
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;
}
}
2022-10-28 19:08:34 +00:00
// 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;
}
2022-10-28 19:08:34 +00:00
// 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;
}
2022-10-28 19:08:34 +00:00
// 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;
2022-10-17 19:20:42 +00:00
}
function flagMatrixStr(matrix: number[][], numFlag: number) {
2022-10-28 19:08:34 +00:00
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;
2022-10-17 23:46:43 +00:00
}
function mulberry32(a: number) {
2022-10-28 19:08:34 +00:00
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;
};
}