@octane-xplat/image-crop 0.0.1 → 0.11.0

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.
Files changed (48) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +121 -3
  3. package/dist/native/Gesture.js +12 -0
  4. package/dist/native/Guide.js +24 -0
  5. package/dist/native/ImageCrop.js +295 -0
  6. package/dist/native/Source.js +83 -0
  7. package/dist/native/geometry.js +102 -0
  8. package/dist/native/index.js +3 -0
  9. package/dist/native/octane-client-build.json +8 -0
  10. package/dist/web/Gesture.web.js +17 -0
  11. package/dist/web/Guide.web.js +15 -0
  12. package/dist/web/ImageCrop.js +199 -0
  13. package/dist/web/Source.web.js +50 -0
  14. package/dist/web/geometry.js +102 -0
  15. package/dist/web/index.js +3 -0
  16. package/dist/web/octane-client-build.json +8 -0
  17. package/package.json +81 -3
  18. package/src/Gesture.macos.tsrx +6 -0
  19. package/src/Gesture.tsrx +6 -0
  20. package/src/Gesture.web.tsrx +5 -0
  21. package/src/Guide.macos.tsrx +6 -0
  22. package/src/Guide.tsrx +6 -0
  23. package/src/Guide.web.tsrx +5 -0
  24. package/src/ImageCrop.tsrx +70 -0
  25. package/src/Source.macos.tsrx +6 -0
  26. package/src/Source.tsrx +8 -0
  27. package/src/Source.web.tsrx +5 -0
  28. package/src/geometry.ts +163 -0
  29. package/src/index.macos.ts +12 -0
  30. package/src/index.ts +12 -0
  31. package/src/index.web.ts +12 -0
  32. package/src/props.ts +57 -0
  33. package/types/generated/.tsrx-typegen-manifest.json +20 -0
  34. package/types/generated/Gesture.d.ts +2 -0
  35. package/types/generated/Gesture.macos.d.ts +2 -0
  36. package/types/generated/Gesture.web.d.ts +2 -0
  37. package/types/generated/Guide.d.ts +2 -0
  38. package/types/generated/Guide.macos.d.ts +2 -0
  39. package/types/generated/Guide.web.d.ts +2 -0
  40. package/types/generated/ImageCrop.d.ts +3 -0
  41. package/types/generated/Source.d.ts +2 -0
  42. package/types/generated/Source.macos.d.ts +2 -0
  43. package/types/generated/Source.web.d.ts +2 -0
  44. package/types/generated/geometry.d.ts +12 -0
  45. package/types/generated/index.d.ts +3 -0
  46. package/types/generated/index.macos.d.ts +3 -0
  47. package/types/generated/index.web.d.ts +3 -0
  48. package/types/generated/props.d.ts +51 -0
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2026 Alec Larson
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in
13
+ all copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
+ THE SOFTWARE.
package/README.md CHANGED
@@ -1,4 +1,122 @@
1
- # @octane-xplat/image-crop
1
+ # Inline image crop selection
2
2
 
3
- This is a placeholder stub published to reserve the package name ahead of the
4
- first automated release. It has no supported API.
3
+ `@octane-xplat/image-crop` selects a rectangle inline on web, iOS, Android, and macOS (AppKit).
4
+ It uses Octane Xplat primitives and shared geometry. It returns selection
5
+ coordinates; it does not produce a cropped image file or open a modal.
6
+
7
+ ## Install and use
8
+
9
+ ```sh
10
+ pnpm add @octane-xplat/image-crop
11
+ ```
12
+
13
+ Use the existing Octane Xplat renderer setup; no extra NativeScript plugin or
14
+ CSS pipeline is required. Supply the source's decoded, oriented natural size
15
+ and a viewport in CSS pixels (web), DIPs (iOS/Android), or points (AppKit).
16
+
17
+ For AppKit, use the existing `xplatMacOS` compiler setup; the `macos` package
18
+ condition supplies source for that renderer. macOS image sources support base64
19
+ data URIs and local paths/file URLs. Remote URLs are not loaded by the AppKit
20
+ renderer; download them separately and supply a local source. Desktop WebView
21
+ apps use the web implementation.
22
+
23
+ ```tsx
24
+ import { useState } from 'octane'
25
+ import { ImageCrop, toNaturalCrop } from '@octane-xplat/image-crop'
26
+ import type { Crop } from '@octane-xplat/image-crop'
27
+
28
+ export function PhotoSelection() {
29
+ const [crop, setCrop] = useState<Crop>({
30
+ unit: '%',
31
+ x: 25,
32
+ y: 25,
33
+ width: 50,
34
+ height: 50,
35
+ })
36
+ return (
37
+ <ImageCrop
38
+ src="https://example.com/photo.jpg"
39
+ alt="Photo to crop"
40
+ imageWidth={1600}
41
+ imageHeight={1200}
42
+ width={400}
43
+ height={300}
44
+ crop={crop}
45
+ aspect={4 / 3}
46
+ minWidth={40}
47
+ ruleOfThirds
48
+ onChange={(_, percent) => setCrop(percent)}
49
+ onComplete={(pixel) => {
50
+ const natural = toNaturalCrop(
51
+ pixel,
52
+ { width: 400, height: 300 },
53
+ { width: 1600, height: 1200 },
54
+ )
55
+ console.log(natural)
56
+ }}
57
+ />
58
+ )
59
+ }
60
+ ```
61
+
62
+ Replace the example URL and dimensions with your source. Keep hook-calling
63
+ components in `.tsx` or `.tsrx` inside your renderer include glob.
64
+ [The maintained demo](https://github.com/octane-xplat/octane-xplat/blob/main/packages/image-crop/examples/selection.tsrx) uses a bundled image.
65
+
66
+ ## Coordinates and control
67
+
68
+ The contract follows react-image-crop's crop shape and callback pair:
69
+ `onChange(pixelCrop, percentCrop)` and `onComplete(pixelCrop, percentCrop)`.
70
+ Store either callback argument and pass it back as `crop`. The widget does not
71
+ hold a separate visible selection when the controlled prop stays unchanged.
72
+
73
+ `px` means displayed-image pixels / DIPs, not natural source pixels. `%` means
74
+ percent of the contained image's width for x/width and height for y/height.
75
+ Letterbox margins are excluded. `containFrame(viewSize, naturalSize)` gives the
76
+ image rectangle; pass its width and height to `toNaturalCrop`. Use
77
+ `fromNaturalCrop` for the reverse mapping. A 1600×1200 source displayed at
78
+ 400×300 maps `{x:100,y:75,width:200,height:150,unit:'px'}` to natural pixels
79
+ `{x:400,y:300,width:800,height:600,unit:'px'}`.
80
+
81
+ Drag the interior to move and any of eight handles to resize. `aspect` is
82
+ width/height. Minimum and optional maximum dimensions always use displayed
83
+ pixels, including for a percent crop. Image bounds and maximums take precedence
84
+ when a minimum cannot fit. Invalid controlled coordinates are constrained for
85
+ rendering without emitting a callback. Side handles with an aspect expand the
86
+ other axis around its midpoint; corners keep the opposite corner fixed.
87
+
88
+ `disabled` blocks gestures. `ruleOfThirds` draws non-interactive guide lines.
89
+ Completion fires on gesture end, not cancellation. Cancellation keeps the last
90
+ controlled selection. A layout change during a gesture aborts that gesture.
91
+
92
+ ## Boundaries and verification
93
+
94
+ V1 assumes centered contain-fit and requires known natural dimensions; it does
95
+ not decode orientation or discover source dimensions. It does not implement
96
+ keyboard nudging, drawing a new rectangle outside the selection, circular
97
+ crops, or the full react-image-crop API. Small selections can have overlapping
98
+ 24-pixel handle hit regions; choose minimum dimensions suitable for your UI.
99
+
100
+ Producing pixels is a separate concern: use canvas on web or ImageSource
101
+ operations on native, with the natural coordinates from `toNaturalCrop`.
102
+ Neither output path is implemented by this leaf.
103
+
104
+ From the monorepo root:
105
+
106
+ ```sh
107
+ pnpm --filter @octane-xplat/image-crop test
108
+ pnpm --filter @octane-xplat/image-crop typecheck
109
+ pnpm --filter @octane-xplat/image-crop build
110
+ pnpm probe doctor
111
+ pnpm probe run packages/image-crop/examples/selection.tsrx --target web --deps @octane-xplat/image-crop
112
+ pnpm probe run packages/image-crop/tests/selection.macos.tsrx --target macos --deps @octane-xplat/image-crop
113
+ ```
114
+
115
+ The mounted probe checks handles and records geometry. It does not establish
116
+ OS touch hit-testing or visual pixel parity. Geometry tests cover conversion,
117
+ letterboxing, movement, aspect resizing, and constraints.
118
+
119
+ The maintained AppKit integration test checks all eight resize handles, interior
120
+ movement, aspect and min/max bounds, cancellation, disabled callbacks, source
121
+ scaling, input-transparent guides, and window resizing. It uses renderer pan
122
+ dispatch and AppKit hit-testing; OS mouse input remains separately unverified.
@@ -0,0 +1,12 @@
1
+ import { defineUniversalComponent, universalComponent, universalPlan, universalProps, universalValue } from "@nativescript-community/octane";
2
+ import { View } from "@octane-xplat/ui";
3
+ //#region src/Gesture.tsrx
4
+ var __octaneUniversalPlan0 = universalPlan("nativescript", {
5
+ "kind": "slot",
6
+ "slot": 0
7
+ });
8
+ var Gesture = defineUniversalComponent("nativescript", function Gesture(props) {
9
+ return universalValue(__octaneUniversalPlan0, [universalComponent("nativescript", View, universalProps([["spread", props]]))]);
10
+ }, { "module": "@nativescript-community/octane" });
11
+ //#endregion
12
+ export { Gesture };
@@ -0,0 +1,24 @@
1
+ import { defineUniversalComponent, universalComponent, universalPlan, universalProps, universalValue } from "@nativescript-community/octane";
2
+ import { View } from "@octane-xplat/ui";
3
+ //#region src/Guide.tsrx
4
+ var __octaneUniversalPlan0 = universalPlan("nativescript", {
5
+ "kind": "slot",
6
+ "slot": 0
7
+ });
8
+ var Guide = defineUniversalComponent("nativescript", function Guide(props) {
9
+ return universalValue(__octaneUniversalPlan0, [universalComponent("nativescript", View, universalProps([
10
+ ["spread", props],
11
+ [
12
+ "set",
13
+ "ios",
14
+ { isUserInteractionEnabled: false }
15
+ ],
16
+ [
17
+ "set",
18
+ "android",
19
+ { isUserInteractionEnabled: false }
20
+ ]
21
+ ]))]);
22
+ }, { "module": "@nativescript-community/octane" });
23
+ //#endregion
24
+ export { Guide };
@@ -0,0 +1,295 @@
1
+ import { constrainCrop, containFrame, convertToPercentCrop, dragCrop } from "./geometry.js";
2
+ import { Gesture } from "./Gesture.js";
3
+ import { Guide } from "./Guide.js";
4
+ import { Source } from "./Source.js";
5
+ import { defineUniversalComponent, hookSlots, universalChildren, universalComponent, universalFor, universalIf, universalPlan, universalProps, universalValue, useRef, withSlot } from "@nativescript-community/octane";
6
+ import { Absolute, View, useMeasure } from "@octane-xplat/ui";
7
+ //#region src/ImageCrop.tsrx
8
+ var _hs$ = /* @__PURE__ */ hookSlots(2);
9
+ var _h$0 = /* @__PURE__ */ Symbol(_hs$);
10
+ var _h$1 = /* @__PURE__ */ Symbol(_hs$ + 1);
11
+ var __octaneUniversalPlan0 = universalPlan("nativescript", {
12
+ "kind": "range",
13
+ "children": [{
14
+ "kind": "slot",
15
+ "slot": 0
16
+ }, {
17
+ "kind": "slot",
18
+ "slot": 1
19
+ }]
20
+ });
21
+ var __octaneUniversalPlan1 = universalPlan("nativescript", {
22
+ "kind": "slot",
23
+ "slot": 0
24
+ });
25
+ var __octaneUniversalPlan2 = universalPlan("nativescript", {
26
+ "kind": "slot",
27
+ "slot": 0
28
+ });
29
+ var __octaneUniversalPlan3 = universalPlan("nativescript", {
30
+ "kind": "range",
31
+ "children": [
32
+ {
33
+ "kind": "slot",
34
+ "slot": 0
35
+ },
36
+ {
37
+ "kind": "slot",
38
+ "slot": 1
39
+ },
40
+ {
41
+ "kind": "slot",
42
+ "slot": 2
43
+ },
44
+ {
45
+ "kind": "slot",
46
+ "slot": 3
47
+ }
48
+ ]
49
+ });
50
+ var __octaneUniversalPlan4 = universalPlan("nativescript", {
51
+ "kind": "slot",
52
+ "slot": 0
53
+ });
54
+ var __octaneUniversalPlan5 = universalPlan("nativescript", {
55
+ "kind": "slot",
56
+ "slot": 0
57
+ });
58
+ var handles = [
59
+ "nw",
60
+ "n",
61
+ "ne",
62
+ "e",
63
+ "se",
64
+ "s",
65
+ "sw",
66
+ "w"
67
+ ];
68
+ var ImageCrop = defineUniversalComponent("nativescript", function ImageCrop(props) {
69
+ const measure = withSlot(_h$0, useMeasure);
70
+ const viewport = {
71
+ width: Math.min(props.width, measure.bounds?.width ?? props.width),
72
+ height: Math.min(props.height, measure.bounds?.height ?? props.height)
73
+ };
74
+ const frame = containFrame(viewport, {
75
+ width: props.imageWidth,
76
+ height: props.imageHeight
77
+ });
78
+ const crop = constrainCrop(props.crop, frame, props);
79
+ const gesture = useRef(null, _h$1);
80
+ const pan = (handle, event) => {
81
+ if (props.disabled || frame.width <= 0 || frame.height <= 0) {
82
+ gesture.current = null;
83
+ return;
84
+ }
85
+ if (event.state === "began") {
86
+ gesture.current = {
87
+ handle,
88
+ start: crop,
89
+ size: frame,
90
+ dx: event.dx,
91
+ dy: event.dy,
92
+ src: props.src
93
+ };
94
+ return;
95
+ }
96
+ const active = gesture.current;
97
+ if (!active || active.handle !== handle) return;
98
+ if (event.state === "cancelled") {
99
+ gesture.current = null;
100
+ return;
101
+ }
102
+ if (active.src !== props.src || active.size.width !== frame.width || active.size.height !== frame.height) {
103
+ gesture.current = null;
104
+ return;
105
+ }
106
+ const next = dragCrop(active.start, handle, event.dx - active.dx, event.dy - active.dy, frame, props);
107
+ const percent = convertToPercentCrop(next, frame);
108
+ props.onChange(next, percent);
109
+ if (event.state === "ended") {
110
+ gesture.current = null;
111
+ props.onComplete?.(next, percent);
112
+ }
113
+ };
114
+ const left = frame.x + crop.x, top = frame.y + crop.y;
115
+ return universalValue(__octaneUniversalPlan5, [universalComponent("nativescript", View, universalProps([
116
+ [
117
+ "set",
118
+ "id",
119
+ props.id
120
+ ],
121
+ [
122
+ "set",
123
+ "ref",
124
+ measure.ref
125
+ ],
126
+ [
127
+ "set",
128
+ "style",
129
+ {
130
+ width: props.width,
131
+ height: props.height
132
+ }
133
+ ]
134
+ ], universalChildren("nativescript", () => {
135
+ return universalValue(__octaneUniversalPlan4, [universalComponent("nativescript", Absolute, universalProps([[
136
+ "set",
137
+ "style",
138
+ viewport
139
+ ]], universalChildren("nativescript", () => {
140
+ return universalValue(__octaneUniversalPlan3, [
141
+ universalComponent("nativescript", Source, universalProps([
142
+ [
143
+ "set",
144
+ "src",
145
+ props.src
146
+ ],
147
+ [
148
+ "set",
149
+ "alt",
150
+ props.alt
151
+ ],
152
+ [
153
+ "set",
154
+ "width",
155
+ viewport.width
156
+ ],
157
+ [
158
+ "set",
159
+ "height",
160
+ viewport.height
161
+ ]
162
+ ])),
163
+ universalComponent("nativescript", Gesture, universalProps([
164
+ [
165
+ "set",
166
+ "id",
167
+ props.id ? props.id + "-move" : void 0
168
+ ],
169
+ [
170
+ "set",
171
+ "left",
172
+ left
173
+ ],
174
+ [
175
+ "set",
176
+ "top",
177
+ top
178
+ ],
179
+ [
180
+ "set",
181
+ "style",
182
+ {
183
+ width: crop.width,
184
+ height: crop.height,
185
+ borderWidth: 1,
186
+ borderColor: "#ffffff",
187
+ backgroundColor: "rgba(255,255,255,0.13)"
188
+ }
189
+ ],
190
+ [
191
+ "set",
192
+ "onPan",
193
+ props.disabled ? void 0 : (e) => pan("move", e)
194
+ ]
195
+ ])),
196
+ universalIf(props.ruleOfThirds, () => {
197
+ return universalValue(__octaneUniversalPlan1, [universalFor([1 / 3, 2 / 3], (fraction, __octaneUniversalIndex) => fraction, (fraction, __octaneUniversalIndex) => {
198
+ return universalValue(__octaneUniversalPlan0, [universalComponent("nativescript", Guide, universalProps([
199
+ [
200
+ "set",
201
+ "left",
202
+ left + crop.width * fraction
203
+ ],
204
+ [
205
+ "set",
206
+ "top",
207
+ top
208
+ ],
209
+ [
210
+ "set",
211
+ "style",
212
+ {
213
+ width: 1,
214
+ height: crop.height,
215
+ backgroundColor: "rgba(255,255,255,0.53)"
216
+ }
217
+ ]
218
+ ])), universalComponent("nativescript", Guide, universalProps([
219
+ [
220
+ "set",
221
+ "left",
222
+ left
223
+ ],
224
+ [
225
+ "set",
226
+ "top",
227
+ top + crop.height * fraction
228
+ ],
229
+ [
230
+ "set",
231
+ "style",
232
+ {
233
+ width: crop.width,
234
+ height: 1,
235
+ backgroundColor: "rgba(255,255,255,0.53)"
236
+ }
237
+ ]
238
+ ]))]);
239
+ })]);
240
+ }),
241
+ universalFor(handles, (handle, __octaneUniversalIndex$) => handle, (handle, __octaneUniversalIndex$) => {
242
+ const hx = handle.includes("w") ? 0 : handle.includes("e") ? crop.width : crop.width / 2;
243
+ const hy = handle.includes("n") ? 0 : handle.includes("s") ? crop.height : crop.height / 2;
244
+ const x = Math.max(frame.x, Math.min(left + hx - 12, frame.x + frame.width - 24));
245
+ const y = Math.max(frame.y, Math.min(top + hy - 12, frame.y + frame.height - 24));
246
+ return universalValue(__octaneUniversalPlan2, [universalComponent("nativescript", Gesture, universalProps([
247
+ [
248
+ "set",
249
+ "id",
250
+ props.id ? props.id + "-" + handle : void 0
251
+ ],
252
+ [
253
+ "set",
254
+ "left",
255
+ x
256
+ ],
257
+ [
258
+ "set",
259
+ "top",
260
+ y
261
+ ],
262
+ [
263
+ "set",
264
+ "accessibilityLabel",
265
+ "Resize crop " + handle
266
+ ],
267
+ [
268
+ "set",
269
+ "accessibilityState",
270
+ { disabled: props.disabled }
271
+ ],
272
+ [
273
+ "set",
274
+ "style",
275
+ {
276
+ width: Math.min(24, frame.width),
277
+ height: Math.min(24, frame.height),
278
+ backgroundColor: props.disabled ? "rgba(255,255,255,0.27)" : "rgba(255,255,255,0.53)",
279
+ borderWidth: 1,
280
+ borderColor: "#ffffff"
281
+ }
282
+ ],
283
+ [
284
+ "set",
285
+ "onPan",
286
+ props.disabled ? void 0 : (e) => pan(handle, e)
287
+ ]
288
+ ]))]);
289
+ })
290
+ ]);
291
+ })))]);
292
+ })))]);
293
+ }, { "module": "@nativescript-community/octane" });
294
+ //#endregion
295
+ export { ImageCrop };
@@ -0,0 +1,83 @@
1
+ import { defineUniversalComponent, markWarm, universalComponent, universalPlan, universalProps, universalValue, useBatch, warmChild } from "@nativescript-community/octane";
2
+ import { Image } from "@octane-xplat/ui";
3
+ //#region src/Source.tsrx
4
+ var __octaneUniversalPlan0 = universalPlan("nativescript", {
5
+ "kind": "slot",
6
+ "slot": 0
7
+ });
8
+ var Source = defineUniversalComponent("nativescript", markWarm(function Source(props) {
9
+ useBatch([], () => {
10
+ warmChild(Image, {
11
+ src: props.src,
12
+ alt: props.alt,
13
+ style: {
14
+ width: props.width,
15
+ height: props.height
16
+ },
17
+ ios: {
18
+ stretch: "aspectFit",
19
+ isUserInteractionEnabled: false
20
+ },
21
+ android: {
22
+ stretch: "aspectFit",
23
+ isUserInteractionEnabled: false
24
+ }
25
+ });
26
+ });
27
+ return universalValue(__octaneUniversalPlan0, [universalComponent("nativescript", Image, universalProps([
28
+ [
29
+ "set",
30
+ "src",
31
+ props.src
32
+ ],
33
+ [
34
+ "set",
35
+ "alt",
36
+ props.alt
37
+ ],
38
+ [
39
+ "set",
40
+ "style",
41
+ {
42
+ width: props.width,
43
+ height: props.height
44
+ }
45
+ ],
46
+ [
47
+ "set",
48
+ "ios",
49
+ {
50
+ stretch: "aspectFit",
51
+ isUserInteractionEnabled: false
52
+ }
53
+ ],
54
+ [
55
+ "set",
56
+ "android",
57
+ {
58
+ stretch: "aspectFit",
59
+ isUserInteractionEnabled: false
60
+ }
61
+ ]
62
+ ]))]);
63
+ }, (__wp) => {
64
+ const props = __wp;
65
+ warmChild(Image, {
66
+ src: props.src,
67
+ alt: props.alt,
68
+ style: {
69
+ width: props.width,
70
+ height: props.height
71
+ },
72
+ ios: {
73
+ stretch: "aspectFit",
74
+ isUserInteractionEnabled: false
75
+ },
76
+ android: {
77
+ stretch: "aspectFit",
78
+ isUserInteractionEnabled: false
79
+ }
80
+ });
81
+ }), { "module": "@nativescript-community/octane" });
82
+ //#endregion
83
+ export { Source };
@@ -0,0 +1,102 @@
1
+ //#region src/geometry.ts
2
+ var positive = (n) => Number.isFinite(n) && n > 0 ? n : 0;
3
+ var finite = (n) => Number.isFinite(n) ? n : 0;
4
+ var clamp = (n, low, high) => Math.min(Math.max(n, Math.min(low, high)), high);
5
+ /** Displayed image rectangle inside a centered contain-fit viewport. */
6
+ function containFrame(view, natural) {
7
+ const scale = natural.width > 0 && natural.height > 0 ? Math.min(positive(view.width) / natural.width, positive(view.height) / natural.height) : 0;
8
+ const width = positive(natural.width * scale), height = positive(natural.height * scale);
9
+ return {
10
+ x: (positive(view.width) - width) / 2,
11
+ y: (positive(view.height) - height) / 2,
12
+ width,
13
+ height
14
+ };
15
+ }
16
+ function convertToPixelCrop(crop, size) {
17
+ const sx = crop.unit === "%" ? positive(size.width) / 100 : 1;
18
+ const sy = crop.unit === "%" ? positive(size.height) / 100 : 1;
19
+ return {
20
+ unit: "px",
21
+ x: finite(crop.x) * sx,
22
+ y: finite(crop.y) * sy,
23
+ width: positive(crop.width) * sx,
24
+ height: positive(crop.height) * sy
25
+ };
26
+ }
27
+ function convertToPercentCrop(crop, size) {
28
+ const p = convertToPixelCrop(crop, size);
29
+ return {
30
+ unit: "%",
31
+ x: size.width > 0 ? p.x / size.width * 100 : 0,
32
+ y: size.height > 0 ? p.y / size.height * 100 : 0,
33
+ width: size.width > 0 ? p.width / size.width * 100 : 0,
34
+ height: size.height > 0 ? p.height / size.height * 100 : 0
35
+ };
36
+ }
37
+ /** Convert displayed-image coordinates to natural pixels for a separate pixel exporter. */
38
+ function toNaturalCrop(crop, displayed, natural) {
39
+ return convertToPixelCrop(convertToPercentCrop(crop, displayed), natural);
40
+ }
41
+ function fromNaturalCrop(crop, natural, displayed) {
42
+ return convertToPixelCrop(convertToPercentCrop(crop, natural), displayed);
43
+ }
44
+ /** Clamp external controlled input. Impossible minimums yield to bounds/maxima. */
45
+ function constrainCrop(crop, size, limits = {}) {
46
+ const p = convertToPixelCrop(crop, size);
47
+ const maxW = Math.min(positive(size.width), limits.maxWidth === void 0 ? Infinity : positive(limits.maxWidth));
48
+ const maxH = Math.min(positive(size.height), limits.maxHeight === void 0 ? Infinity : positive(limits.maxHeight));
49
+ let width = clamp(p.width, positive(limits.minWidth ?? 0), maxW);
50
+ let height = clamp(p.height, positive(limits.minHeight ?? 0), maxH);
51
+ const aspect = positive(limits.aspect ?? 0);
52
+ if (aspect) {
53
+ width = clamp(width, Math.max(positive(limits.minWidth ?? 0), positive(limits.minHeight ?? 0) * aspect), Math.min(maxW, maxH * aspect));
54
+ height = width / aspect;
55
+ }
56
+ return {
57
+ unit: "px",
58
+ x: clamp(p.x, 0, positive(size.width) - width),
59
+ y: clamp(p.y, 0, positive(size.height) - height),
60
+ width,
61
+ height
62
+ };
63
+ }
64
+ /** Resize from a frozen gesture origin using cumulative view-space deltas. */
65
+ function dragCrop(start, handle, dx, dy, size, limits = {}) {
66
+ const p = constrainCrop(start, size, limits);
67
+ dx = finite(dx);
68
+ dy = finite(dy);
69
+ if (handle === "move") return {
70
+ ...p,
71
+ x: clamp(p.x + dx, 0, size.width - p.width),
72
+ y: clamp(p.y + dy, 0, size.height - p.height)
73
+ };
74
+ const west = handle.includes("w"), east = handle.includes("e");
75
+ const north = handle.includes("n"), south = handle.includes("s");
76
+ const ax = west ? p.x + p.width : east ? p.x : p.x + p.width / 2;
77
+ const ay = north ? p.y + p.height : south ? p.y : p.y + p.height / 2;
78
+ const boundW = west ? ax : east ? size.width - ax : 2 * Math.min(ax, size.width - ax);
79
+ const boundH = north ? ay : south ? size.height - ay : 2 * Math.min(ay, size.height - ay);
80
+ const maxW = Math.min(boundW, limits.maxWidth ?? Infinity);
81
+ const maxH = Math.min(boundH, limits.maxHeight ?? Infinity);
82
+ let width = p.width + (west ? -dx : east ? dx : 0);
83
+ let height = p.height + (north ? -dy : south ? dy : 0);
84
+ const aspect = positive(limits.aspect ?? 0);
85
+ if (aspect) {
86
+ if (!west && !east || (north || south) && Math.abs(dy * aspect) > Math.abs(dx)) width = height * aspect;
87
+ width = clamp(width, Math.max(positive(limits.minWidth ?? 0), positive(limits.minHeight ?? 0) * aspect), Math.max(0, Math.min(maxW, maxH * aspect)));
88
+ height = width / aspect;
89
+ } else {
90
+ width = clamp(width, positive(limits.minWidth ?? 0), Math.max(0, maxW));
91
+ height = clamp(height, positive(limits.minHeight ?? 0), Math.max(0, maxH));
92
+ }
93
+ return {
94
+ unit: "px",
95
+ x: west ? ax - width : east ? ax : ax - width / 2,
96
+ y: north ? ay - height : south ? ay : ay - height / 2,
97
+ width,
98
+ height
99
+ };
100
+ }
101
+ //#endregion
102
+ export { constrainCrop, containFrame, convertToPercentCrop, convertToPixelCrop, dragCrop, fromNaturalCrop, toNaturalCrop };
@@ -0,0 +1,3 @@
1
+ import { constrainCrop, containFrame, convertToPercentCrop, convertToPixelCrop, dragCrop, fromNaturalCrop, toNaturalCrop } from "./geometry.js";
2
+ import { ImageCrop } from "./ImageCrop.js";
3
+ export { ImageCrop, constrainCrop, containFrame, convertToPercentCrop, convertToPixelCrop, dragCrop, fromNaturalCrop, toNaturalCrop };
@@ -0,0 +1,8 @@
1
+ {
2
+ "version": 1,
3
+ "buildId": "7ccceb2f-0c9f-43cc-beb6-b354d84d8552",
4
+ "mode": "production",
5
+ "capabilities": {
6
+ "independentHydration": false
7
+ }
8
+ }
@@ -0,0 +1,17 @@
1
+ import { componentSlot } from "octane";
2
+ import { View } from "@octane-xplat/ui";
3
+ //#region src/Gesture.web.tsrx
4
+ var Gesture = function Gesture(props, __s, __extra) {
5
+ const __block = __s.block;
6
+ componentSlot(__s, 0, __block.parentNode, View, {
7
+ ...props,
8
+ "style": {
9
+ ...props.style,
10
+ touchAction: "none",
11
+ userSelect: "none",
12
+ boxSizing: "border-box"
13
+ }
14
+ }, __block.endMarker, void 0, void 0, true, void 0, "c:883cc1f9");
15
+ };
16
+ //#endregion
17
+ export { Gesture };