Files
feuerwerk/src/editor/texture/imageGrid.ts
T

80 lines
2.9 KiB
TypeScript

/**
* 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 };
}