@lmb-it/kitsconcerto-native 0.1.285 → 0.1.286
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/apps/mobile/src/ui/accordion/index.js +2 -2
- package/components/ImageCropper/compression.cjs +63 -0
- package/components/ImageCropper/compression.cjs.map +1 -0
- package/components/ImageCropper/compression.js +58 -0
- package/components/ImageCropper/compression.js.map +1 -0
- package/components/ImageCropper/constants.cjs +35 -0
- package/components/ImageCropper/constants.cjs.map +1 -0
- package/components/ImageCropper/constants.js +23 -0
- package/components/ImageCropper/constants.js.map +1 -0
- package/components/ImageCropper/cropperState.cjs +119 -0
- package/components/ImageCropper/cropperState.cjs.map +1 -0
- package/components/ImageCropper/cropperState.js +114 -0
- package/components/ImageCropper/cropperState.js.map +1 -0
- package/components/ImageCropper/encode.cjs +45 -0
- package/components/ImageCropper/encode.cjs.map +1 -0
- package/components/ImageCropper/encode.js +42 -0
- package/components/ImageCropper/encode.js.map +1 -0
- package/components/ImageCropper/encoderRegistry.cjs +13 -0
- package/components/ImageCropper/encoderRegistry.cjs.map +1 -0
- package/components/ImageCropper/encoderRegistry.js +9 -0
- package/components/ImageCropper/encoderRegistry.js.map +1 -0
- package/components/ImageCropper/encoders/rnImageEncoder.cjs +54 -0
- package/components/ImageCropper/encoders/rnImageEncoder.cjs.map +1 -0
- package/components/ImageCropper/encoders/rnImageEncoder.js +51 -0
- package/components/ImageCropper/encoders/rnImageEncoder.js.map +1 -0
- package/components/ImageCropper/geometry.cjs +208 -0
- package/components/ImageCropper/geometry.cjs.map +1 -0
- package/components/ImageCropper/geometry.js +186 -0
- package/components/ImageCropper/geometry.js.map +1 -0
- package/components/ImageCropper/index.cjs +436 -0
- package/components/ImageCropper/index.cjs.map +1 -0
- package/components/ImageCropper/index.js +434 -0
- package/components/ImageCropper/index.js.map +1 -0
- package/components/ImageCropper/pinchZoom.cjs +8 -0
- package/components/ImageCropper/pinchZoom.cjs.map +1 -0
- package/components/ImageCropper/pinchZoom.js +5 -0
- package/components/ImageCropper/pinchZoom.js.map +1 -0
- package/components/ImageCropper/useImageSource.cjs +87 -0
- package/components/ImageCropper/useImageSource.cjs.map +1 -0
- package/components/ImageCropper/useImageSource.js +84 -0
- package/components/ImageCropper/useImageSource.js.map +1 -0
- package/hooks/useComponentDefaults.cjs +12 -0
- package/hooks/useComponentDefaults.cjs.map +1 -1
- package/hooks/useComponentDefaults.js +12 -0
- package/hooks/useComponentDefaults.js.map +1 -1
- package/index.cjs +120 -115
- package/index.cjs.map +1 -1
- package/index.d.ts +394 -3
- package/index.js +2 -0
- package/index.js.map +1 -1
- package/package.json +13 -1
- package/packages/types/src/Theme/defaultTheme.cjs +111 -0
- package/packages/types/src/Theme/defaultTheme.cjs.map +1 -1
- package/packages/types/src/Theme/defaultTheme.js +111 -0
- package/packages/types/src/Theme/defaultTheme.js.map +1 -1
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { runEncodePlan } from './compression.js';
|
|
2
|
+
import { getCropEncoder } from './encoderRegistry.js';
|
|
3
|
+
export { hasCropEncoder, setCropEncoder } from './encoderRegistry.js';
|
|
4
|
+
|
|
5
|
+
const encodeCropNative = async (uri, sourceRect, sourceSize, rotation, max, format) => {
|
|
6
|
+
const encoder = getCropEncoder();
|
|
7
|
+
if (!encoder) {
|
|
8
|
+
throw new Error(
|
|
9
|
+
"[ImageCropper] No native encoder is registered, so the crop cannot be saved. Call setCropEncoder(\u2026) once at app start \u2014 see encode.native.ts for the one-function contract. (expo-image-manipulator, the library the plan named, needs the Expo modules runtime, which this bare React Native app does not have.)"
|
|
10
|
+
);
|
|
11
|
+
}
|
|
12
|
+
const quarter = rotation === 90 || rotation === 270;
|
|
13
|
+
const natural = quarter ? { width: sourceRect.height, height: sourceRect.width } : { width: sourceRect.width, height: sourceRect.height };
|
|
14
|
+
const run = await runEncodePlan(natural, max, format, async (attempt) => {
|
|
15
|
+
const out = await encoder({
|
|
16
|
+
uri,
|
|
17
|
+
sourceRect,
|
|
18
|
+
sourceSize,
|
|
19
|
+
rotation,
|
|
20
|
+
size: attempt.size,
|
|
21
|
+
quality: attempt.quality,
|
|
22
|
+
format
|
|
23
|
+
});
|
|
24
|
+
return { bytes: out.bytes, value: { uri: out.uri, width: out.width, height: out.height } };
|
|
25
|
+
});
|
|
26
|
+
return {
|
|
27
|
+
uri: run.value.uri,
|
|
28
|
+
// ⚠️ WHAT THE ENCODER ACTUALLY MADE, falling back to what the search
|
|
29
|
+
// asked for. The bare-React-Native encoder drives its resizer with a
|
|
30
|
+
// square `contain` ceiling — the only orientation-safe way to use it —
|
|
31
|
+
// so the result can be a pixel or two off the request, and a consumer
|
|
32
|
+
// sizing a layout from this number should get the true one.
|
|
33
|
+
width: Math.max(1, Math.round(run.value.width ?? run.size.width)),
|
|
34
|
+
height: Math.max(1, Math.round(run.value.height ?? run.size.height)),
|
|
35
|
+
size: run.bytes,
|
|
36
|
+
format,
|
|
37
|
+
withinBudget: run.withinBudget
|
|
38
|
+
};
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
export { encodeCropNative };
|
|
42
|
+
//# sourceMappingURL=encode.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"encode.js","sources":["../../../../src/components/ImageCropper/encode.native.ts"],"sourcesContent":["import type {CropOutputFormat, ICropRect, ICropResult, ICropSize} from \"@lmb-it/kitsconcerto-types\";\nimport {runEncodePlan} from \"./compression\";\nimport {getCropEncoder} from \"./encoderRegistry\";\n\nexport {setCropEncoder, hasCropEncoder, type ICropEncoder} from \"./encoderRegistry\";\n\n/**\n * The one thing the native cropper cannot do by itself.\n *\n * ⛔ THERE IS NO CANVAS ON A DEVICE. Every other part of this component —\n * geometry, state, gestures, the compression SEARCH — runs identically on both\n * platforms out of the shared modules. Turning the chosen rect into an encoded\n * file is the exception: it needs a native module, and this repository does not\n * currently ship one.\n *\n * ⚠️ WHAT IS ACTUALLY MISSING, MEASURED RATHER THAN ASSUMED. The approved plan\n * named `expo-image-manipulator`. It is not installed, and `apps/mobile` is a\n * BARE React Native app — there is no `expo`, no `expo-modules-core`, and\n * `expo-image-manipulator` cannot run without them. Adding it means running\n * `install-expo-modules`, which edits the iOS and Android projects, the Podfile\n * and the app delegate, and needs a native rebuild. That is a decision about\n * the app's native setup, not a line of component code, so this file does not\n * make it quietly.\n *\n * ── HOW TO FINISH IT, WHICHEVER LIBRARY IS CHOSEN ──\n * Call `setCropEncoder` once at app start with an implementation of\n * {@link ICropEncoder}. One function, four inputs, one output; the search that\n * decides WHICH size and quality to ask for stays in `compression.ts` and is\n * already tested. For expo-image-manipulator that body is a `manipulateAsync`\n * with a `crop` action, a `rotate` action, a `resize` action and a\n * `SaveOptions` — roughly fifteen lines.\n */\n\n\n/**\n * Crops, rotates, resizes and encodes — the whole output path on native.\n *\n * ⚠️ THE SEARCH IS NOT HERE, exactly as on web. Which (size, quality) to try,\n * in what order, and when to stop squeezing and start shrinking all live in\n * `compression.ts`. This drives the registered encoder with that plan, so both\n * platforms reach a byte ceiling by the same route.\n */\nexport const encodeCropNative = async (\n uri: string,\n sourceRect: ICropRect,\n sourceSize: ICropSize,\n rotation: number,\n max: {width?: number; height?: number; bytes: number},\n format: CropOutputFormat,\n): Promise<ICropResult & {withinBudget: boolean}> => {\n const encoder = getCropEncoder();\n if (!encoder) {\n throw new Error(\n \"[ImageCropper] No native encoder is registered, so the crop cannot be saved. \" +\n \"Call setCropEncoder(…) once at app start — see encode.native.ts for the \" +\n \"one-function contract. (expo-image-manipulator, the library the plan named, \" +\n \"needs the Expo modules runtime, which this bare React Native app does not have.)\",\n );\n }\n\n const quarter = rotation === 90 || rotation === 270;\n // The crop at 1:1, in the orientation it will be SAVED in.\n const natural: ICropSize = quarter\n ? {width: sourceRect.height, height: sourceRect.width}\n : {width: sourceRect.width, height: sourceRect.height};\n\n const run = await runEncodePlan(natural, max, format, async (attempt) => {\n const out = await encoder({\n uri,\n sourceRect,\n sourceSize,\n rotation,\n size: attempt.size,\n quality: attempt.quality,\n format,\n });\n return {bytes: out.bytes, value: {uri: out.uri, width: out.width, height: out.height}};\n });\n\n return {\n uri: run.value.uri,\n // ⚠️ WHAT THE ENCODER ACTUALLY MADE, falling back to what the search\n // asked for. The bare-React-Native encoder drives its resizer with a\n // square `contain` ceiling — the only orientation-safe way to use it —\n // so the result can be a pixel or two off the request, and a consumer\n // sizing a layout from this number should get the true one.\n width: Math.max(1, Math.round(run.value.width ?? run.size.width)),\n height: Math.max(1, Math.round(run.value.height ?? run.size.height)),\n size: run.bytes,\n format,\n withinBudget: run.withinBudget,\n };\n};\n"],"names":[],"mappings":";;;;AA0CO,MAAM,mBAAmB,OAC5B,GAAA,EACA,YACA,UAAA,EACA,QAAA,EACA,KACA,MAAA,KACiD;AACjD,EAAA,MAAM,UAAU,cAAA,EAAe;AAC/B,EAAA,IAAI,CAAC,OAAA,EAAS;AACV,IAAA,MAAM,IAAI,KAAA;AAAA,MACN;AAAA,KAIJ;AAAA,EACJ;AAEA,EAAA,MAAM,OAAA,GAAU,QAAA,KAAa,EAAA,IAAM,QAAA,KAAa,GAAA;AAEhD,EAAA,MAAM,UAAqB,OAAA,GACrB,EAAC,KAAA,EAAO,UAAA,CAAW,QAAQ,MAAA,EAAQ,UAAA,CAAW,KAAA,EAAK,GACnD,EAAC,KAAA,EAAO,UAAA,CAAW,KAAA,EAAO,MAAA,EAAQ,WAAW,MAAA,EAAM;AAEzD,EAAA,MAAM,MAAM,MAAM,aAAA,CAAc,SAAS,GAAA,EAAK,MAAA,EAAQ,OAAO,OAAA,KAAY;AACrE,IAAA,MAAM,GAAA,GAAM,MAAM,OAAA,CAAQ;AAAA,MACtB,GAAA;AAAA,MACA,UAAA;AAAA,MACA,UAAA;AAAA,MACA,QAAA;AAAA,MACA,MAAM,OAAA,CAAQ,IAAA;AAAA,MACd,SAAS,OAAA,CAAQ,OAAA;AAAA,MACjB;AAAA,KACH,CAAA;AACD,IAAA,OAAO,EAAC,KAAA,EAAO,GAAA,CAAI,KAAA,EAAO,OAAO,EAAC,GAAA,EAAK,GAAA,CAAI,GAAA,EAAK,OAAO,GAAA,CAAI,KAAA,EAAO,MAAA,EAAQ,GAAA,CAAI,QAAM,EAAC;AAAA,EACzF,CAAC,CAAA;AAED,EAAA,OAAO;AAAA,IACH,GAAA,EAAK,IAAI,KAAA,CAAM,GAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,IAMf,KAAA,EAAO,IAAA,CAAK,GAAA,CAAI,CAAA,EAAG,IAAA,CAAK,KAAA,CAAM,GAAA,CAAI,KAAA,CAAM,KAAA,IAAS,GAAA,CAAI,IAAA,CAAK,KAAK,CAAC,CAAA;AAAA,IAChE,MAAA,EAAQ,IAAA,CAAK,GAAA,CAAI,CAAA,EAAG,IAAA,CAAK,KAAA,CAAM,GAAA,CAAI,KAAA,CAAM,MAAA,IAAU,GAAA,CAAI,IAAA,CAAK,MAAM,CAAC,CAAA;AAAA,IACnE,MAAM,GAAA,CAAI,KAAA;AAAA,IACV,MAAA;AAAA,IACA,cAAc,GAAA,CAAI;AAAA,GACtB;AACJ;;;;"}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
let encoder = null;
|
|
4
|
+
const setCropEncoder = (next) => {
|
|
5
|
+
encoder = next;
|
|
6
|
+
};
|
|
7
|
+
const getCropEncoder = () => encoder;
|
|
8
|
+
const hasCropEncoder = () => encoder !== null;
|
|
9
|
+
|
|
10
|
+
exports.getCropEncoder = getCropEncoder;
|
|
11
|
+
exports.hasCropEncoder = hasCropEncoder;
|
|
12
|
+
exports.setCropEncoder = setCropEncoder;
|
|
13
|
+
//# sourceMappingURL=encoderRegistry.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"encoderRegistry.cjs","sources":["../../../../src/components/ImageCropper/encoderRegistry.ts"],"sourcesContent":["import type {CropOutputFormat, ICropRect, ICropSize} from \"@lmb-it/kitsconcerto-types\";\n\n/**\n * Where a host app plugs in the one thing the NATIVE cropper cannot do itself.\n *\n * ⛔ THIS FILE HAS NO `.native` SUFFIX ON PURPOSE, AND THAT IS NOT COSMETIC.\n * `rollup.web.config.mjs` treats any path containing `.native.` as EXTERNAL —\n * so a registry living in `encode.native.ts` and re-exported from the shared\n * `components` barrel would compile, and then emit a bare import of a native\n * file into the WEB bundle for a consumer's bundler to fail on. Found by\n * reading that config rather than by shipping it. The registry itself imports\n * nothing platform-specific, so a neutral name is also the honest one.\n *\n * ⚠️ THE WEB CROPPER NEVER CONSULTS THIS. It has a canvas and needs no\n * injection. A cross-platform app can therefore call `setCropEncoder` once at\n * start-up unconditionally: on web it is a harmless no-op, on a device it is\n * what makes Save work.\n */\nexport interface ICropEncoder {\n /**\n * Produce one encoded file.\n *\n * @param uri the source image\n * @param sourceRect the region to keep, in SOURCE pixels, rotation undone\n * @param sourceSize the source's full pixel size\n * @param rotation degrees clockwise to apply when saving\n * @param size the output's pixel size, already decided by the search\n * @param quality 0–1; ignored by lossless formats\n * @param format the encoder to use\n * @returns the written file's uri, its size in bytes, and — when the\n * implementation knows them — the dimensions it actually produced\n *\n * ⚠️ `width`/`height` ARE REPORTED BECAUSE THE REQUEST IS NOT ALWAYS THE\n * RESULT. The bare-React-Native encoder asks its resizer for a square\n * ceiling with `contain` (the only orientation-safe way to drive it), so\n * the output can land a pixel or two off the requested size. Reporting the\n * number back beats the caller assuming the one it asked for.\n */\n (args: {\n uri: string;\n sourceRect: ICropRect;\n /**\n * The source's full size.\n *\n * ⚠️ THE BUNDLED ENCODER NO LONGER NEEDS THIS, and the field stays\n * anyway. It was added when that encoder rotated the file BEFORE\n * cropping it and therefore had to re-express the rect into the rotated\n * frame. Reordering the chain to crop first removed that mapping — but\n * the contract is public, any implementation that rotates first still\n * needs the dimensions, and they are free to carry. A canvas encoder\n * ignores the field entirely.\n */\n sourceSize: ICropSize;\n rotation: number;\n size: ICropSize;\n quality: number;\n format: CropOutputFormat;\n }): Promise<{uri: string; bytes: number; width?: number; height?: number}>;\n}\n\nlet encoder: ICropEncoder | null = null;\n\n/**\n * Register the native encoder. Call once, at app start, before the cropper is\n * used. Passing `null` unregisters it, which is what a test wants.\n */\nexport const setCropEncoder = (next: ICropEncoder | null): void => {\n encoder = next;\n};\n\n/** The registered encoder, or null. */\nexport const getCropEncoder = (): ICropEncoder | null => encoder;\n\n/** Whether a host app has wired one up. The native UI reads this to disable Save. */\nexport const hasCropEncoder = (): boolean => encoder !== null;\n"],"names":[],"mappings":";;AA4DA,IAAI,OAAA,GAA+B,IAAA;AAM5B,MAAM,cAAA,GAAiB,CAAC,IAAA,KAAoC;AAC/D,EAAA,OAAA,GAAU,IAAA;AACd;AAGO,MAAM,iBAAiB,MAA2B;AAGlD,MAAM,cAAA,GAAiB,MAAe,OAAA,KAAY;;;;;;"}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
let encoder = null;
|
|
2
|
+
const setCropEncoder = (next) => {
|
|
3
|
+
encoder = next;
|
|
4
|
+
};
|
|
5
|
+
const getCropEncoder = () => encoder;
|
|
6
|
+
const hasCropEncoder = () => encoder !== null;
|
|
7
|
+
|
|
8
|
+
export { getCropEncoder, hasCropEncoder, setCropEncoder };
|
|
9
|
+
//# sourceMappingURL=encoderRegistry.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"encoderRegistry.js","sources":["../../../../src/components/ImageCropper/encoderRegistry.ts"],"sourcesContent":["import type {CropOutputFormat, ICropRect, ICropSize} from \"@lmb-it/kitsconcerto-types\";\n\n/**\n * Where a host app plugs in the one thing the NATIVE cropper cannot do itself.\n *\n * ⛔ THIS FILE HAS NO `.native` SUFFIX ON PURPOSE, AND THAT IS NOT COSMETIC.\n * `rollup.web.config.mjs` treats any path containing `.native.` as EXTERNAL —\n * so a registry living in `encode.native.ts` and re-exported from the shared\n * `components` barrel would compile, and then emit a bare import of a native\n * file into the WEB bundle for a consumer's bundler to fail on. Found by\n * reading that config rather than by shipping it. The registry itself imports\n * nothing platform-specific, so a neutral name is also the honest one.\n *\n * ⚠️ THE WEB CROPPER NEVER CONSULTS THIS. It has a canvas and needs no\n * injection. A cross-platform app can therefore call `setCropEncoder` once at\n * start-up unconditionally: on web it is a harmless no-op, on a device it is\n * what makes Save work.\n */\nexport interface ICropEncoder {\n /**\n * Produce one encoded file.\n *\n * @param uri the source image\n * @param sourceRect the region to keep, in SOURCE pixels, rotation undone\n * @param sourceSize the source's full pixel size\n * @param rotation degrees clockwise to apply when saving\n * @param size the output's pixel size, already decided by the search\n * @param quality 0–1; ignored by lossless formats\n * @param format the encoder to use\n * @returns the written file's uri, its size in bytes, and — when the\n * implementation knows them — the dimensions it actually produced\n *\n * ⚠️ `width`/`height` ARE REPORTED BECAUSE THE REQUEST IS NOT ALWAYS THE\n * RESULT. The bare-React-Native encoder asks its resizer for a square\n * ceiling with `contain` (the only orientation-safe way to drive it), so\n * the output can land a pixel or two off the requested size. Reporting the\n * number back beats the caller assuming the one it asked for.\n */\n (args: {\n uri: string;\n sourceRect: ICropRect;\n /**\n * The source's full size.\n *\n * ⚠️ THE BUNDLED ENCODER NO LONGER NEEDS THIS, and the field stays\n * anyway. It was added when that encoder rotated the file BEFORE\n * cropping it and therefore had to re-express the rect into the rotated\n * frame. Reordering the chain to crop first removed that mapping — but\n * the contract is public, any implementation that rotates first still\n * needs the dimensions, and they are free to carry. A canvas encoder\n * ignores the field entirely.\n */\n sourceSize: ICropSize;\n rotation: number;\n size: ICropSize;\n quality: number;\n format: CropOutputFormat;\n }): Promise<{uri: string; bytes: number; width?: number; height?: number}>;\n}\n\nlet encoder: ICropEncoder | null = null;\n\n/**\n * Register the native encoder. Call once, at app start, before the cropper is\n * used. Passing `null` unregisters it, which is what a test wants.\n */\nexport const setCropEncoder = (next: ICropEncoder | null): void => {\n encoder = next;\n};\n\n/** The registered encoder, or null. */\nexport const getCropEncoder = (): ICropEncoder | null => encoder;\n\n/** Whether a host app has wired one up. The native UI reads this to disable Save. */\nexport const hasCropEncoder = (): boolean => encoder !== null;\n"],"names":[],"mappings":"AA4DA,IAAI,OAAA,GAA+B,IAAA;AAM5B,MAAM,cAAA,GAAiB,CAAC,IAAA,KAAoC;AAC/D,EAAA,OAAA,GAAU,IAAA;AACd;AAGO,MAAM,iBAAiB,MAA2B;AAGlD,MAAM,cAAA,GAAiB,MAAe,OAAA,KAAY;;;;"}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
var ImageEditor = require('@react-native-community/image-editor');
|
|
4
|
+
var ImageResizer = require('@bam.tech/react-native-image-resizer');
|
|
5
|
+
|
|
6
|
+
const RESIZER_FORMAT = {
|
|
7
|
+
jpeg: "JPEG",
|
|
8
|
+
png: "PNG",
|
|
9
|
+
webp: "WEBP"
|
|
10
|
+
};
|
|
11
|
+
let cropCache = null;
|
|
12
|
+
const clearCropCache = () => {
|
|
13
|
+
cropCache = null;
|
|
14
|
+
};
|
|
15
|
+
const croppedSource = async (uri, rect) => {
|
|
16
|
+
const offset = { x: Math.round(rect.x), y: Math.round(rect.y) };
|
|
17
|
+
const size = { width: Math.round(rect.width), height: Math.round(rect.height) };
|
|
18
|
+
const key = `${uri}@${offset.x},${offset.y},${size.width},${size.height}`;
|
|
19
|
+
if (cropCache?.key === key) return cropCache.uri;
|
|
20
|
+
const out = await ImageEditor.cropImage(uri, { offset, size });
|
|
21
|
+
cropCache = { key, uri: out.uri };
|
|
22
|
+
return out.uri;
|
|
23
|
+
};
|
|
24
|
+
const rnImageEncoder = async ({
|
|
25
|
+
uri,
|
|
26
|
+
sourceRect,
|
|
27
|
+
rotation,
|
|
28
|
+
size,
|
|
29
|
+
quality,
|
|
30
|
+
format
|
|
31
|
+
}) => {
|
|
32
|
+
const cropped = await croppedSource(uri, sourceRect);
|
|
33
|
+
const ceiling = Math.max(1, Math.round(Math.max(size.width, size.height)));
|
|
34
|
+
const out = await ImageResizer.createResizedImage(
|
|
35
|
+
cropped,
|
|
36
|
+
ceiling,
|
|
37
|
+
ceiling,
|
|
38
|
+
RESIZER_FORMAT[format],
|
|
39
|
+
// ⚠️ 0–100 HERE, 0–1 IN THE CONTRACT. The shared search speaks in the
|
|
40
|
+
// fractions a canvas wants; this library wants percent. Passing 0.8
|
|
41
|
+
// straight through would ask for 0.8% quality and return a smear —
|
|
42
|
+
// which would still "fit the byte ceiling", silently.
|
|
43
|
+
Math.round(quality * 100),
|
|
44
|
+
rotation,
|
|
45
|
+
void 0,
|
|
46
|
+
false,
|
|
47
|
+
{ mode: "contain", onlyScaleDown: true }
|
|
48
|
+
);
|
|
49
|
+
return { uri: out.uri, bytes: out.size, width: out.width, height: out.height };
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
exports.clearCropCache = clearCropCache;
|
|
53
|
+
exports.rnImageEncoder = rnImageEncoder;
|
|
54
|
+
//# sourceMappingURL=rnImageEncoder.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"rnImageEncoder.cjs","sources":["../../../../../src/components/ImageCropper/encoders/rnImageEncoder.ts"],"sourcesContent":["import ImageEditor from \"@react-native-community/image-editor\";\nimport ImageResizer from \"@bam.tech/react-native-image-resizer\";\nimport type {CropOutputFormat} from \"@lmb-it/kitsconcerto-types\";\nimport type {ICropEncoder} from \"../encoderRegistry\";\n\n/**\n * The native encoder: crop with one library, then compress with the other.\n *\n * ⛔ IT TAKES TWO BECAUSE NEITHER DOES THE WHOLE JOB, and that was checked by\n * reading their types rather than assumed. `@react-native-community/image-editor`\n * crops a sub-rectangle (`offset` + `size`) — and cannot rotate.\n * `@bam.tech/react-native-image-resizer` rotates, resizes, and sets quality and\n * format — and its entire options object is `{mode, onlyScaleDown}`, so it has\n * no offset and cannot crop. (`expo-image-manipulator` does all four in one\n * call, which is why the plan named it, and it needs the Expo modules runtime\n * this bare app does not have.)\n *\n * ── CROP ONCE, COMPRESS MANY ──\n * ⛔ THE CROP RECT IS THE SAME ON EVERY ATTEMPT. The compression search varies\n * only the output SIZE and the QUALITY — the region of the photo was chosen by\n * the user before Save was pressed and never moves. So the crop happens once,\n * into a cached temporary file, and each of the (up to fourteen) attempts is a\n * single resizer call against it. Cropping inside the loop would be fourteen\n * identical crops of the same rect.\n *\n * ⚠️ AND THE LOSSY STEP IS LAST, which is the other reason this order is right.\n * The crop is taken at full quality; the only place bytes are deliberately\n * thrown away is the final encode. Compressing first and cropping after would\n * pay the quality cost twice.\n *\n * ⚠️ NO ROTATION MAPPING IS NEEDED IN THIS ORDER, and that is a real\n * simplification over the first version. `sourceRect` arrives in the ORIGINAL\n * image's coordinates with the rotation undone, and the file being cropped has\n * not been rotated yet — so the rect goes straight through. The previous chain\n * rotated first and therefore had to re-express the rect into the rotated\n * frame, which is one more place for a 90°-wrong crop to hide.\n */\n\nconst RESIZER_FORMAT: Record<CropOutputFormat, \"JPEG\" | \"PNG\" | \"WEBP\"> = {\n jpeg: \"JPEG\",\n png: \"PNG\",\n webp: \"WEBP\",\n};\n\nlet cropCache: {key: string; uri: string} | null = null;\n\n/** Drops the cached crop. Call when the cropper closes, and in tests. */\nexport const clearCropCache = (): void => {\n cropCache = null;\n};\n\n/**\n * The chosen region as a file, at full quality, cached across attempts.\n *\n * ⚠️ THE CACHE HOLDS ONE ENTRY. The cropper works on one image at a time, and\n * an unbounded map of temp-file paths in a long-lived app is a leak that looks\n * like nothing until storage fills.\n */\nconst croppedSource = async (\n uri: string,\n rect: {x: number; y: number; width: number; height: number},\n): Promise<string> => {\n const offset = {x: Math.round(rect.x), y: Math.round(rect.y)};\n const size = {width: Math.round(rect.width), height: Math.round(rect.height)};\n const key = `${uri}@${offset.x},${offset.y},${size.width},${size.height}`;\n if (cropCache?.key === key) return cropCache.uri;\n\n // ⚠️ NO `displaySize` AND NO `quality` HERE ON PURPOSE. This is an\n // intermediate: scaling it now would resample twice, and compressing it now\n // would throw away bytes the real encode is about to throw away again.\n const out = await ImageEditor.cropImage(uri, {offset, size});\n\n cropCache = {key, uri: out.uri};\n return out.uri;\n};\n\n/**\n * An {@link ICropEncoder} for bare React Native apps.\n *\n * Wire it once at app start:\n * ```ts\n * import {setCropEncoder} from '@lmb-it/kitsconcerto-native';\n * import {rnImageEncoder} from '@lmb-it/kitsconcerto-native/components/ImageCropper/encoders/rnImageEncoder';\n * setCropEncoder(rnImageEncoder);\n * ```\n */\nexport const rnImageEncoder: ICropEncoder = async ({\n uri,\n sourceRect,\n rotation,\n size,\n quality,\n format,\n}) => {\n const cropped = await croppedSource(uri, sourceRect);\n\n /**\n * ⛔ A SQUARE CEILING, NOT THE TARGET WIDTH AND HEIGHT, AND THIS IS THE ONE\n * THING HERE I CANNOT VERIFY WITHOUT A DEVICE. `createResizedImage` takes\n * `maxWidth`/`maxHeight` and a rotation, and its documentation does not say\n * whether the ceiling is applied BEFORE or AFTER the turn. Those two give\n * different answers for a quarter turn: a 1600×900 crop asked for 450×800\n * comes out 450×800 one way and 253×450 the other.\n *\n * A square box removes the question. `contain` preserves the aspect ratio,\n * and a square ceiling is the same box whichever way the image is facing —\n * so both orderings produce the identical result. The cost is that the\n * output may land a pixel or two off the exact request, which is why the\n * ACTUAL dimensions are read back from the response below rather than\n * assumed from the request.\n */\n const ceiling = Math.max(1, Math.round(Math.max(size.width, size.height)));\n\n const out = await ImageResizer.createResizedImage(\n cropped,\n ceiling,\n ceiling,\n RESIZER_FORMAT[format],\n // ⚠️ 0–100 HERE, 0–1 IN THE CONTRACT. The shared search speaks in the\n // fractions a canvas wants; this library wants percent. Passing 0.8\n // straight through would ask for 0.8% quality and return a smear —\n // which would still \"fit the byte ceiling\", silently.\n Math.round(quality * 100),\n rotation,\n undefined,\n false,\n {mode: \"contain\", onlyScaleDown: true},\n );\n\n return {uri: out.uri, bytes: out.size, width: out.width, height: out.height};\n};\n"],"names":[],"mappings":";;;;;AAsCA,MAAM,cAAA,GAAoE;AAAA,EACtE,IAAA,EAAM,MAAA;AAAA,EACN,GAAA,EAAK,KAAA;AAAA,EACL,IAAA,EAAM;AACV,CAAA;AAEA,IAAI,SAAA,GAA+C,IAAA;AAG5C,MAAM,iBAAiB,MAAY;AACtC,EAAA,SAAA,GAAY,IAAA;AAChB;AASA,MAAM,aAAA,GAAgB,OAClB,GAAA,EACA,IAAA,KACkB;AAClB,EAAA,MAAM,MAAA,GAAS,EAAC,CAAA,EAAG,IAAA,CAAK,KAAA,CAAM,IAAA,CAAK,CAAC,CAAA,EAAG,CAAA,EAAG,IAAA,CAAK,KAAA,CAAM,IAAA,CAAK,CAAC,CAAA,EAAC;AAC5D,EAAA,MAAM,IAAA,GAAO,EAAC,KAAA,EAAO,IAAA,CAAK,KAAA,CAAM,IAAA,CAAK,KAAK,CAAA,EAAG,MAAA,EAAQ,IAAA,CAAK,KAAA,CAAM,IAAA,CAAK,MAAM,CAAA,EAAC;AAC5E,EAAA,MAAM,GAAA,GAAM,CAAA,EAAG,GAAG,CAAA,CAAA,EAAI,OAAO,CAAC,CAAA,CAAA,EAAI,MAAA,CAAO,CAAC,CAAA,CAAA,EAAI,IAAA,CAAK,KAAK,CAAA,CAAA,EAAI,KAAK,MAAM,CAAA,CAAA;AACvE,EAAA,IAAI,SAAA,EAAW,GAAA,KAAQ,GAAA,EAAK,OAAO,SAAA,CAAU,GAAA;AAK7C,EAAA,MAAM,GAAA,GAAM,MAAM,WAAA,CAAY,SAAA,CAAU,KAAK,EAAC,MAAA,EAAQ,MAAK,CAAA;AAE3D,EAAA,SAAA,GAAY,EAAC,GAAA,EAAK,GAAA,EAAK,GAAA,CAAI,GAAA,EAAG;AAC9B,EAAA,OAAO,GAAA,CAAI,GAAA;AACf,CAAA;AAYO,MAAM,iBAA+B,OAAO;AAAA,EAC/C,GAAA;AAAA,EACA,UAAA;AAAA,EACA,QAAA;AAAA,EACA,IAAA;AAAA,EACA,OAAA;AAAA,EACA;AACJ,CAAA,KAAM;AACF,EAAA,MAAM,OAAA,GAAU,MAAM,aAAA,CAAc,GAAA,EAAK,UAAU,CAAA;AAiBnD,EAAA,MAAM,OAAA,GAAU,IAAA,CAAK,GAAA,CAAI,CAAA,EAAG,IAAA,CAAK,KAAA,CAAM,IAAA,CAAK,GAAA,CAAI,IAAA,CAAK,KAAA,EAAO,IAAA,CAAK,MAAM,CAAC,CAAC,CAAA;AAEzE,EAAA,MAAM,GAAA,GAAM,MAAM,YAAA,CAAa,kBAAA;AAAA,IAC3B,OAAA;AAAA,IACA,OAAA;AAAA,IACA,OAAA;AAAA,IACA,eAAe,MAAM,CAAA;AAAA;AAAA;AAAA;AAAA;AAAA,IAKrB,IAAA,CAAK,KAAA,CAAM,OAAA,GAAU,GAAG,CAAA;AAAA,IACxB,QAAA;AAAA,IACA,MAAA;AAAA,IACA,KAAA;AAAA,IACA,EAAC,IAAA,EAAM,SAAA,EAAW,aAAA,EAAe,IAAA;AAAI,GACzC;AAEA,EAAA,OAAO,EAAC,GAAA,EAAK,GAAA,CAAI,GAAA,EAAK,KAAA,EAAO,GAAA,CAAI,IAAA,EAAM,KAAA,EAAO,GAAA,CAAI,KAAA,EAAO,MAAA,EAAQ,GAAA,CAAI,MAAA,EAAM;AAC/E;;;;;"}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import ImageEditor from '@react-native-community/image-editor';
|
|
2
|
+
import ImageResizer from '@bam.tech/react-native-image-resizer';
|
|
3
|
+
|
|
4
|
+
const RESIZER_FORMAT = {
|
|
5
|
+
jpeg: "JPEG",
|
|
6
|
+
png: "PNG",
|
|
7
|
+
webp: "WEBP"
|
|
8
|
+
};
|
|
9
|
+
let cropCache = null;
|
|
10
|
+
const clearCropCache = () => {
|
|
11
|
+
cropCache = null;
|
|
12
|
+
};
|
|
13
|
+
const croppedSource = async (uri, rect) => {
|
|
14
|
+
const offset = { x: Math.round(rect.x), y: Math.round(rect.y) };
|
|
15
|
+
const size = { width: Math.round(rect.width), height: Math.round(rect.height) };
|
|
16
|
+
const key = `${uri}@${offset.x},${offset.y},${size.width},${size.height}`;
|
|
17
|
+
if (cropCache?.key === key) return cropCache.uri;
|
|
18
|
+
const out = await ImageEditor.cropImage(uri, { offset, size });
|
|
19
|
+
cropCache = { key, uri: out.uri };
|
|
20
|
+
return out.uri;
|
|
21
|
+
};
|
|
22
|
+
const rnImageEncoder = async ({
|
|
23
|
+
uri,
|
|
24
|
+
sourceRect,
|
|
25
|
+
rotation,
|
|
26
|
+
size,
|
|
27
|
+
quality,
|
|
28
|
+
format
|
|
29
|
+
}) => {
|
|
30
|
+
const cropped = await croppedSource(uri, sourceRect);
|
|
31
|
+
const ceiling = Math.max(1, Math.round(Math.max(size.width, size.height)));
|
|
32
|
+
const out = await ImageResizer.createResizedImage(
|
|
33
|
+
cropped,
|
|
34
|
+
ceiling,
|
|
35
|
+
ceiling,
|
|
36
|
+
RESIZER_FORMAT[format],
|
|
37
|
+
// ⚠️ 0–100 HERE, 0–1 IN THE CONTRACT. The shared search speaks in the
|
|
38
|
+
// fractions a canvas wants; this library wants percent. Passing 0.8
|
|
39
|
+
// straight through would ask for 0.8% quality and return a smear —
|
|
40
|
+
// which would still "fit the byte ceiling", silently.
|
|
41
|
+
Math.round(quality * 100),
|
|
42
|
+
rotation,
|
|
43
|
+
void 0,
|
|
44
|
+
false,
|
|
45
|
+
{ mode: "contain", onlyScaleDown: true }
|
|
46
|
+
);
|
|
47
|
+
return { uri: out.uri, bytes: out.size, width: out.width, height: out.height };
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
export { clearCropCache, rnImageEncoder };
|
|
51
|
+
//# sourceMappingURL=rnImageEncoder.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"rnImageEncoder.js","sources":["../../../../../src/components/ImageCropper/encoders/rnImageEncoder.ts"],"sourcesContent":["import ImageEditor from \"@react-native-community/image-editor\";\nimport ImageResizer from \"@bam.tech/react-native-image-resizer\";\nimport type {CropOutputFormat} from \"@lmb-it/kitsconcerto-types\";\nimport type {ICropEncoder} from \"../encoderRegistry\";\n\n/**\n * The native encoder: crop with one library, then compress with the other.\n *\n * ⛔ IT TAKES TWO BECAUSE NEITHER DOES THE WHOLE JOB, and that was checked by\n * reading their types rather than assumed. `@react-native-community/image-editor`\n * crops a sub-rectangle (`offset` + `size`) — and cannot rotate.\n * `@bam.tech/react-native-image-resizer` rotates, resizes, and sets quality and\n * format — and its entire options object is `{mode, onlyScaleDown}`, so it has\n * no offset and cannot crop. (`expo-image-manipulator` does all four in one\n * call, which is why the plan named it, and it needs the Expo modules runtime\n * this bare app does not have.)\n *\n * ── CROP ONCE, COMPRESS MANY ──\n * ⛔ THE CROP RECT IS THE SAME ON EVERY ATTEMPT. The compression search varies\n * only the output SIZE and the QUALITY — the region of the photo was chosen by\n * the user before Save was pressed and never moves. So the crop happens once,\n * into a cached temporary file, and each of the (up to fourteen) attempts is a\n * single resizer call against it. Cropping inside the loop would be fourteen\n * identical crops of the same rect.\n *\n * ⚠️ AND THE LOSSY STEP IS LAST, which is the other reason this order is right.\n * The crop is taken at full quality; the only place bytes are deliberately\n * thrown away is the final encode. Compressing first and cropping after would\n * pay the quality cost twice.\n *\n * ⚠️ NO ROTATION MAPPING IS NEEDED IN THIS ORDER, and that is a real\n * simplification over the first version. `sourceRect` arrives in the ORIGINAL\n * image's coordinates with the rotation undone, and the file being cropped has\n * not been rotated yet — so the rect goes straight through. The previous chain\n * rotated first and therefore had to re-express the rect into the rotated\n * frame, which is one more place for a 90°-wrong crop to hide.\n */\n\nconst RESIZER_FORMAT: Record<CropOutputFormat, \"JPEG\" | \"PNG\" | \"WEBP\"> = {\n jpeg: \"JPEG\",\n png: \"PNG\",\n webp: \"WEBP\",\n};\n\nlet cropCache: {key: string; uri: string} | null = null;\n\n/** Drops the cached crop. Call when the cropper closes, and in tests. */\nexport const clearCropCache = (): void => {\n cropCache = null;\n};\n\n/**\n * The chosen region as a file, at full quality, cached across attempts.\n *\n * ⚠️ THE CACHE HOLDS ONE ENTRY. The cropper works on one image at a time, and\n * an unbounded map of temp-file paths in a long-lived app is a leak that looks\n * like nothing until storage fills.\n */\nconst croppedSource = async (\n uri: string,\n rect: {x: number; y: number; width: number; height: number},\n): Promise<string> => {\n const offset = {x: Math.round(rect.x), y: Math.round(rect.y)};\n const size = {width: Math.round(rect.width), height: Math.round(rect.height)};\n const key = `${uri}@${offset.x},${offset.y},${size.width},${size.height}`;\n if (cropCache?.key === key) return cropCache.uri;\n\n // ⚠️ NO `displaySize` AND NO `quality` HERE ON PURPOSE. This is an\n // intermediate: scaling it now would resample twice, and compressing it now\n // would throw away bytes the real encode is about to throw away again.\n const out = await ImageEditor.cropImage(uri, {offset, size});\n\n cropCache = {key, uri: out.uri};\n return out.uri;\n};\n\n/**\n * An {@link ICropEncoder} for bare React Native apps.\n *\n * Wire it once at app start:\n * ```ts\n * import {setCropEncoder} from '@lmb-it/kitsconcerto-native';\n * import {rnImageEncoder} from '@lmb-it/kitsconcerto-native/components/ImageCropper/encoders/rnImageEncoder';\n * setCropEncoder(rnImageEncoder);\n * ```\n */\nexport const rnImageEncoder: ICropEncoder = async ({\n uri,\n sourceRect,\n rotation,\n size,\n quality,\n format,\n}) => {\n const cropped = await croppedSource(uri, sourceRect);\n\n /**\n * ⛔ A SQUARE CEILING, NOT THE TARGET WIDTH AND HEIGHT, AND THIS IS THE ONE\n * THING HERE I CANNOT VERIFY WITHOUT A DEVICE. `createResizedImage` takes\n * `maxWidth`/`maxHeight` and a rotation, and its documentation does not say\n * whether the ceiling is applied BEFORE or AFTER the turn. Those two give\n * different answers for a quarter turn: a 1600×900 crop asked for 450×800\n * comes out 450×800 one way and 253×450 the other.\n *\n * A square box removes the question. `contain` preserves the aspect ratio,\n * and a square ceiling is the same box whichever way the image is facing —\n * so both orderings produce the identical result. The cost is that the\n * output may land a pixel or two off the exact request, which is why the\n * ACTUAL dimensions are read back from the response below rather than\n * assumed from the request.\n */\n const ceiling = Math.max(1, Math.round(Math.max(size.width, size.height)));\n\n const out = await ImageResizer.createResizedImage(\n cropped,\n ceiling,\n ceiling,\n RESIZER_FORMAT[format],\n // ⚠️ 0–100 HERE, 0–1 IN THE CONTRACT. The shared search speaks in the\n // fractions a canvas wants; this library wants percent. Passing 0.8\n // straight through would ask for 0.8% quality and return a smear —\n // which would still \"fit the byte ceiling\", silently.\n Math.round(quality * 100),\n rotation,\n undefined,\n false,\n {mode: \"contain\", onlyScaleDown: true},\n );\n\n return {uri: out.uri, bytes: out.size, width: out.width, height: out.height};\n};\n"],"names":[],"mappings":";;;AAsCA,MAAM,cAAA,GAAoE;AAAA,EACtE,IAAA,EAAM,MAAA;AAAA,EACN,GAAA,EAAK,KAAA;AAAA,EACL,IAAA,EAAM;AACV,CAAA;AAEA,IAAI,SAAA,GAA+C,IAAA;AAG5C,MAAM,iBAAiB,MAAY;AACtC,EAAA,SAAA,GAAY,IAAA;AAChB;AASA,MAAM,aAAA,GAAgB,OAClB,GAAA,EACA,IAAA,KACkB;AAClB,EAAA,MAAM,MAAA,GAAS,EAAC,CAAA,EAAG,IAAA,CAAK,KAAA,CAAM,IAAA,CAAK,CAAC,CAAA,EAAG,CAAA,EAAG,IAAA,CAAK,KAAA,CAAM,IAAA,CAAK,CAAC,CAAA,EAAC;AAC5D,EAAA,MAAM,IAAA,GAAO,EAAC,KAAA,EAAO,IAAA,CAAK,KAAA,CAAM,IAAA,CAAK,KAAK,CAAA,EAAG,MAAA,EAAQ,IAAA,CAAK,KAAA,CAAM,IAAA,CAAK,MAAM,CAAA,EAAC;AAC5E,EAAA,MAAM,GAAA,GAAM,CAAA,EAAG,GAAG,CAAA,CAAA,EAAI,OAAO,CAAC,CAAA,CAAA,EAAI,MAAA,CAAO,CAAC,CAAA,CAAA,EAAI,IAAA,CAAK,KAAK,CAAA,CAAA,EAAI,KAAK,MAAM,CAAA,CAAA;AACvE,EAAA,IAAI,SAAA,EAAW,GAAA,KAAQ,GAAA,EAAK,OAAO,SAAA,CAAU,GAAA;AAK7C,EAAA,MAAM,GAAA,GAAM,MAAM,WAAA,CAAY,SAAA,CAAU,KAAK,EAAC,MAAA,EAAQ,MAAK,CAAA;AAE3D,EAAA,SAAA,GAAY,EAAC,GAAA,EAAK,GAAA,EAAK,GAAA,CAAI,GAAA,EAAG;AAC9B,EAAA,OAAO,GAAA,CAAI,GAAA;AACf,CAAA;AAYO,MAAM,iBAA+B,OAAO;AAAA,EAC/C,GAAA;AAAA,EACA,UAAA;AAAA,EACA,QAAA;AAAA,EACA,IAAA;AAAA,EACA,OAAA;AAAA,EACA;AACJ,CAAA,KAAM;AACF,EAAA,MAAM,OAAA,GAAU,MAAM,aAAA,CAAc,GAAA,EAAK,UAAU,CAAA;AAiBnD,EAAA,MAAM,OAAA,GAAU,IAAA,CAAK,GAAA,CAAI,CAAA,EAAG,IAAA,CAAK,KAAA,CAAM,IAAA,CAAK,GAAA,CAAI,IAAA,CAAK,KAAA,EAAO,IAAA,CAAK,MAAM,CAAC,CAAC,CAAA;AAEzE,EAAA,MAAM,GAAA,GAAM,MAAM,YAAA,CAAa,kBAAA;AAAA,IAC3B,OAAA;AAAA,IACA,OAAA;AAAA,IACA,OAAA;AAAA,IACA,eAAe,MAAM,CAAA;AAAA;AAAA;AAAA;AAAA;AAAA,IAKrB,IAAA,CAAK,KAAA,CAAM,OAAA,GAAU,GAAG,CAAA;AAAA,IACxB,QAAA;AAAA,IACA,MAAA;AAAA,IACA,KAAA;AAAA,IACA,EAAC,IAAA,EAAM,SAAA,EAAW,aAAA,EAAe,IAAA;AAAI,GACzC;AAEA,EAAA,OAAO,EAAC,GAAA,EAAK,GAAA,CAAI,GAAA,EAAK,KAAA,EAAO,GAAA,CAAI,IAAA,EAAM,KAAA,EAAO,GAAA,CAAI,KAAA,EAAO,MAAA,EAAQ,GAAA,CAAI,MAAA,EAAM;AAC/E;;;;"}
|
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const clamp = (value, min, max) => {
|
|
4
|
+
if (max < min) return min;
|
|
5
|
+
return value < min ? min : value > max ? max : value;
|
|
6
|
+
};
|
|
7
|
+
const aspectOf = (aspect) => aspect[0] / aspect[1];
|
|
8
|
+
const coverScale = (content, target) => Math.max(target.width / content.width, target.height / content.height);
|
|
9
|
+
const rectRight = (r) => r.x + r.width;
|
|
10
|
+
const rectBottom = (r) => r.y + r.height;
|
|
11
|
+
const rectCenter = (r) => ({
|
|
12
|
+
x: r.x + r.width / 2,
|
|
13
|
+
y: r.y + r.height / 2
|
|
14
|
+
});
|
|
15
|
+
const largestRectOfRatio = (bounds, ratio) => {
|
|
16
|
+
const byWidth = { width: bounds.width, height: bounds.width / ratio };
|
|
17
|
+
return byWidth.height <= bounds.height ? byWidth : { width: bounds.height * ratio, height: bounds.height };
|
|
18
|
+
};
|
|
19
|
+
const centerRect = (size, within) => ({
|
|
20
|
+
x: (within.width - size.width) / 2,
|
|
21
|
+
y: (within.height - size.height) / 2,
|
|
22
|
+
width: size.width,
|
|
23
|
+
height: size.height
|
|
24
|
+
});
|
|
25
|
+
const clampRectInside = (rect, bounds) => ({
|
|
26
|
+
...rect,
|
|
27
|
+
x: clamp(rect.x, bounds.x, rectRight(bounds) - rect.width),
|
|
28
|
+
y: clamp(rect.y, bounds.y, rectBottom(bounds) - rect.height)
|
|
29
|
+
});
|
|
30
|
+
const orientedSize = (source, rotation) => rotation === 90 || rotation === 270 ? { width: source.height, height: source.width } : { width: source.width, height: source.height };
|
|
31
|
+
const orientedPointToSource = (p, source, rotation) => {
|
|
32
|
+
switch (rotation) {
|
|
33
|
+
case 90:
|
|
34
|
+
return { x: p.y, y: source.height - p.x };
|
|
35
|
+
case 180:
|
|
36
|
+
return { x: source.width - p.x, y: source.height - p.y };
|
|
37
|
+
case 270:
|
|
38
|
+
return { x: source.width - p.y, y: p.x };
|
|
39
|
+
default:
|
|
40
|
+
return { x: p.x, y: p.y };
|
|
41
|
+
}
|
|
42
|
+
};
|
|
43
|
+
const orientedRectToSource = (rect, source, rotation) => {
|
|
44
|
+
const a = orientedPointToSource({ x: rect.x, y: rect.y }, source, rotation);
|
|
45
|
+
const b = orientedPointToSource(
|
|
46
|
+
{ x: rect.x + rect.width, y: rect.y + rect.height },
|
|
47
|
+
source,
|
|
48
|
+
rotation
|
|
49
|
+
);
|
|
50
|
+
return {
|
|
51
|
+
x: Math.min(a.x, b.x),
|
|
52
|
+
y: Math.min(a.y, b.y),
|
|
53
|
+
width: Math.abs(b.x - a.x),
|
|
54
|
+
height: Math.abs(b.y - a.y)
|
|
55
|
+
};
|
|
56
|
+
};
|
|
57
|
+
const imageRect = (oriented, t) => ({
|
|
58
|
+
x: t.x,
|
|
59
|
+
y: t.y,
|
|
60
|
+
width: oriented.width * t.scale,
|
|
61
|
+
height: oriented.height * t.scale
|
|
62
|
+
});
|
|
63
|
+
const viewportToOriented = (p, t) => ({
|
|
64
|
+
x: (p.x - t.x) / t.scale,
|
|
65
|
+
y: (p.y - t.y) / t.scale
|
|
66
|
+
});
|
|
67
|
+
const zoomAbout = (t, anchor, nextScale) => {
|
|
68
|
+
const k = nextScale / t.scale;
|
|
69
|
+
return {
|
|
70
|
+
scale: nextScale,
|
|
71
|
+
x: anchor.x - (anchor.x - t.x) * k,
|
|
72
|
+
y: anchor.y - (anchor.y - t.y) * k
|
|
73
|
+
};
|
|
74
|
+
};
|
|
75
|
+
const minScaleToCover = (oriented, crop) => coverScale(oriented, crop);
|
|
76
|
+
const coverAxis = (imgStart, imgLen, cropStart, cropLen) => imgLen < cropLen ? cropStart + (cropLen - imgLen) / 2 : clamp(imgStart, cropStart + cropLen - imgLen, cropStart);
|
|
77
|
+
const clampTransformToCover = (t, oriented, crop) => {
|
|
78
|
+
const img = imageRect(oriented, t);
|
|
79
|
+
return {
|
|
80
|
+
scale: t.scale,
|
|
81
|
+
x: coverAxis(img.x, img.width, crop.x, crop.width),
|
|
82
|
+
y: coverAxis(img.y, img.height, crop.y, crop.height)
|
|
83
|
+
};
|
|
84
|
+
};
|
|
85
|
+
const anchorFor = (rect, handle) => {
|
|
86
|
+
const c = rectCenter(rect);
|
|
87
|
+
switch (handle) {
|
|
88
|
+
case "se":
|
|
89
|
+
return { x: rect.x, y: rect.y };
|
|
90
|
+
case "sw":
|
|
91
|
+
return { x: rectRight(rect), y: rect.y };
|
|
92
|
+
case "ne":
|
|
93
|
+
return { x: rect.x, y: rectBottom(rect) };
|
|
94
|
+
case "nw":
|
|
95
|
+
return { x: rectRight(rect), y: rectBottom(rect) };
|
|
96
|
+
case "e":
|
|
97
|
+
return { x: rect.x, y: c.y };
|
|
98
|
+
case "w":
|
|
99
|
+
return { x: rectRight(rect), y: c.y };
|
|
100
|
+
case "s":
|
|
101
|
+
return { x: c.x, y: rect.y };
|
|
102
|
+
default:
|
|
103
|
+
return { x: c.x, y: rectBottom(rect) };
|
|
104
|
+
}
|
|
105
|
+
};
|
|
106
|
+
const growsLeft = (h) => h === "w" || h === "nw" || h === "sw";
|
|
107
|
+
const growsUp = (h) => h === "n" || h === "ne" || h === "nw";
|
|
108
|
+
const movesX = (h) => h !== "n" && h !== "s";
|
|
109
|
+
const movesY = (h) => h !== "e" && h !== "w";
|
|
110
|
+
const resizeRect = (rect, handle, pointer, options = {}) => {
|
|
111
|
+
const { ratio, min, bounds } = options;
|
|
112
|
+
const anchor = anchorFor(rect, handle);
|
|
113
|
+
let width = movesX(handle) ? growsLeft(handle) ? anchor.x - pointer.x : pointer.x - anchor.x : rect.width;
|
|
114
|
+
let height = movesY(handle) ? growsUp(handle) ? anchor.y - pointer.y : pointer.y - anchor.y : rect.height;
|
|
115
|
+
if (ratio) {
|
|
116
|
+
if (movesX(handle) && movesY(handle)) {
|
|
117
|
+
width = Math.max(width, height * ratio);
|
|
118
|
+
height = width / ratio;
|
|
119
|
+
} else if (movesX(handle)) {
|
|
120
|
+
height = width / ratio;
|
|
121
|
+
} else {
|
|
122
|
+
width = height * ratio;
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
if (bounds) {
|
|
126
|
+
const c = rectCenter(rect);
|
|
127
|
+
const maxWidth = Math.max(0, movesX(handle) ? growsLeft(handle) ? anchor.x - bounds.x : rectRight(bounds) - anchor.x : 2 * Math.min(c.x - bounds.x, rectRight(bounds) - c.x));
|
|
128
|
+
const maxHeight = Math.max(0, movesY(handle) ? growsUp(handle) ? anchor.y - bounds.y : rectBottom(bounds) - anchor.y : 2 * Math.min(c.y - bounds.y, rectBottom(bounds) - c.y));
|
|
129
|
+
if (ratio) {
|
|
130
|
+
if (width > 0 && height > 0) {
|
|
131
|
+
const shrink = Math.min(1, maxWidth / width, maxHeight / height);
|
|
132
|
+
if (shrink < 1 && Number.isFinite(shrink)) {
|
|
133
|
+
width *= shrink;
|
|
134
|
+
height *= shrink;
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
} else {
|
|
138
|
+
if (width > 0) width = Math.min(width, maxWidth);
|
|
139
|
+
if (height > 0) height = Math.min(height, maxHeight);
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
if (min) {
|
|
143
|
+
const minWidth = ratio ? Math.max(min.width, min.height * ratio) : min.width;
|
|
144
|
+
const minHeight = ratio ? minWidth / ratio : min.height;
|
|
145
|
+
if (width < minWidth) {
|
|
146
|
+
width = minWidth;
|
|
147
|
+
if (ratio) height = minHeight;
|
|
148
|
+
}
|
|
149
|
+
if (height < minHeight) {
|
|
150
|
+
height = minHeight;
|
|
151
|
+
if (ratio) width = minWidth;
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
const x = movesX(handle) ? growsLeft(handle) ? anchor.x - width : anchor.x : anchor.x - width / 2;
|
|
155
|
+
const y = movesY(handle) ? growsUp(handle) ? anchor.y - height : anchor.y : anchor.y - height / 2;
|
|
156
|
+
return { x, y, width, height };
|
|
157
|
+
};
|
|
158
|
+
const cropToSourceRect = (crop, t, source, rotation = 0) => {
|
|
159
|
+
const topLeft = viewportToOriented({ x: crop.x, y: crop.y }, t);
|
|
160
|
+
const bottomRight = viewportToOriented({ x: rectRight(crop), y: rectBottom(crop) }, t);
|
|
161
|
+
const oriented = {
|
|
162
|
+
x: topLeft.x,
|
|
163
|
+
y: topLeft.y,
|
|
164
|
+
width: bottomRight.x - topLeft.x,
|
|
165
|
+
height: bottomRight.y - topLeft.y
|
|
166
|
+
};
|
|
167
|
+
const inSource = orientedRectToSource(oriented, source, rotation);
|
|
168
|
+
const x = clamp(inSource.x, 0, source.width);
|
|
169
|
+
const y = clamp(inSource.y, 0, source.height);
|
|
170
|
+
return {
|
|
171
|
+
x,
|
|
172
|
+
y,
|
|
173
|
+
width: clamp(inSource.width, 0, source.width - x),
|
|
174
|
+
height: clamp(inSource.height, 0, source.height - y)
|
|
175
|
+
};
|
|
176
|
+
};
|
|
177
|
+
const fitWithin = (size, max) => {
|
|
178
|
+
const limits = [
|
|
179
|
+
max.width && max.width > 0 ? max.width / size.width : 1,
|
|
180
|
+
max.height && max.height > 0 ? max.height / size.height : 1
|
|
181
|
+
];
|
|
182
|
+
const scale = Math.min(1, ...limits);
|
|
183
|
+
return { width: size.width * scale, height: size.height * scale };
|
|
184
|
+
};
|
|
185
|
+
const rotateCw = (rotation) => (rotation + 90) % 360;
|
|
186
|
+
|
|
187
|
+
exports.aspectOf = aspectOf;
|
|
188
|
+
exports.centerRect = centerRect;
|
|
189
|
+
exports.clamp = clamp;
|
|
190
|
+
exports.clampRectInside = clampRectInside;
|
|
191
|
+
exports.clampTransformToCover = clampTransformToCover;
|
|
192
|
+
exports.coverScale = coverScale;
|
|
193
|
+
exports.cropToSourceRect = cropToSourceRect;
|
|
194
|
+
exports.fitWithin = fitWithin;
|
|
195
|
+
exports.imageRect = imageRect;
|
|
196
|
+
exports.largestRectOfRatio = largestRectOfRatio;
|
|
197
|
+
exports.minScaleToCover = minScaleToCover;
|
|
198
|
+
exports.orientedPointToSource = orientedPointToSource;
|
|
199
|
+
exports.orientedRectToSource = orientedRectToSource;
|
|
200
|
+
exports.orientedSize = orientedSize;
|
|
201
|
+
exports.rectBottom = rectBottom;
|
|
202
|
+
exports.rectCenter = rectCenter;
|
|
203
|
+
exports.rectRight = rectRight;
|
|
204
|
+
exports.resizeRect = resizeRect;
|
|
205
|
+
exports.rotateCw = rotateCw;
|
|
206
|
+
exports.viewportToOriented = viewportToOriented;
|
|
207
|
+
exports.zoomAbout = zoomAbout;
|
|
208
|
+
//# sourceMappingURL=geometry.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"geometry.cjs","sources":["../../../../src/components/ImageCropper/geometry.ts"],"sourcesContent":["import type {\n CropAspect,\n CropHandle,\n CropRotation,\n ICropPoint,\n ICropRect,\n ICropResizeOptions,\n ICropSize,\n ICropTransform,\n} from \"@lmb-it/kitsconcerto-types\";\n\n/**\n * The cropper's arithmetic. No React, no DOM, no React Native — this file is\n * the half of the component that both platforms run unchanged.\n *\n * ⛔ IT IS SPLIT OUT BECAUSE THE TWO RENDERERS AGREEING IS NOT OPTIONAL. A web\n * cropper and a native cropper that each derive \"where did the user actually\n * crop\" from their own copy of this maths will diverge — and the way you find\n * out is a profile photo that is framed correctly on the phone and off-centre\n * on the dashboard, from one upload. Every number that decides the OUTPUT comes\n * from here; the platform files only draw and listen.\n *\n * ⚠️ READ `geometry.types.ts` FIRST FOR THE THREE COORDINATE SPACES. Every\n * function below belongs to exactly one, and the ones that cross between spaces\n * say so in their name (`viewportToOriented`, `orientedRectToSource`).\n */\n\n/* ─── Scalars ─────────────────────────────────────────────────────────────── */\n\n/**\n * Constrain `value` to [`min`, `max`].\n *\n * ⚠️ `max < min` RETURNS `min`, IT DOES NOT THROW OR RETURN NaN. Inverted\n * bounds are not a caller error here — they are the ordinary way a range\n * collapses, e.g. the legal x of an image narrower than the window it has to\n * cover. Returning `min` pins such a case to one deterministic edge instead of\n * letting it oscillate between two. Callers that need the midpoint instead\n * detect the collapse themselves; {@link clampTransformToCover} does.\n */\nexport const clamp = (value: number, min: number, max: number): number => {\n if (max < min) return min;\n return value < min ? min : value > max ? max : value;\n};\n\n/** `[16, 9]` → `1.777…`. Width ÷ height, the way CSS `aspect-ratio` reads. */\nexport const aspectOf = (aspect: CropAspect): number => aspect[0] / aspect[1];\n\n/**\n * The SMALLEST uniform scale at which `content` fully covers `target` —\n * `background-size: cover`.\n *\n * ⚠️ RETURNS `Infinity` FOR A ZERO-SIZED `content`, by plain division. That is\n * the honest answer (nothing scales up from nothing) and it propagates loudly\n * rather than silently producing a 1. Callers hold off until the image has\n * loaded and its natural size is known.\n */\nexport const coverScale = (content: ICropSize, target: ICropSize): number =>\n Math.max(target.width / content.width, target.height / content.height);\n\n/** The LARGEST uniform scale at which `content` fits inside `target` — `contain`. */\nexport const containScale = (content: ICropSize, target: ICropSize): number =>\n Math.min(target.width / content.width, target.height / content.height);\n\n/* ─── Rects ───────────────────────────────────────────────────────────────── */\n\nexport const rectRight = (r: ICropRect): number => r.x + r.width;\nexport const rectBottom = (r: ICropRect): number => r.y + r.height;\nexport const rectCenter = (r: ICropRect): ICropPoint => ({\n x: r.x + r.width / 2,\n y: r.y + r.height / 2,\n});\n\n/** The largest `ratio`-shaped size that fits inside `bounds`. */\nexport const largestRectOfRatio = (bounds: ICropSize, ratio: number): ICropSize => {\n const byWidth = {width: bounds.width, height: bounds.width / ratio};\n return byWidth.height <= bounds.height\n ? byWidth\n : {width: bounds.height * ratio, height: bounds.height};\n};\n\n/** Place `size` centred inside `within`. */\nexport const centerRect = (size: ICropSize, within: ICropSize): ICropRect => ({\n x: (within.width - size.width) / 2,\n y: (within.height - size.height) / 2,\n width: size.width,\n height: size.height,\n});\n\n/**\n * Move `rect` — never resize it — until it lies inside `bounds`.\n *\n * ⚠️ A RECT LARGER THAN `bounds` IS PINNED TO THE TOP-LEFT rather than\n * resized, via {@link clamp}'s inverted-bounds rule. Resizing here would make\n * this function silently change a crop's aspect, which is the one thing the\n * crop window is not allowed to do on its own.\n */\nexport const clampRectInside = (rect: ICropRect, bounds: ICropRect): ICropRect => ({\n ...rect,\n x: clamp(rect.x, bounds.x, rectRight(bounds) - rect.width),\n y: clamp(rect.y, bounds.y, rectBottom(bounds) - rect.height),\n});\n\n/* ─── Rotation ────────────────────────────────────────────────────────────── */\n\n/** Source size after a 90°-step rotation — swapped for 90 and 270. */\nexport const orientedSize = (source: ICropSize, rotation: CropRotation): ICropSize =>\n rotation === 90 || rotation === 270\n ? {width: source.height, height: source.width}\n : {width: source.width, height: source.height};\n\n/**\n * An ORIENTED point back to the SOURCE pixel it came from.\n *\n * ⛔ THIS IS THE INVERSE OF THE DISPLAY ROTATION, NOT THE ROTATION ITSELF, and\n * getting that backwards is a 90°-wrong crop that still looks plausible in a\n * unit test written the same wrong way round. The round-trip property in the\n * test file is what actually pins it: four 90° steps must land back on the\n * identity.\n */\nexport const orientedPointToSource = (\n p: ICropPoint,\n source: ICropSize,\n rotation: CropRotation,\n): ICropPoint => {\n switch (rotation) {\n case 90:\n return {x: p.y, y: source.height - p.x};\n case 180:\n return {x: source.width - p.x, y: source.height - p.y};\n case 270:\n return {x: source.width - p.y, y: p.x};\n default:\n return {x: p.x, y: p.y};\n }\n};\n\n/**\n * An ORIENTED rect back to SOURCE pixels.\n *\n * ⚠️ MAPPED BY ITS CORNERS AND RE-NORMALISED, not by per-rotation width/height\n * formulas. The corner map is one expression that is right for all four cases;\n * the formulas are four chances to transpose a width for a height, and the\n * wrong one of those is a crop that is correct at 0° and subtly off at 90°.\n */\nexport const orientedRectToSource = (\n rect: ICropRect,\n source: ICropSize,\n rotation: CropRotation,\n): ICropRect => {\n const a = orientedPointToSource({x: rect.x, y: rect.y}, source, rotation);\n const b = orientedPointToSource(\n {x: rect.x + rect.width, y: rect.y + rect.height},\n source,\n rotation,\n );\n return {\n x: Math.min(a.x, b.x),\n y: Math.min(a.y, b.y),\n width: Math.abs(b.x - a.x),\n height: Math.abs(b.y - a.y),\n };\n};\n\n/**\n * A SOURCE rect forward into ORIENTED space — the inverse of\n * {@link orientedRectToSource}.\n *\n * ⛔ IT EXISTS FOR ENCODERS THAT CROP AN ALREADY-ROTATED FILE. The native path\n * rotates once into a temporary file and then crops it, because the library\n * that can crop cannot rotate and the one that can rotate cannot crop. That\n * crop has to be expressed in the ROTATED image's coordinates, and the rect the\n * component computed is in the source's. Web needs none of this — a canvas\n * rotates and crops in one draw — which is exactly why this is a separate\n * function and not a change to the one above.\n *\n * ⚠️ IMPLEMENTED BY CALLING THE INVERSE ROTATION, not by writing four more\n * cases. Turning a rect back is the same operation as turning it, with the\n * complementary angle and the sides swapped; two sets of hand-written cases are\n * two chances to disagree, and the round-trip test is what would have to catch\n * it.\n */\nexport const sourceRectToOriented = (\n rect: ICropRect,\n source: ICropSize,\n rotation: CropRotation,\n): ICropRect => {\n const oriented = orientedSize(source, rotation);\n const inverse = (((360 - rotation) % 360) as CropRotation);\n return orientedRectToSource(rect, oriented, inverse);\n};\n\n/* ─── The image on the stage ──────────────────────────────────────────────── */\n\n/** Where the oriented image lands in VIEWPORT space under `t`. */\nexport const imageRect = (oriented: ICropSize, t: ICropTransform): ICropRect => ({\n x: t.x,\n y: t.y,\n width: oriented.width * t.scale,\n height: oriented.height * t.scale,\n});\n\n/** A VIEWPORT point to the ORIENTED image pixel under it. */\nexport const viewportToOriented = (p: ICropPoint, t: ICropTransform): ICropPoint => ({\n x: (p.x - t.x) / t.scale,\n y: (p.y - t.y) / t.scale,\n});\n\n/**\n * Change scale while keeping whatever image pixel is under `anchor` under\n * `anchor` — scroll-to-zoom at the cursor, pinch about the two fingers' midpoint.\n *\n * ⛔ WITHOUT THE ANCHOR TERM, ZOOM PULLS THE IMAGE TOWARDS THE ORIGIN. Scaling\n * `x` and `y` alone is a zoom about the stage's top-left corner; the subject\n * drifts out from under the finger, and every user reads that as the control\n * fighting them. The fixed-point condition is one line of algebra: the oriented\n * point `o = (anchor − t) / t.scale` must still satisfy\n * `anchor = o·next + t′`, hence `t′ = anchor − (anchor − t)·(next / t.scale)`.\n */\nexport const zoomAbout = (\n t: ICropTransform,\n anchor: ICropPoint,\n nextScale: number,\n): ICropTransform => {\n const k = nextScale / t.scale;\n return {\n scale: nextScale,\n x: anchor.x - (anchor.x - t.x) * k,\n y: anchor.y - (anchor.y - t.y) * k,\n };\n};\n\n/** The smallest scale at which the oriented image still covers `crop`. */\nexport const minScaleToCover = (oriented: ICropSize, crop: ICropSize): number =>\n coverScale(oriented, crop);\n\n/**\n * One axis of {@link clampTransformToCover}.\n *\n * ⚠️ AN IMAGE TOO SMALL TO COVER IS CENTRED, NOT PINNED. This is the case\n * {@link clamp} would resolve to one edge, and an edge is the wrong answer\n * here: it puts all of the uncovered gap on one side, which reads as the image\n * having slipped rather than as it being too small. Callers normally prevent\n * the case entirely by flooring scale at {@link minScaleToCover}; this is what\n * happens in the frame before they do.\n */\nconst coverAxis = (imgStart: number, imgLen: number, cropStart: number, cropLen: number): number =>\n imgLen < cropLen\n ? cropStart + (cropLen - imgLen) / 2\n : clamp(imgStart, cropStart + cropLen - imgLen, cropStart);\n\n/**\n * Slide the image — scale untouched — until it fully covers `crop`.\n *\n * This is the invariant that keeps empty corners out of a crop: whatever the\n * drag did, the window stays filled.\n */\nexport const clampTransformToCover = (\n t: ICropTransform,\n oriented: ICropSize,\n crop: ICropRect,\n): ICropTransform => {\n const img = imageRect(oriented, t);\n return {\n scale: t.scale,\n x: coverAxis(img.x, img.width, crop.x, crop.width),\n y: coverAxis(img.y, img.height, crop.y, crop.height),\n };\n};\n\n/* ─── The eight handles ───────────────────────────────────────────────────── */\n\n/** The corner or edge-midpoint a given handle pivots around. */\nconst anchorFor = (rect: ICropRect, handle: CropHandle): ICropPoint => {\n const c = rectCenter(rect);\n switch (handle) {\n case 'se': return {x: rect.x, y: rect.y};\n case 'sw': return {x: rectRight(rect), y: rect.y};\n case 'ne': return {x: rect.x, y: rectBottom(rect)};\n case 'nw': return {x: rectRight(rect), y: rectBottom(rect)};\n case 'e': return {x: rect.x, y: c.y};\n case 'w': return {x: rectRight(rect), y: c.y};\n case 's': return {x: c.x, y: rect.y};\n default: return {x: c.x, y: rectBottom(rect)}; // 'n'\n }\n};\n\nconst growsLeft = (h: CropHandle): boolean => h === 'w' || h === 'nw' || h === 'sw';\nconst growsUp = (h: CropHandle): boolean => h === 'n' || h === 'ne' || h === 'nw';\nconst movesX = (h: CropHandle): boolean => h !== 'n' && h !== 's';\nconst movesY = (h: CropHandle): boolean => h !== 'e' && h !== 'w';\n\n/**\n * Resize `rect` by dragging `handle` to `pointer`.\n *\n * All eight handles, one rule: **the anchor never moves.** For a corner that is\n * the opposite corner; for an edge it is the opposite edge, with the rect\n * growing symmetrically about that edge's midpoint on the free axis. Everything\n * else — ratio, floor, bounds — is applied to the size, then the rect is\n * rebuilt from the anchor.\n *\n * ⛔ THE RECT DOES NOT FLIP THROUGH ITS ANCHOR. Drag the south-east grip up past\n * the top-left corner and the width goes negative; taking `Math.abs` of it\n * would turn the crop inside out and leave the handle under the cursor on the\n * wrong side, which is how a drag ends up selecting a region the user never saw\n * highlighted. The negative is kept, and `min` floors it — so the crop stops at\n * its smallest and waits, which is what every other cropper does.\n *\n * ⚠️ `min` BEATS `bounds` WHEN THEY CONFLICT. A stage smaller than the minimum\n * crop cannot satisfy both; returning the minimum keeps the crop usable and\n * visibly overflowing, where honouring bounds would hand back a sliver the user\n * cannot grab. Callers that can resize the stage should; this is the fallback.\n */\nexport const resizeRect = (\n rect: ICropRect,\n handle: CropHandle,\n pointer: ICropPoint,\n options: ICropResizeOptions = {},\n): ICropRect => {\n const {ratio, min, bounds} = options;\n const anchor = anchorFor(rect, handle);\n\n /* 1 — what the pointer is asking for, signed, on each axis the handle owns. */\n let width = movesX(handle)\n ? (growsLeft(handle) ? anchor.x - pointer.x : pointer.x - anchor.x)\n : rect.width;\n let height = movesY(handle)\n ? (growsUp(handle) ? anchor.y - pointer.y : pointer.y - anchor.y)\n : rect.height;\n\n /* 2 — the ratio decides the axis the handle does not own. On a corner both\n axes are owned, so the larger request wins and the other follows. */\n if (ratio) {\n if (movesX(handle) && movesY(handle)) {\n width = Math.max(width, height * ratio);\n height = width / ratio;\n } else if (movesX(handle)) {\n height = width / ratio;\n } else {\n width = height * ratio;\n }\n }\n\n /* 3 — bounds, as a shrink about the anchor so the ratio survives it.\n\n ⛔ ONLY WHERE THE REQUEST IS POSITIVE, AND THAT GUARD IS NOT DEFENSIVE\n NOISE. Dragging a grip PAST its anchor makes the request negative — step\n 1 keeps the sign on purpose, see the non-flipping note. Shrinking means\n multiplying by `maxBound / requested`, and when `requested` is negative\n that quotient is negative: the rect does not shrink, it FLIPS AND\n INFLATES. Measured before the guard: the south grip of a 600×600 window\n dragged up to y = -500 on an 800×600 stage returned an 800×800 rect,\n larger than the stage it was being clamped into.\n\n ⚠️ PER AXIS IN THE FREE CASE, because a negative request on one axis\n does not excuse the other. `ne` dragged far down-right asks for a\n positive width and a negative height at once; a single combined guard\n let that width through unclamped and the rect reached x + width = 5000\n inside a 600-wide bounds. With a ratio the two axes share a sign, so one\n test covers both.\n\n ⚠️ THE CEILINGS ARE FLOORED AT ZERO for the same family of reason: an\n anchor already outside `bounds` makes one negative, and the sign error\n returns by a different door. */\n if (bounds) {\n const c = rectCenter(rect);\n const maxWidth = Math.max(0, movesX(handle)\n ? (growsLeft(handle) ? anchor.x - bounds.x : rectRight(bounds) - anchor.x)\n : 2 * Math.min(c.x - bounds.x, rectRight(bounds) - c.x));\n const maxHeight = Math.max(0, movesY(handle)\n ? (growsUp(handle) ? anchor.y - bounds.y : rectBottom(bounds) - anchor.y)\n : 2 * Math.min(c.y - bounds.y, rectBottom(bounds) - c.y));\n\n if (ratio) {\n if (width > 0 && height > 0) {\n const shrink = Math.min(1, maxWidth / width, maxHeight / height);\n if (shrink < 1 && Number.isFinite(shrink)) {\n width *= shrink;\n height *= shrink;\n }\n }\n } else {\n if (width > 0) width = Math.min(width, maxWidth);\n if (height > 0) height = Math.min(height, maxHeight);\n }\n }\n\n /* 4 — the floor, last, so it is the one thing that always holds. */\n if (min) {\n const minWidth = ratio ? Math.max(min.width, min.height * ratio) : min.width;\n const minHeight = ratio ? minWidth / ratio : min.height;\n if (width < minWidth) {\n width = minWidth;\n if (ratio) height = minHeight;\n }\n if (height < minHeight) {\n height = minHeight;\n if (ratio) width = minWidth;\n }\n }\n\n /* 5 — rebuild from the anchor, which has not moved. */\n const x = movesX(handle)\n ? (growsLeft(handle) ? anchor.x - width : anchor.x)\n : anchor.x - width / 2;\n const y = movesY(handle)\n ? (growsUp(handle) ? anchor.y - height : anchor.y)\n : anchor.y - height / 2;\n\n return {x, y, width, height};\n};\n\n/* ─── Start state, and the answer ─────────────────────────────────────────── */\n\n/**\n * The state a cropper opens in: the biggest `ratio`-shaped window the stage\n * allows, with the image scaled just enough to fill it and centred behind it.\n */\nexport const initialState = (\n source: ICropSize,\n viewport: ICropSize,\n ratio: number,\n rotation: CropRotation = 0,\n): {crop: ICropRect; transform: ICropTransform} => {\n const crop = centerRect(largestRectOfRatio(viewport, ratio), viewport);\n const oriented = orientedSize(source, rotation);\n const scale = minScaleToCover(oriented, crop);\n const img = imageRect(oriented, {scale, x: 0, y: 0});\n return {\n crop,\n transform: {\n scale,\n x: crop.x + (crop.width - img.width) / 2,\n y: crop.y + (crop.height - img.height) / 2,\n },\n };\n};\n\n/**\n * The crop window, in SOURCE pixels — the one number the encoders need.\n *\n * ⚠️ CLAMPED TO THE SOURCE AT THE END, because floating point does not care\n * about your invariants. A cover-clamped transform is exact in theory and a\n * fraction of a pixel outside in practice, and a decoder handed `x = -0.0001`\n * either throws or silently shifts the whole crop.\n */\nexport const cropToSourceRect = (\n crop: ICropRect,\n t: ICropTransform,\n source: ICropSize,\n rotation: CropRotation = 0,\n): ICropRect => {\n const topLeft = viewportToOriented({x: crop.x, y: crop.y}, t);\n const bottomRight = viewportToOriented({x: rectRight(crop), y: rectBottom(crop)}, t);\n const oriented: ICropRect = {\n x: topLeft.x,\n y: topLeft.y,\n width: bottomRight.x - topLeft.x,\n height: bottomRight.y - topLeft.y,\n };\n const inSource = orientedRectToSource(oriented, source, rotation);\n const x = clamp(inSource.x, 0, source.width);\n const y = clamp(inSource.y, 0, source.height);\n return {\n x,\n y,\n width: clamp(inSource.width, 0, source.width - x),\n height: clamp(inSource.height, 0, source.height - y),\n };\n};\n\n/**\n * Scale `size` DOWN to fit inside `max`, preserving its ratio.\n *\n * ⚠️ NEVER SCALES UP. A 200px avatar asked for at most 1024px stays 200px —\n * upscaling invents detail and costs bytes for it. An absent or non-positive\n * bound is ignored rather than treated as zero.\n */\nexport const fitWithin = (size: ICropSize, max: Partial<ICropSize>): ICropSize => {\n const limits = [\n max.width && max.width > 0 ? max.width / size.width : 1,\n max.height && max.height > 0 ? max.height / size.height : 1,\n ];\n const scale = Math.min(1, ...limits);\n return {width: size.width * scale, height: size.height * scale};\n};\n\n/** The next 90° step clockwise. */\nexport const rotateCw = (rotation: CropRotation): CropRotation =>\n (((rotation + 90) % 360) as CropRotation);\n"],"names":[],"mappings":"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"}
|