@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/src/geometry.ts
ADDED
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
Crop,
|
|
3
|
+
CropConstraints,
|
|
4
|
+
CropHandle,
|
|
5
|
+
ImageFrame,
|
|
6
|
+
PercentCrop,
|
|
7
|
+
PixelCrop,
|
|
8
|
+
Size,
|
|
9
|
+
} from './props'
|
|
10
|
+
|
|
11
|
+
const positive = (n: number) => (Number.isFinite(n) && n > 0 ? n : 0)
|
|
12
|
+
const finite = (n: number) => (Number.isFinite(n) ? n : 0)
|
|
13
|
+
const clamp = (n: number, low: number, high: number) =>
|
|
14
|
+
Math.min(Math.max(n, Math.min(low, high)), high)
|
|
15
|
+
|
|
16
|
+
/** Displayed image rectangle inside a centered contain-fit viewport. */
|
|
17
|
+
export function containFrame(view: Size, natural: Size): ImageFrame {
|
|
18
|
+
const scale =
|
|
19
|
+
natural.width > 0 && natural.height > 0
|
|
20
|
+
? Math.min(positive(view.width) / natural.width, positive(view.height) / natural.height)
|
|
21
|
+
: 0
|
|
22
|
+
|
|
23
|
+
const width = positive(natural.width * scale),
|
|
24
|
+
height = positive(natural.height * scale)
|
|
25
|
+
|
|
26
|
+
return {
|
|
27
|
+
x: (positive(view.width) - width) / 2,
|
|
28
|
+
y: (positive(view.height) - height) / 2,
|
|
29
|
+
width,
|
|
30
|
+
height,
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export function convertToPixelCrop(crop: Crop, size: Size): PixelCrop {
|
|
35
|
+
const sx = crop.unit === '%' ? positive(size.width) / 100 : 1
|
|
36
|
+
const sy = crop.unit === '%' ? positive(size.height) / 100 : 1
|
|
37
|
+
return {
|
|
38
|
+
unit: 'px',
|
|
39
|
+
x: finite(crop.x) * sx,
|
|
40
|
+
y: finite(crop.y) * sy,
|
|
41
|
+
width: positive(crop.width) * sx,
|
|
42
|
+
height: positive(crop.height) * sy,
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export function convertToPercentCrop(crop: Crop, size: Size): PercentCrop {
|
|
47
|
+
const p = convertToPixelCrop(crop, size)
|
|
48
|
+
return {
|
|
49
|
+
unit: '%',
|
|
50
|
+
x: size.width > 0 ? (p.x / size.width) * 100 : 0,
|
|
51
|
+
y: size.height > 0 ? (p.y / size.height) * 100 : 0,
|
|
52
|
+
width: size.width > 0 ? (p.width / size.width) * 100 : 0,
|
|
53
|
+
height: size.height > 0 ? (p.height / size.height) * 100 : 0,
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/** Convert displayed-image coordinates to natural pixels for a separate pixel exporter. */
|
|
58
|
+
export function toNaturalCrop(crop: Crop, displayed: Size, natural: Size): PixelCrop {
|
|
59
|
+
const p = convertToPercentCrop(crop, displayed)
|
|
60
|
+
return convertToPixelCrop(p, natural)
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export function fromNaturalCrop(crop: PixelCrop, natural: Size, displayed: Size): PixelCrop {
|
|
64
|
+
return convertToPixelCrop(convertToPercentCrop(crop, natural), displayed)
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/** Clamp external controlled input. Impossible minimums yield to bounds/maxima. */
|
|
68
|
+
export function constrainCrop(crop: Crop, size: Size, limits: CropConstraints = {}): PixelCrop {
|
|
69
|
+
const p = convertToPixelCrop(crop, size)
|
|
70
|
+
const maxW = Math.min(
|
|
71
|
+
positive(size.width),
|
|
72
|
+
limits.maxWidth === undefined ? Infinity : positive(limits.maxWidth),
|
|
73
|
+
)
|
|
74
|
+
|
|
75
|
+
const maxH = Math.min(
|
|
76
|
+
positive(size.height),
|
|
77
|
+
limits.maxHeight === undefined ? Infinity : positive(limits.maxHeight),
|
|
78
|
+
)
|
|
79
|
+
|
|
80
|
+
let width = clamp(p.width, positive(limits.minWidth ?? 0), maxW)
|
|
81
|
+
let height = clamp(p.height, positive(limits.minHeight ?? 0), maxH)
|
|
82
|
+
const aspect = positive(limits.aspect ?? 0)
|
|
83
|
+
if (aspect) {
|
|
84
|
+
width = clamp(
|
|
85
|
+
width,
|
|
86
|
+
Math.max(positive(limits.minWidth ?? 0), positive(limits.minHeight ?? 0) * aspect),
|
|
87
|
+
Math.min(maxW, maxH * aspect),
|
|
88
|
+
)
|
|
89
|
+
|
|
90
|
+
height = width / aspect
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
return {
|
|
94
|
+
unit: 'px',
|
|
95
|
+
x: clamp(p.x, 0, positive(size.width) - width),
|
|
96
|
+
y: clamp(p.y, 0, positive(size.height) - height),
|
|
97
|
+
width,
|
|
98
|
+
height,
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/** Resize from a frozen gesture origin using cumulative view-space deltas. */
|
|
103
|
+
export function dragCrop(
|
|
104
|
+
start: Crop,
|
|
105
|
+
handle: CropHandle,
|
|
106
|
+
dx: number,
|
|
107
|
+
dy: number,
|
|
108
|
+
size: Size,
|
|
109
|
+
limits: CropConstraints = {},
|
|
110
|
+
): PixelCrop {
|
|
111
|
+
const p = constrainCrop(start, size, limits)
|
|
112
|
+
dx = finite(dx)
|
|
113
|
+
dy = finite(dy)
|
|
114
|
+
if (handle === 'move') {
|
|
115
|
+
return {
|
|
116
|
+
...p,
|
|
117
|
+
x: clamp(p.x + dx, 0, size.width - p.width),
|
|
118
|
+
y: clamp(p.y + dy, 0, size.height - p.height),
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
const west = handle.includes('w'),
|
|
123
|
+
east = handle.includes('e')
|
|
124
|
+
|
|
125
|
+
const north = handle.includes('n'),
|
|
126
|
+
south = handle.includes('s')
|
|
127
|
+
|
|
128
|
+
// Corners anchor their opposite corner; side handles anchor the opposite
|
|
129
|
+
// edge midpoint under aspect lock, so the other axis expands symmetrically.
|
|
130
|
+
const ax = west ? p.x + p.width : east ? p.x : p.x + p.width / 2
|
|
131
|
+
const ay = north ? p.y + p.height : south ? p.y : p.y + p.height / 2
|
|
132
|
+
const boundW = west ? ax : east ? size.width - ax : 2 * Math.min(ax, size.width - ax)
|
|
133
|
+
const boundH = north ? ay : south ? size.height - ay : 2 * Math.min(ay, size.height - ay)
|
|
134
|
+
const maxW = Math.min(boundW, limits.maxWidth ?? Infinity)
|
|
135
|
+
const maxH = Math.min(boundH, limits.maxHeight ?? Infinity)
|
|
136
|
+
let width = p.width + (west ? -dx : east ? dx : 0)
|
|
137
|
+
let height = p.height + (north ? -dy : south ? dy : 0)
|
|
138
|
+
const aspect = positive(limits.aspect ?? 0)
|
|
139
|
+
if (aspect) {
|
|
140
|
+
if ((!west && !east) || ((north || south) && Math.abs(dy * aspect) > Math.abs(dx))) {
|
|
141
|
+
width = height * aspect
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
width = clamp(
|
|
145
|
+
width,
|
|
146
|
+
Math.max(positive(limits.minWidth ?? 0), positive(limits.minHeight ?? 0) * aspect),
|
|
147
|
+
Math.max(0, Math.min(maxW, maxH * aspect)),
|
|
148
|
+
)
|
|
149
|
+
|
|
150
|
+
height = width / aspect
|
|
151
|
+
} else {
|
|
152
|
+
width = clamp(width, positive(limits.minWidth ?? 0), Math.max(0, maxW))
|
|
153
|
+
height = clamp(height, positive(limits.minHeight ?? 0), Math.max(0, maxH))
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
return {
|
|
157
|
+
unit: 'px',
|
|
158
|
+
x: west ? ax - width : east ? ax : ax - width / 2,
|
|
159
|
+
y: north ? ay - height : south ? ay : ay - height / 2,
|
|
160
|
+
width,
|
|
161
|
+
height,
|
|
162
|
+
}
|
|
163
|
+
}
|
package/src/index.ts
ADDED
package/src/index.web.ts
ADDED
package/src/props.ts
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
export interface Crop {
|
|
2
|
+
x: number
|
|
3
|
+
y: number
|
|
4
|
+
width: number
|
|
5
|
+
height: number
|
|
6
|
+
unit: 'px' | '%'
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export interface PixelCrop extends Crop {
|
|
10
|
+
unit: 'px'
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export interface PercentCrop extends Crop {
|
|
14
|
+
unit: '%'
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export interface Size {
|
|
18
|
+
width: number
|
|
19
|
+
height: number
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export interface ImageFrame extends Size {
|
|
23
|
+
x: number
|
|
24
|
+
y: number
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export type CropHandle = 'move' | 'n' | 'ne' | 'e' | 'se' | 's' | 'sw' | 'w' | 'nw'
|
|
28
|
+
export interface CropConstraints {
|
|
29
|
+
/** Fixed width / height ratio in displayed coordinates. */
|
|
30
|
+
aspect?: number
|
|
31
|
+
/** Minimum displayed width in pixels / DIPs, including for percentage crops. */
|
|
32
|
+
minWidth?: number
|
|
33
|
+
/** Minimum displayed height in pixels / DIPs, including for percentage crops. */
|
|
34
|
+
minHeight?: number
|
|
35
|
+
maxWidth?: number
|
|
36
|
+
maxHeight?: number
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export interface ImageCropProps extends CropConstraints {
|
|
40
|
+
src: string
|
|
41
|
+
alt?: string
|
|
42
|
+
/** Natural image dimensions; must match the decoded, oriented source. */
|
|
43
|
+
imageWidth: number
|
|
44
|
+
imageHeight: number
|
|
45
|
+
/** Controlled crop relative to the contained image, excluding letterboxing. */
|
|
46
|
+
crop: Crop
|
|
47
|
+
/** Update crop with either argument; both describe the same selection. */
|
|
48
|
+
onChange: (pixelCrop: PixelCrop, percentCrop: PercentCrop) => void
|
|
49
|
+
/** Called once on gesture end; cancellation does not complete. */
|
|
50
|
+
onComplete?: (pixelCrop: PixelCrop, percentCrop: PercentCrop) => void
|
|
51
|
+
disabled?: boolean
|
|
52
|
+
ruleOfThirds?: boolean
|
|
53
|
+
/** Frame dimensions in CSS pixels / native DIPs. */
|
|
54
|
+
width: number
|
|
55
|
+
height: number
|
|
56
|
+
id?: string
|
|
57
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 1,
|
|
3
|
+
"files": [
|
|
4
|
+
"Gesture.d.ts",
|
|
5
|
+
"Gesture.macos.d.ts",
|
|
6
|
+
"Gesture.web.d.ts",
|
|
7
|
+
"Guide.d.ts",
|
|
8
|
+
"Guide.macos.d.ts",
|
|
9
|
+
"Guide.web.d.ts",
|
|
10
|
+
"ImageCrop.d.ts",
|
|
11
|
+
"Source.d.ts",
|
|
12
|
+
"Source.macos.d.ts",
|
|
13
|
+
"Source.web.d.ts",
|
|
14
|
+
"geometry.d.ts",
|
|
15
|
+
"index.d.ts",
|
|
16
|
+
"index.macos.d.ts",
|
|
17
|
+
"index.web.d.ts",
|
|
18
|
+
"props.d.ts"
|
|
19
|
+
]
|
|
20
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { Crop, CropConstraints, CropHandle, ImageFrame, PercentCrop, PixelCrop, Size } from './props.js';
|
|
2
|
+
/** Displayed image rectangle inside a centered contain-fit viewport. */
|
|
3
|
+
export declare function containFrame(view: Size, natural: Size): ImageFrame;
|
|
4
|
+
export declare function convertToPixelCrop(crop: Crop, size: Size): PixelCrop;
|
|
5
|
+
export declare function convertToPercentCrop(crop: Crop, size: Size): PercentCrop;
|
|
6
|
+
/** Convert displayed-image coordinates to natural pixels for a separate pixel exporter. */
|
|
7
|
+
export declare function toNaturalCrop(crop: Crop, displayed: Size, natural: Size): PixelCrop;
|
|
8
|
+
export declare function fromNaturalCrop(crop: PixelCrop, natural: Size, displayed: Size): PixelCrop;
|
|
9
|
+
/** Clamp external controlled input. Impossible minimums yield to bounds/maxima. */
|
|
10
|
+
export declare function constrainCrop(crop: Crop, size: Size, limits?: CropConstraints): PixelCrop;
|
|
11
|
+
/** Resize from a frozen gesture origin using cumulative view-space deltas. */
|
|
12
|
+
export declare function dragCrop(start: Crop, handle: CropHandle, dx: number, dy: number, size: Size, limits?: CropConstraints): PixelCrop;
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
export interface Crop {
|
|
2
|
+
x: number;
|
|
3
|
+
y: number;
|
|
4
|
+
width: number;
|
|
5
|
+
height: number;
|
|
6
|
+
unit: 'px' | '%';
|
|
7
|
+
}
|
|
8
|
+
export interface PixelCrop extends Crop {
|
|
9
|
+
unit: 'px';
|
|
10
|
+
}
|
|
11
|
+
export interface PercentCrop extends Crop {
|
|
12
|
+
unit: '%';
|
|
13
|
+
}
|
|
14
|
+
export interface Size {
|
|
15
|
+
width: number;
|
|
16
|
+
height: number;
|
|
17
|
+
}
|
|
18
|
+
export interface ImageFrame extends Size {
|
|
19
|
+
x: number;
|
|
20
|
+
y: number;
|
|
21
|
+
}
|
|
22
|
+
export type CropHandle = 'move' | 'n' | 'ne' | 'e' | 'se' | 's' | 'sw' | 'w' | 'nw';
|
|
23
|
+
export interface CropConstraints {
|
|
24
|
+
/** Fixed width / height ratio in displayed coordinates. */
|
|
25
|
+
aspect?: number;
|
|
26
|
+
/** Minimum displayed width in pixels / DIPs, including for percentage crops. */
|
|
27
|
+
minWidth?: number;
|
|
28
|
+
/** Minimum displayed height in pixels / DIPs, including for percentage crops. */
|
|
29
|
+
minHeight?: number;
|
|
30
|
+
maxWidth?: number;
|
|
31
|
+
maxHeight?: number;
|
|
32
|
+
}
|
|
33
|
+
export interface ImageCropProps extends CropConstraints {
|
|
34
|
+
src: string;
|
|
35
|
+
alt?: string;
|
|
36
|
+
/** Natural image dimensions; must match the decoded, oriented source. */
|
|
37
|
+
imageWidth: number;
|
|
38
|
+
imageHeight: number;
|
|
39
|
+
/** Controlled crop relative to the contained image, excluding letterboxing. */
|
|
40
|
+
crop: Crop;
|
|
41
|
+
/** Update crop with either argument; both describe the same selection. */
|
|
42
|
+
onChange: (pixelCrop: PixelCrop, percentCrop: PercentCrop) => void;
|
|
43
|
+
/** Called once on gesture end; cancellation does not complete. */
|
|
44
|
+
onComplete?: (pixelCrop: PixelCrop, percentCrop: PercentCrop) => void;
|
|
45
|
+
disabled?: boolean;
|
|
46
|
+
ruleOfThirds?: boolean;
|
|
47
|
+
/** Frame dimensions in CSS pixels / native DIPs. */
|
|
48
|
+
width: number;
|
|
49
|
+
height: number;
|
|
50
|
+
id?: string;
|
|
51
|
+
}
|