fifteen
values
A visualization of the famous Fifteen puzzle, where the hash of the input is used to move the tiles around.
🔗 Layout
A lot of these pieces work on canvases of all sizes and shapes (as long as they’re rectangular). Many pieces can reasonably centered in a “square” based on the minimum of the width and height of the canvas, but for this piece I wanted to intelligently lay out the individual bits.
Given a width, height, and number of things, attempt to product an aestheticallypleasing layout as defined by:

The number of rows

The number of columns

The size of each item

The space between each item
function computeIdealLayout(w, h, grids) {
const idealNegativeSpace = (w * h) / 2
let bestLayout = {
negativeSpace: Infinity,
}
for (
let columns = 1;
columns < grids.length;
columns++
) {
for (
let puzzleSize = 8;
puzzleSize < 200;
puzzleSize += 4
) {
for (
let padding = puzzleSize / 4;
padding < puzzleSize;
padding += 1
) {
let rows = Math.ceil(
grids.length / columns
)
let layoutWidth =
columns * puzzleSize +
(columns + 1) * padding
let layoutHeight =
rows * puzzleSize +
(rows + 1) * padding
// Doesn't fit, continue
if (
layoutWidth > w 
layoutHeight > h
) {
continue
}
const negativeSpace =
// Size of the canvas
w * h 
// Subtract the area of the puzzles
puzzleSize *
puzzleSize *
columns *
rows
if (
Math.abs(
idealNegativeSpace 
negativeSpace
) <
Math.abs(
idealNegativeSpace 
bestLayout.negativeSpace
)
) {
bestLayout = {
negativeSpace,
columns,
rows,
layoutWidth,
layoutHeight,
padding,
puzzleSize,
}
}
}
}
}
return bestLayout
}
My particular approach has two assumptions coded in. First, an individual “thing” has a minimum size of 8, a maximum of 200, and must be a multiple of 2. Second, the padding should fall between 1/4th of the item size, to the item size.
After that, we simply loop a bunch of times over the different parameters to generate a triple (rows, padding, puzzleSize). For each of these we compute (1) if the pieces even fit on the canvas with these parameters and (2) how much whitespace will be left after everything is laid out.
We then pick the candidate whose whitespace is closest to 1/2 of the entire area of the canvas. This can be tweaked as we see fit (see
idealNegativeSpace
).
I’m shocked it works and runs quickly! There are many unnecessary loops and there is likely a better way to search for a good layout  but it’s fast enough for typing so it’s fast enough for me.