UNPKG

bentogreed

Version:

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

229 lines (228 loc) 7.94 kB
"use strict"; 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