80 lines
2.9 KiB
TypeScript
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 };
|
|
}
|