import { type CropValue, type Point, ROTATIONS, type Rotation, type Size } from './types.ts'; /** * constrain a value to a closed range. * * @param value value to constrain. * @param min lower bound. * @param max upper bound. * @returns the clamped value. */ export const clamp = (value: number, min: number, max: number): number => { if (value < min) { return min; } if (value > max) { return max; } return value; }; /** * convert zoom to a logarithmic slider fraction. * * @param zoom zoom relative to the cover fit. * @param maxZoom maximum zoom. * @returns slider fraction from `0` to `1`. */ export const zoomToFraction = (zoom: number, maxZoom: number): number => { const fraction = Math.log2(zoom) / Math.log2(maxZoom); return Number.isFinite(fraction) ? clamp(fraction, 0, 1) : 0; }; /** * convert a logarithmic slider fraction to zoom. * * @param fraction slider position from `0` to `1`. * @param maxZoom maximum zoom. * @returns zoom relative to the cover fit. */ export const fractionToZoom = (fraction: number, maxZoom: number): number => { const zoom = 2 ** (clamp(fraction, 0, 1) * Math.log2(maxZoom)); return Number.isFinite(zoom) && zoom >= 1 ? zoom : 1; }; /** * get the source size after rotation. * * @param size the source's natural size. * @param rotation quarter-turns clockwise. * @returns rotated source size. */ export const rotatedSize = (size: Size, rotation: Rotation): Size => { return rotation % 2 === 0 ? size : { width: size.height, height: size.width }; }; /** * normalize a quarter-turn count. * * @param turns quarter-turns clockwise; may be negative. * @returns equivalent rotation from `0` to `3`. */ export const normalizeRotation = (turns: number): Rotation => { return ROTATIONS[((Math.trunc(turns) % 4) + 4) % 4]; }; /** * fit an aspect ratio inside a size. * * @param outer the box to fit within. * @param aspectRatio target width / height. * @returns contained size, or zero size for invalid input. */ export const containSize = (outer: Size, aspectRatio: number): Size => { if (!(outer.width > 0) || !(outer.height > 0) || !(aspectRatio > 0) || !Number.isFinite(aspectRatio)) { return { width: 0, height: 0 }; } if (outer.width / outer.height > aspectRatio) { return { width: outer.height * aspectRatio, height: outer.height }; } return { width: outer.width, height: outer.width / aspectRatio }; }; const turnClockwise = (point: Point, size: Size): Point => { return { x: size.height - point.y, y: point.x }; }; /** * transform a point between rotated source spaces. * * @param options point and source size in the `from` space, with the target rotation. * @returns the equivalent point in the space rotated by `to`. */ export const rotatePoint = (options: { point: Point; size: Size; from: Rotation; to: Rotation }): Point => { const { point, size, from, to } = options; const turns = (((to - from) % 4) + 4) % 4; let p = point; let s = size; for (let i = 0; i < turns; i++) { p = turnClockwise(p, s); s = { width: s.height, height: s.width }; } return p; }; /** * compare optional sizes. * * @param a first size, or null. * @param b second size, or null. * @returns whether the sizes match. */ export const sameSize = (a: Size | null, b: Size | null): boolean => { if (!a || !b) { return a === b; } return a.width === b.width && a.height === b.height; }; /** * compare crop values. * * @param a first crop. * @param b second crop. * @returns whether every field matches. */ export const sameCropValue = (a: CropValue, b: CropValue): boolean => { return ( a.rotation === b.rotation && a.x === b.x && a.y === b.y && a.width === b.width && a.height === b.height ); };