/** * Geometry of the image picker's tile grid: how many tiles fit a width, where each one sits, and * which one a point hits. Pure, so the arithmetic is testable without a canvas. */ export interface Rect { x: number; y: number; w: number; h: number; } /** Narrowest a tile may get before a column is dropped, and widest it may grow. */ export const TILE_MIN = 92; export const TILE_MAX = 132; export const TILE_GAP = 8; /** Strip under the picture holding the caption. */ export const CAPTION_HEIGHT = 16; export interface GridMetrics { columns: number; /** Side of a tile's picture area; a tile is this plus the caption strip. */ tile: number; rows: number; /** Total height of the grid, which is what the scroll container has to give it. */ height: number; } /** How small and how large a tile may get; the tile palette of a tilemap wants smaller ones (#139). */ export interface TileSizes { min: number; max: number; } export function gridMetrics( width: number, count: number, gap = TILE_GAP, sizes: TileSizes = { min: TILE_MIN, max: TILE_MAX }, ): GridMetrics { const columns = Math.max(1, Math.floor((width + gap) / (sizes.min + gap))); const spread = (width - gap * (columns - 1)) / columns; const tile = Math.max(sizes.min, Math.min(sizes.max, Math.floor(spread))); const rows = Math.ceil(count / columns); const height = rows * (tile + CAPTION_HEIGHT) + Math.max(0, rows - 1) * gap; return { columns, tile, rows, height }; } export function tileRect(index: number, metrics: GridMetrics, gap = TILE_GAP): Rect { const column = index % metrics.columns; const row = Math.floor(index / metrics.columns); const h = metrics.tile + CAPTION_HEIGHT; return { x: column * (metrics.tile + gap), y: row * (h + gap), w: metrics.tile, h }; } /** Index of the tile under a point, or -1 for the gaps between them and everything past the last. */ export function tileAt(x: number, y: number, count: number, metrics: GridMetrics, gap = TILE_GAP): number { if (x < 0 || y < 0) return -1; const h = metrics.tile + CAPTION_HEIGHT; const column = Math.floor(x / (metrics.tile + gap)); const row = Math.floor(y / (h + gap)); if (column >= metrics.columns) return -1; if (x - column * (metrics.tile + gap) > metrics.tile) return -1; if (y - row * (h + gap) > h) return -1; const index = row * metrics.columns + column; return index < count ? index : -1; } /** * The picture centred in the box, scaled to fit either way. Small pictures are blown up: an icon * shown at its own 16 px would be unrecognisable next to a full-size sheet. */ export function containRect(imageW: number, imageH: number, boxW: number, boxH: number): Rect { if (imageW <= 0 || imageH <= 0) return { x: 0, y: 0, w: 0, h: 0 }; const scale = Math.min(boxW / imageW, boxH / imageH); const w = Math.max(1, Math.round(imageW * scale)); const h = Math.max(1, Math.round(imageH * scale)); return { x: Math.round((boxW - w) / 2), y: Math.round((boxH - h) / 2), w, h }; }