Something went wrong. Try again.
headless React image cropper
Something went wrong. Try again.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131import { 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 );};