export const VIEWPORT_DPR_SCHEMA_VERSION = 1 as const; export const VIEWPORT_MAX_DPR = 2 as const; export interface ViewportPixelMetrics { schemaVersion: typeof VIEWPORT_DPR_SCHEMA_VERSION; cssWidth: number; cssHeight: number; pixelRatio: number; backingWidth: number; backingHeight: number; } export interface ViewportNDC { x: number; y: number; } function finitePositive(value: number): boolean { return Number.isFinite(value) && value > 0; } export function resolveViewportPixelRatio(devicePixelRatio: number | undefined, maximum = VIEWPORT_MAX_DPR): number { if (!finitePositive(maximum)) throw new Error("VIEWPORT_DPR_INVALID"); const observed = finitePositive(devicePixelRatio ?? 1) ? devicePixelRatio! : 1; return Math.min(observed, maximum); } export function resolveViewportPixelMetrics(cssWidth: number, cssHeight: number, devicePixelRatio: number | undefined, maximum = VIEWPORT_MAX_DPR): ViewportPixelMetrics { if (!finitePositive(cssWidth) || !finitePositive(cssHeight)) throw new Error("VIEWPORT_SIZE_INVALID"); const pixelRatio = resolveViewportPixelRatio(devicePixelRatio, maximum); return { schemaVersion: VIEWPORT_DPR_SCHEMA_VERSION, cssWidth, cssHeight, pixelRatio, backingWidth: Math.max(1, Math.floor(cssWidth * pixelRatio)), backingHeight: Math.max(1, Math.floor(cssHeight * pixelRatio)), }; } export function viewportNDC(clientX: number, clientY: number, bounds: { left: number; top: number; width: number; height: number }): ViewportNDC { if (![clientX, clientY, bounds.left, bounds.top, bounds.width, bounds.height].every(Number.isFinite) || bounds.width <= 0 || bounds.height <= 0) { throw new Error("VIEWPORT_BOUNDS_INVALID"); } return { x: ((clientX - bounds.left) / bounds.width) * 2 - 1, y: -((clientY - bounds.top) / bounds.height) * 2 + 1, }; }