@octane-xplat/image-crop 0.0.1 → 0.10.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.
- package/LICENSE +21 -0
- package/README.md +121 -3
- package/dist/native/Gesture.js +12 -0
- package/dist/native/Guide.js +24 -0
- package/dist/native/ImageCrop.js +295 -0
- package/dist/native/Source.js +83 -0
- package/dist/native/geometry.js +102 -0
- package/dist/native/index.js +3 -0
- package/dist/native/octane-client-build.json +8 -0
- package/dist/web/Gesture.web.js +17 -0
- package/dist/web/Guide.web.js +15 -0
- package/dist/web/ImageCrop.js +199 -0
- package/dist/web/Source.web.js +50 -0
- package/dist/web/geometry.js +102 -0
- package/dist/web/index.js +3 -0
- package/dist/web/octane-client-build.json +8 -0
- package/package.json +81 -3
- package/src/Gesture.macos.tsrx +6 -0
- package/src/Gesture.tsrx +6 -0
- package/src/Gesture.web.tsrx +5 -0
- package/src/Guide.macos.tsrx +6 -0
- package/src/Guide.tsrx +6 -0
- package/src/Guide.web.tsrx +5 -0
- package/src/ImageCrop.tsrx +70 -0
- package/src/Source.macos.tsrx +6 -0
- package/src/Source.tsrx +8 -0
- package/src/Source.web.tsrx +5 -0
- package/src/geometry.ts +163 -0
- package/src/index.macos.ts +12 -0
- package/src/index.ts +12 -0
- package/src/index.web.ts +12 -0
- package/src/props.ts +57 -0
- package/types/generated/.tsrx-typegen-manifest.json +20 -0
- package/types/generated/Gesture.d.ts +2 -0
- package/types/generated/Gesture.macos.d.ts +2 -0
- package/types/generated/Gesture.web.d.ts +2 -0
- package/types/generated/Guide.d.ts +2 -0
- package/types/generated/Guide.macos.d.ts +2 -0
- package/types/generated/Guide.web.d.ts +2 -0
- package/types/generated/ImageCrop.d.ts +3 -0
- package/types/generated/Source.d.ts +2 -0
- package/types/generated/Source.macos.d.ts +2 -0
- package/types/generated/Source.web.d.ts +2 -0
- package/types/generated/geometry.d.ts +12 -0
- package/types/generated/index.d.ts +3 -0
- package/types/generated/index.macos.d.ts +3 -0
- package/types/generated/index.web.d.ts +3 -0
- 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
|
-
#
|
|
1
|
+
# Inline image crop selection
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
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,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 };
|