/** * Pixel perfect cameras: what they show is drawn on a grid of one step per device pixel a world unit * gets – a game view at 100% on whole units, at 500% on fifths. The model keeps its fractions; only * where things are drawn snaps. Pure, so the arithmetic is tested without a GPU. */ import type { Mat } from '../core/model/matrix'; export function gridSteps(devicePixelsPerUnit: number): number { return Math.max(1, Math.round(devicePixelsPerUnit)); } export function onGrid(v: number, steps: number): number { return Math.round(v * steps) / steps; } /** * Where a box of `w`×`h` of its own units, placed by `m` around `pivot` (parts of the box), is drawn * on a grid of `steps` per unit; null draws it as it is. An unturned box snaps a corner, so an odd * size lands on whole pixels too, a turned one its pivot. */ export function snapPlacement( m: Mat, w: number, h: number, steps: number | null, pivot = { x: 0.5, y: 0.5 }, ): Mat { if (!steps) return m; if (Math.abs(m.b) > 1e-9 || Math.abs(m.c) > 1e-9) return { ...m, tx: onGrid(m.tx, steps), ty: onGrid(m.ty, steps) }; const left = pivot.x * m.a * w; const top = pivot.y * m.d * h; return { ...m, tx: onGrid(m.tx - left, steps) + left, ty: onGrid(m.ty - top, steps) + top }; } /** * The point a view looks from instead of `x`, so the world's origin lands on a whole pixel: `centre` * is where the view puts what it looks at, `scale` pixels per unit and `ratio` pixels per CSS pixel * (1 for a picture that counts its own). */ export function lookFrom(x: number, centre: number, scale: number, ratio = 1): number { const origin = Math.round((centre - x * scale) * ratio) / ratio; return (centre - origin) / scale; }