bentogreed
Version:
A lightweight, framework-agnostic library for generating bento grid layouts
1 lines • 15.9 kB
Source Map (JSON)
{"version":3,"file":"index.mjs","sources":["../src/algorithms/squarified.ts","../src/algorithms/binary.ts","../src/algorithms/index.ts","../src/layout.ts"],"sourcesContent":["import { Rectangle, Tile, LayoutTile } from '../types';\n\n/**\n * Squarified treemap algorithm implementation\n * Based on the algorithm by Bruls, Huizing, and van Wijk\n */\nexport function squarifiedLayout(\n tiles: Tile[],\n rect: Rectangle,\n gutter: number = 0\n): LayoutTile[] {\n if (tiles.length === 0) return [];\n if (tiles.length === 1) {\n const tile = tiles[0];\n return [\n {\n id: tile.id,\n area: tile.area,\n rect: {\n x: rect.x + gutter / 2,\n y: rect.y + gutter / 2,\n width: Math.max(0, rect.width - gutter),\n height: Math.max(0, rect.height - gutter),\n },\n },\n ];\n }\n\n const totalArea = tiles.reduce((sum, t) => sum + t.area, 0);\n const availableArea = rect.width * rect.height;\n const scale = totalArea === 0 ? 0 : availableArea / totalArea;\n\n const normalizedTiles = tiles.map((tile) => ({\n ...tile,\n normalizedArea: tile.area * scale,\n }));\n\n return squarify(normalizedTiles, rect, gutter);\n}\n\nfunction squarify(\n tiles: Array<Tile & { normalizedArea: number }>,\n rect: Rectangle,\n gutter: number\n): LayoutTile[] {\n const result: LayoutTile[] = [];\n let startIndex = 0;\n let remainingRect: Rectangle = { ...rect };\n\n while (\n startIndex < tiles.length &&\n remainingRect.width > 0 &&\n remainingRect.height > 0\n ) {\n const horizontal = remainingRect.width >= remainingRect.height;\n const row = getNextRow(tiles, startIndex, remainingRect, horizontal);\n\n const rowArea = row.reduce((sum, tile) => sum + tile.normalizedArea, 0);\n const thickness = horizontal\n ? rowArea / remainingRect.width\n : rowArea / remainingRect.height;\n const primarySpan = horizontal ? remainingRect.width : remainingRect.height;\n\n let cursor = horizontal ? remainingRect.x : remainingRect.y;\n\n for (let index = 0; index < row.length; index++) {\n const tile = row[index];\n const tileSpan = primarySpan === 0 ? 0 : (tile.normalizedArea / rowArea) * primarySpan;\n\n const tileWidth = horizontal ? tileSpan : thickness;\n const tileHeight = horizontal ? thickness : tileSpan;\n\n const gutterPrimaryStart = index === 0 ? gutter / 2 : gutter;\n const gutterPrimaryEnd = index === row.length - 1 ? gutter / 2 : gutter;\n const gutterSecondaryStart = gutter / 2;\n const gutterSecondaryEnd = gutter / 2;\n\n const x = horizontal\n ? cursor + gutterPrimaryStart\n : remainingRect.x + gutterSecondaryStart;\n const y = horizontal\n ? remainingRect.y + gutterSecondaryStart\n : cursor + gutterPrimaryStart;\n\n const width = horizontal\n ? Math.max(0, tileWidth - gutterPrimaryStart - gutterPrimaryEnd)\n : Math.max(0, tileWidth - gutterSecondaryStart - gutterSecondaryEnd);\n const height = horizontal\n ? Math.max(0, tileHeight - gutterSecondaryStart - gutterSecondaryEnd)\n : Math.max(0, tileHeight - gutterPrimaryStart - gutterPrimaryEnd);\n\n result.push({\n id: tile.id,\n area: tile.area,\n rect: { x, y, width, height },\n });\n\n cursor += tileSpan;\n }\n\n if (horizontal) {\n remainingRect = {\n x: remainingRect.x,\n y: remainingRect.y + thickness,\n width: remainingRect.width,\n height: Math.max(0, remainingRect.height - thickness),\n };\n } else {\n remainingRect = {\n x: remainingRect.x + thickness,\n y: remainingRect.y,\n width: Math.max(0, remainingRect.width - thickness),\n height: remainingRect.height,\n };\n }\n\n startIndex += row.length;\n }\n\n return result;\n}\n\nfunction getNextRow(\n tiles: Array<Tile & { normalizedArea: number }>,\n startIndex: number,\n rect: Rectangle,\n horizontal: boolean\n): Array<Tile & { normalizedArea: number }> {\n if (startIndex >= tiles.length) return [];\n\n const row: Array<Tile & { normalizedArea: number }> = [tiles[startIndex]];\n let worstAspect = getWorstAspect(row, rect, horizontal);\n\n for (let i = startIndex + 1; i < tiles.length; i++) {\n row.push(tiles[i]);\n const nextWorst = getWorstAspect(row, rect, horizontal);\n if (nextWorst > worstAspect) {\n row.pop();\n break;\n }\n worstAspect = nextWorst;\n }\n\n return row;\n}\n\nfunction getWorstAspect(\n row: Array<Tile & { normalizedArea: number }>,\n rect: Rectangle,\n horizontal: boolean\n): number {\n const rowArea = row.reduce((sum, tile) => sum + tile.normalizedArea, 0);\n if (rowArea === 0) return 0;\n\n const primarySpan = horizontal ? rect.width : rect.height;\n const thickness = horizontal\n ? rowArea / Math.max(primarySpan, Number.EPSILON)\n : rowArea / Math.max(primarySpan, Number.EPSILON);\n\n let worst = 0;\n for (const tile of row) {\n const tileSpan = primarySpan === 0 ? 0 : (tile.normalizedArea / rowArea) * primarySpan;\n const width = horizontal ? tileSpan : thickness;\n const height = horizontal ? thickness : tileSpan;\n\n const aspect = width === 0 || height === 0\n ? 0\n : Math.max(width / height, height / width);\n\n if (aspect > worst) {\n worst = aspect;\n }\n }\n\n return worst;\n}\n","import { Rectangle, Tile, LayoutTile } from '../types';\n\n/**\n * Binary split algorithm - recursively splits along the longer dimension\n */\nexport function binaryLayout(\n tiles: Tile[],\n rect: Rectangle,\n gutter: number = 0\n): LayoutTile[] {\n if (tiles.length === 0) return [];\n if (tiles.length === 1) {\n return [{\n id: tiles[0].id,\n area: tiles[0].area,\n rect: {\n x: rect.x + gutter / 2,\n y: rect.y + gutter / 2,\n width: Math.max(0, rect.width - gutter),\n height: Math.max(0, rect.height - gutter)\n }\n }];\n }\n\n // Normalize areas\n const totalArea = tiles.reduce((sum, t) => sum + t.area, 0);\n const availableArea = rect.width * rect.height;\n const scale = availableArea / totalArea;\n\n const normalizedTiles = tiles.map(t => ({\n ...t,\n normalizedArea: t.area * scale\n }));\n\n return splitRecursive(normalizedTiles, rect, gutter);\n}\n\nfunction splitRecursive(\n tiles: Array<Tile & { normalizedArea: number }>,\n rect: Rectangle,\n gutter: number\n): LayoutTile[] {\n if (tiles.length === 0) return [];\n if (tiles.length === 1) {\n return [{\n id: tiles[0].id,\n area: tiles[0].area,\n rect: {\n x: rect.x + gutter / 2,\n y: rect.y + gutter / 2,\n width: Math.max(0, rect.width - gutter),\n height: Math.max(0, rect.height - gutter)\n }\n }];\n }\n\n // Split tiles into two groups\n const mid = Math.floor(tiles.length / 2);\n const left = tiles.slice(0, mid);\n const right = tiles.slice(mid);\n\n const leftArea = left.reduce((sum, t) => sum + t.normalizedArea, 0);\n const rightArea = right.reduce((sum, t) => sum + t.normalizedArea, 0);\n const totalArea = leftArea + rightArea;\n\n // Determine split direction based on aspect ratio\n const splitHorizontally = rect.width > rect.height;\n\n let leftRect: Rectangle;\n let rightRect: Rectangle;\n\n if (splitHorizontally) {\n const leftWidth = (leftArea / totalArea) * rect.width;\n leftRect = {\n x: rect.x,\n y: rect.y,\n width: leftWidth,\n height: rect.height\n };\n rightRect = {\n x: rect.x + leftWidth,\n y: rect.y,\n width: rect.width - leftWidth,\n height: rect.height\n };\n } else {\n const leftHeight = (leftArea / totalArea) * rect.height;\n leftRect = {\n x: rect.x,\n y: rect.y,\n width: rect.width,\n height: leftHeight\n };\n rightRect = {\n x: rect.x,\n y: rect.y + leftHeight,\n width: rect.width,\n height: rect.height - leftHeight\n };\n }\n\n return [\n ...splitRecursive(left, leftRect, gutter),\n ...splitRecursive(right, rightRect, gutter)\n ];\n}\n\n","import { Rectangle, Tile, LayoutTile, LayoutStrategy } from '../types';\nimport { squarifiedLayout } from './squarified';\nimport { binaryLayout } from './binary';\n\n/**\n * Strategy registry - makes it easy to add new algorithms\n */\nconst strategies: Record<LayoutStrategy, (tiles: Tile[], rect: Rectangle, gutter: number) => LayoutTile[]> = {\n squarified: squarifiedLayout,\n binary: binaryLayout,\n};\n\n/**\n * Apply a layout strategy to tiles within a rectangle\n */\nexport function applyLayoutStrategy(\n strategy: LayoutStrategy,\n tiles: Tile[],\n rect: Rectangle,\n gutter: number = 0\n): LayoutTile[] {\n const layoutFn = strategies[strategy];\n if (!layoutFn) {\n throw new Error(`Unknown layout strategy: ${strategy}`);\n }\n return layoutFn(tiles, rect, gutter);\n}\n\n","import { BentoGridConfig, BentoGridLayout, LayoutStrategy, Tile, Rectangle } from './types';\nimport { applyLayoutStrategy } from './algorithms';\n\nexport function computeBentoLayout(config: BentoGridConfig): BentoGridLayout {\n const { canvas, tiles: tileAreas, options } = config;\n const padding = canvas.padding ?? 0;\n const strategy: LayoutStrategy = options?.strategy ?? 'squarified';\n const gutter = options?.gutter ?? 0;\n\n if (canvas.width <= 0 || canvas.height <= 0) {\n throw new Error('Canvas width and height must be greater than 0');\n }\n\n const rect: Rectangle = {\n x: padding,\n y: padding,\n width: Math.max(0, canvas.width - padding * 2),\n height: Math.max(0, canvas.height - padding * 2),\n };\n\n const tiles: Tile[] = tileAreas\n .map((area, index) => ({\n id: `tile-${index}`,\n area,\n }))\n .filter((tile) => tile.area > 0);\n\n const layoutTiles = tiles.length\n ? applyLayoutStrategy(strategy, tiles, rect, gutter)\n : [];\n\n return {\n rect,\n tiles: layoutTiles,\n };\n}\n\n"],"names":[],"mappings":"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"}