UNPKG

bentogreed

Version:

A lightweight, framework-agnostic library for generating bento grid layouts

133 lines (132 loc) 5.21 kB
/** * Squarified treemap algorithm implementation * Based on the algorithm by Bruls, Huizing, and van Wijk */ export 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; }