bentogreed
Version:
A lightweight, framework-agnostic library for generating bento grid layouts
229 lines (228 loc) • 7.94 kB
JavaScript
;
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
function squarifiedLayout(tiles, rect, gutter = 0) {
if (tiles.length === 0) return [];
if (tiles.length === 1) {
const tile = tiles[0];
return [
{
id: tile.id,
area: tile.area,
rect: {
x: rect.x + gutter / 2,
y: rect.y + gutter / 2,
width: Math.max(0, rect.width - gutter),
height: Math.max(0, rect.height - gutter)
}
}
];
}
const totalArea = tiles.reduce((sum, t) => sum + t.area, 0);
const availableArea = rect.width * rect.height;
const scale = totalArea === 0 ? 0 : availableArea / totalArea;
const normalizedTiles = tiles.map((tile) => ({
...tile,
normalizedArea: tile.area * scale
}));
return squarify(normalizedTiles, rect, gutter);
}
function squarify(tiles, rect, gutter) {
const result = [];
let startIndex = 0;
let remainingRect = { ...rect };
while (startIndex < tiles.length && remainingRect.width > 0 && remainingRect.height > 0) {
const horizontal = remainingRect.width >= remainingRect.height;
const row = getNextRow(tiles, startIndex, remainingRect, horizontal);
const rowArea = row.reduce((sum, tile) => sum + tile.normalizedArea, 0);
const thickness = horizontal ? rowArea / remainingRect.width : rowArea / remainingRect.height;
const primarySpan = horizontal ? remainingRect.width : remainingRect.height;
let cursor = horizontal ? remainingRect.x : remainingRect.y;
for (let index = 0; index < row.length; index++) {
const tile = row[index];
const tileSpan = primarySpan === 0 ? 0 : tile.normalizedArea / rowArea * primarySpan;
const tileWidth = horizontal ? tileSpan : thickness;
const tileHeight = horizontal ? thickness : tileSpan;
const gutterPrimaryStart = index === 0 ? gutter / 2 : gutter;
const gutterPrimaryEnd = index === row.length - 1 ? gutter / 2 : gutter;
const gutterSecondaryStart = gutter / 2;
const gutterSecondaryEnd = gutter / 2;
const x = horizontal ? cursor + gutterPrimaryStart : remainingRect.x + gutterSecondaryStart;
const y = horizontal ? remainingRect.y + gutterSecondaryStart : cursor + gutterPrimaryStart;
const width = horizontal ? Math.max(0, tileWidth - gutterPrimaryStart - gutterPrimaryEnd) : Math.max(0, tileWidth - gutterSecondaryStart - gutterSecondaryEnd);
const height = horizontal ? Math.max(0, tileHeight - gutterSecondaryStart - gutterSecondaryEnd) : Math.max(0, tileHeight - gutterPrimaryStart - gutterPrimaryEnd);
result.push({
id: tile.id,
area: tile.area,
rect: { x, y, width, height }
});
cursor += tileSpan;
}
if (horizontal) {
remainingRect = {
x: remainingRect.x,
y: remainingRect.y + thickness,
width: remainingRect.width,
height: Math.max(0, remainingRect.height - thickness)
};
} else {
remainingRect = {
x: remainingRect.x + thickness,
y: remainingRect.y,
width: Math.max(0, remainingRect.width - thickness),
height: remainingRect.height
};
}
startIndex += row.length;
}
return result;
}
function getNextRow(tiles, startIndex, rect, horizontal) {
if (startIndex >= tiles.length) return [];
const row = [tiles[startIndex]];
let worstAspect = getWorstAspect(row, rect, horizontal);
for (let i = startIndex + 1; i < tiles.length; i++) {
row.push(tiles[i]);
const nextWorst = getWorstAspect(row, rect, horizontal);
if (nextWorst > worstAspect) {
row.pop();
break;
}
worstAspect = nextWorst;
}
return row;
}
function getWorstAspect(row, rect, horizontal) {
const rowArea = row.reduce((sum, tile) => sum + tile.normalizedArea, 0);
if (rowArea === 0) return 0;
const primarySpan = horizontal ? rect.width : rect.height;
const thickness = horizontal ? rowArea / Math.max(primarySpan, Number.EPSILON) : rowArea / Math.max(primarySpan, Number.EPSILON);
let worst = 0;
for (const tile of row) {
const tileSpan = primarySpan === 0 ? 0 : tile.normalizedArea / rowArea * primarySpan;
const width = horizontal ? tileSpan : thickness;
const height = horizontal ? thickness : tileSpan;
const aspect = width === 0 || height === 0 ? 0 : Math.max(width / height, height / width);
if (aspect > worst) {
worst = aspect;
}
}
return worst;
}
function binaryLayout(tiles, rect, gutter = 0) {
if (tiles.length === 0) return [];
if (tiles.length === 1) {
return [{
id: tiles[0].id,
area: tiles[0].area,
rect: {
x: rect.x + gutter / 2,
y: rect.y + gutter / 2,
width: Math.max(0, rect.width - gutter),
height: Math.max(0, rect.height - gutter)
}
}];
}
const totalArea = tiles.reduce((sum, t) => sum + t.area, 0);
const availableArea = rect.width * rect.height;
const scale = availableArea / totalArea;
const normalizedTiles = tiles.map((t) => ({
...t,
normalizedArea: t.area * scale
}));
return splitRecursive(normalizedTiles, rect, gutter);
}
function splitRecursive(tiles, rect, gutter) {
if (tiles.length === 0) return [];
if (tiles.length === 1) {
return [{
id: tiles[0].id,
area: tiles[0].area,
rect: {
x: rect.x + gutter / 2,
y: rect.y + gutter / 2,
width: Math.max(0, rect.width - gutter),
height: Math.max(0, rect.height - gutter)
}
}];
}
const mid = Math.floor(tiles.length / 2);
const left = tiles.slice(0, mid);
const right = tiles.slice(mid);
const leftArea = left.reduce((sum, t) => sum + t.normalizedArea, 0);
const rightArea = right.reduce((sum, t) => sum + t.normalizedArea, 0);
const totalArea = leftArea + rightArea;
const splitHorizontally = rect.width > rect.height;
let leftRect;
let rightRect;
if (splitHorizontally) {
const leftWidth = leftArea / totalArea * rect.width;
leftRect = {
x: rect.x,
y: rect.y,
width: leftWidth,
height: rect.height
};
rightRect = {
x: rect.x + leftWidth,
y: rect.y,
width: rect.width - leftWidth,
height: rect.height
};
} else {
const leftHeight = leftArea / totalArea * rect.height;
leftRect = {
x: rect.x,
y: rect.y,
width: rect.width,
height: leftHeight
};
rightRect = {
x: rect.x,
y: rect.y + leftHeight,
width: rect.width,
height: rect.height - leftHeight
};
}
return [
...splitRecursive(left, leftRect, gutter),
...splitRecursive(right, rightRect, gutter)
];
}
const strategies = {
squarified: squarifiedLayout,
binary: binaryLayout
};
function applyLayoutStrategy(strategy, tiles, rect, gutter = 0) {
const layoutFn = strategies[strategy];
if (!layoutFn) {
throw new Error(`Unknown layout strategy: ${strategy}`);
}
return layoutFn(tiles, rect, gutter);
}
function computeBentoLayout(config) {
const { canvas, tiles: tileAreas, options } = config;
const padding = canvas.padding ?? 0;
const strategy = (options == null ? void 0 : options.strategy) ?? "squarified";
const gutter = (options == null ? void 0 : options.gutter) ?? 0;
if (canvas.width <= 0 || canvas.height <= 0) {
throw new Error("Canvas width and height must be greater than 0");
}
const rect = {
x: padding,
y: padding,
width: Math.max(0, canvas.width - padding * 2),
height: Math.max(0, canvas.height - padding * 2)
};
const tiles = tileAreas.map((area, index) => ({
id: `tile-${index}`,
area
})).filter((tile) => tile.area > 0);
const layoutTiles = tiles.length ? applyLayoutStrategy(strategy, tiles, rect, gutter) : [];
return {
rect,
tiles: layoutTiles
};
}
exports.applyLayoutStrategy = applyLayoutStrategy;
exports.computeBentoLayout = computeBentoLayout;
//# sourceMappingURL=index.cjs.map