{"version":3,"file":"crop-geometry.cjs","names":[],"sources":["../../../src/components/ImageCropper/crop-geometry.ts"],"sourcesContent":["/** A width/height pair in pixels. */\nexport interface Size {\n    width: number;\n    height: number;\n}\n\n/** A pan offset in frame pixels, measured from the centered position. */\nexport interface Offset {\n    x: number;\n    y: number;\n}\n\n/** The source rectangle to read out of the natural-size image. */\nexport interface CropRect {\n    sx: number;\n    sy: number;\n    sWidth: number;\n    sHeight: number;\n}\n\n/**\n * Scale that makes `image` exactly cover `frame` with no empty space.\n *\n * `max` rather than `min`: a crop frame must never show background, so the\n * constraining axis is the one that would leave a gap.\n *\n * @param image - Natural image size.\n * @param frame - Crop frame size.\n * @returns The cover scale, or `0` when either dimension is unusable.\n */\nexport function coverScale(image: Size, frame: Size): number {\n    if (image.width <= 0 || image.height <= 0 || frame.width <= 0 || frame.height <= 0) return 0;\n    return Math.max(frame.width / image.width, frame.height / image.height);\n}\n\n/**\n * The largest pan offset that keeps the image covering the frame.\n *\n * Zero on an axis means the image is exactly as wide (or tall) as the frame\n * there, so panning along it would expose background.\n *\n * @param displayed - On-screen image size, after scale and zoom.\n * @param frame - Crop frame size.\n * @returns Maximum absolute offset per axis.\n */\nexport function maxOffset(displayed: Size, frame: Size): Offset {\n    return {\n        x: Math.max(0, (displayed.width - frame.width) / 2),\n        y: Math.max(0, (displayed.height - frame.height) / 2),\n    };\n}\n\n/**\n * Clamp a pan offset so the frame stays fully covered.\n *\n * This is what stops the single most common defect in a cropper: dragging or\n * zooming until the frame shows empty space, which then bakes transparent or\n * black bands into the exported image.\n *\n * @param offset - Desired offset.\n * @param displayed - On-screen image size, after scale and zoom.\n * @param frame - Crop frame size.\n * @returns The offset, clamped per axis.\n */\nexport function clampOffset(offset: Offset, displayed: Size, frame: Size): Offset {\n    const max = maxOffset(displayed, frame);\n    return {\n        x: normalizeZero(Math.min(max.x, Math.max(-max.x, offset.x))),\n        y: normalizeZero(Math.min(max.y, Math.max(-max.y, offset.y))),\n    };\n}\n\n/**\n * Turn `-0` into `0`.\n *\n * Clamping a negative offset against a zero maximum yields `-0`, which compares\n * equal to `0` under `===` but not under `Object.is`. Left alone it leaks into\n * state comparisons and into the `translate()` string, so it is normalized once\n * here rather than guarded at every call site.\n */\nfunction normalizeZero(value: number): number {\n    return value === 0 ? 0 : value;\n}\n\n/**\n * Map the crop frame back onto the natural-size image.\n *\n * The frame is what the user sees; the export has to read the corresponding\n * region of the *original* pixels, so every on-screen quantity is divided back\n * out by the effective scale. Working in natural pixels — instead of exporting\n * whatever the preview happens to be sized at — is what keeps a 4000 px photo\n * from being downsampled to the width of a 320 px preview.\n *\n * @param params.image - Natural image size.\n * @param params.frame - Crop frame size.\n * @param params.zoom - Zoom multiplier over the cover scale (`1` = cover).\n * @param params.offset - Pan offset in frame pixels.\n * @returns The source rectangle, clamped to the image bounds.\n */\nexport function computeCropRect({\n    image,\n    frame,\n    zoom,\n    offset,\n}: {\n    image: Size;\n    frame: Size;\n    zoom: number;\n    offset: Offset;\n}): CropRect {\n    const scale = coverScale(image, frame) * zoom;\n    if (scale <= 0) return { sx: 0, sy: 0, sWidth: 0, sHeight: 0 };\n\n    const sWidth = frame.width / scale;\n    const sHeight = frame.height / scale;\n\n    // A positive offset moves the image right/down on screen, which means the\n    // visible region moves left/up within the source — hence the subtraction.\n    const centerX = image.width / 2 - offset.x / scale;\n    const centerY = image.height / 2 - offset.y / scale;\n\n    const sx = Math.max(0, Math.min(image.width - sWidth, centerX - sWidth / 2));\n    const sy = Math.max(0, Math.min(image.height - sHeight, centerY - sHeight / 2));\n\n    return { sx, sy, sWidth, sHeight };\n}\n\n/**\n * Output size for a crop, honoring an optional cap on the long edge.\n *\n * Exporting at the frame's own size would tie file size to whatever the preview\n * happened to measure. Exporting at full source resolution is right by default,\n * but a 12 MP phone photo cropped for a 96 px avatar is megabytes of waste — so\n * `maxSize` caps the long edge while preserving the aspect ratio.\n *\n * @param crop - The source rectangle being exported.\n * @param maxSize - Cap on the longest output edge, if any.\n * @returns Integer output dimensions, at least 1 px per axis.\n */\nexport function outputSize(crop: CropRect, maxSize?: number): Size {\n    const width = crop.sWidth;\n    const height = crop.sHeight;\n    if (!maxSize || maxSize <= 0 || (width <= maxSize && height <= maxSize)) {\n        return { width: Math.max(1, Math.round(width)), height: Math.max(1, Math.round(height)) };\n    }\n    const ratio = maxSize / Math.max(width, height);\n    return {\n        width: Math.max(1, Math.round(width * ratio)),\n        height: Math.max(1, Math.round(height * ratio)),\n    };\n}\n"],"mappings":"AA8BA,SAAgB,EAAW,EAAa,EAAqB,CAEzD,OADI,EAAM,OAAS,GAAK,EAAM,QAAU,GAAK,EAAM,OAAS,GAAK,EAAM,QAAU,EAAU,EACpF,KAAK,IAAI,EAAM,MAAQ,EAAM,MAAO,EAAM,OAAS,EAAM,MAAM,CAC1E,CAYA,SAAgB,EAAU,EAAiB,EAAqB,CAC5D,MAAO,CACH,EAAG,KAAK,IAAI,GAAI,EAAU,MAAQ,EAAM,OAAS,CAAC,EAClD,EAAG,KAAK,IAAI,GAAI,EAAU,OAAS,EAAM,QAAU,CAAC,CACxD,CACJ,CAcA,SAAgB,EAAY,EAAgB,EAAiB,EAAqB,CAC9E,IAAM,EAAM,EAAU,EAAW,CAAK,EACtC,MAAO,CACH,EAAG,EAAc,KAAK,IAAI,EAAI,EAAG,KAAK,IAAI,CAAC,EAAI,EAAG,EAAO,CAAC,CAAC,CAAC,EAC5D,EAAG,EAAc,KAAK,IAAI,EAAI,EAAG,KAAK,IAAI,CAAC,EAAI,EAAG,EAAO,CAAC,CAAC,CAAC,CAChE,CACJ,CAUA,SAAS,EAAc,EAAuB,CAC1C,OAAO,IAAU,EAAI,EAAI,CAC7B,CAiBA,SAAgB,EAAgB,CAC5B,QACA,QACA,OACA,UAMS,CACT,IAAM,EAAQ,EAAW,EAAO,CAAK,EAAI,EACzC,GAAI,GAAS,EAAG,MAAO,CAAE,GAAI,EAAG,GAAI,EAAG,OAAQ,EAAG,QAAS,CAAE,EAE7D,IAAM,EAAS,EAAM,MAAQ,EACvB,EAAU,EAAM,OAAS,EAIzB,EAAU,EAAM,MAAQ,EAAI,EAAO,EAAI,EACvC,EAAU,EAAM,OAAS,EAAI,EAAO,EAAI,EAK9C,MAAO,CAAE,GAHE,KAAK,IAAI,EAAG,KAAK,IAAI,EAAM,MAAQ,EAAQ,EAAU,EAAS,CAAC,CAGjE,EAAI,GAFF,KAAK,IAAI,EAAG,KAAK,IAAI,EAAM,OAAS,EAAS,EAAU,EAAU,CAAC,CAEhE,EAAI,SAAQ,SAAQ,CACrC,CAcA,SAAgB,EAAW,EAAgB,EAAwB,CAC/D,IAAM,EAAQ,EAAK,OACb,EAAS,EAAK,QACpB,GAAI,CAAC,GAAW,GAAW,GAAM,GAAS,GAAW,GAAU,EAC3D,MAAO,CAAE,MAAO,KAAK,IAAI,EAAG,KAAK,MAAM,CAAK,CAAC,EAAG,OAAQ,KAAK,IAAI,EAAG,KAAK,MAAM,CAAM,CAAC,CAAE,EAE5F,IAAM,EAAQ,EAAU,KAAK,IAAI,EAAO,CAAM,EAC9C,MAAO,CACH,MAAO,KAAK,IAAI,EAAG,KAAK,MAAM,EAAQ,CAAK,CAAC,EAC5C,OAAQ,KAAK,IAAI,EAAG,KAAK,MAAM,EAAS,CAAK,CAAC,CAClD,CACJ"}