@bartgarbiak/image-cropper 1.0.0 → 1.1.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/README.md +226 -25
- package/dist/components/ImageCropper.d.ts +3 -13
- package/dist/components/ImageCropper.types.d.ts +68 -0
- package/dist/components/helpers/clampCropDims.d.ts +2 -0
- package/dist/components/helpers/clampOffset.d.ts +2 -0
- package/dist/components/helpers/computeCropSize.d.ts +2 -0
- package/dist/components/helpers/findMaxRotation.d.ts +2 -0
- package/dist/index.cjs +1 -1
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.mjs +337 -178
- package/dist/index.mjs.map +1 -1
- package/dist/utils/useHistory.d.ts +26 -0
- package/package.json +19 -3
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# ImageCropper — Documentation
|
|
2
2
|
|
|
3
|
-
A React component for interactive image rotation and cropping. Built with TypeScript, React 18,
|
|
3
|
+
A React component for interactive image rotation and cropping. Built with TypeScript, React 18, pure CSS, and Vite.
|
|
4
4
|
|
|
5
5
|
---
|
|
6
6
|
|
|
@@ -21,18 +21,32 @@ Requires **Node ≥ 18**.
|
|
|
21
21
|
|
|
22
22
|
```
|
|
23
23
|
cropper/
|
|
24
|
-
├── index.html # Entry HTML (loads src/main.tsx)
|
|
25
24
|
├── package.json
|
|
26
25
|
├── tsconfig.json
|
|
26
|
+
├── tsconfig.build.json
|
|
27
27
|
├── vite.config.js
|
|
28
|
-
├──
|
|
29
|
-
|
|
28
|
+
├── README.md # ← you are here
|
|
29
|
+
├── demo/ # Demo app
|
|
30
|
+
│ ├── index.html
|
|
31
|
+
│ ├── main.tsx
|
|
32
|
+
│ └── App.tsx
|
|
33
|
+
├── test/ # Vitest test suites
|
|
34
|
+
│ ├── helpers.spec.ts
|
|
35
|
+
│ ├── useHistory.spec.ts
|
|
36
|
+
│ └── ImageCropper.spec.tsx
|
|
30
37
|
└── src/
|
|
31
|
-
├──
|
|
32
|
-
├──
|
|
33
|
-
|
|
38
|
+
├── index.ts # Barrel exports
|
|
39
|
+
├── utils/
|
|
40
|
+
│ └── useHistory.ts # Undo/redo history hook
|
|
34
41
|
└── components/
|
|
35
|
-
|
|
42
|
+
├── ImageCropper.tsx # Core component
|
|
43
|
+
├── ImageCropper.types.ts # Type definitions
|
|
44
|
+
├── ImageCropper.css # Component styles
|
|
45
|
+
└── helpers/ # Geometry helpers
|
|
46
|
+
├── computeCropSize.ts
|
|
47
|
+
├── clampCropDims.ts
|
|
48
|
+
├── findMaxRotation.ts
|
|
49
|
+
└── clampOffset.ts
|
|
36
50
|
```
|
|
37
51
|
|
|
38
52
|
---
|
|
@@ -41,18 +55,118 @@ cropper/
|
|
|
41
55
|
|
|
42
56
|
### Props
|
|
43
57
|
|
|
44
|
-
| Prop
|
|
45
|
-
|
|
|
46
|
-
| `
|
|
47
|
-
| `
|
|
58
|
+
| Prop | Type | Default | Description |
|
|
59
|
+
| ------------------ | --------------------------------------- | --------- | ------------------------------------------------------------------------ |
|
|
60
|
+
| `imageSrc` | `string \| null` | `null` | Image source URL or data URL to crop. |
|
|
61
|
+
| `minCropWidth` | `number` | `250` | Minimum crop rectangle width in pixels. |
|
|
62
|
+
| `minCropHeight` | `number` | `250` | Minimum crop rectangle height in pixels. |
|
|
63
|
+
| `labels` | `ImageCropperLabels` | See below | Override any UI label string. |
|
|
64
|
+
| `onCrop` | `(data: CropData) => void` | — | Fired after the user stops cropping for 500 ms. |
|
|
65
|
+
| `onRotate` | `(data: RotationData) => void` | — | Fired after the user stops rotating for 500 ms. |
|
|
66
|
+
| `onChange` | `(data: ChangeData) => void` | — | Fired after any crop or rotation commit (same 500 ms debounce). |
|
|
67
|
+
| `onHistoryChange` | `(canUndo: boolean, canRedo: boolean) => void` | — | Fired whenever the undo/redo availability changes. Use this to keep external buttons in sync. |
|
|
68
|
+
| `ref` | `React.Ref<ImageCropperRef>` | — | Forward ref giving access to `undo`, `redo`, `canUndo`, `canRedo`, `getHistory`. |
|
|
69
|
+
|
|
70
|
+
#### `ImageCropperLabels`
|
|
71
|
+
|
|
72
|
+
All fields are optional. Any omitted key falls back to the English default.
|
|
73
|
+
|
|
74
|
+
| Key | Type | Default |
|
|
75
|
+
| ---------------- | -------- | ---------------------------------- |
|
|
76
|
+
| `rotation` | `string` | `"Rotation"` |
|
|
77
|
+
| `rotate90` | `string` | `"Rotate 90°"` |
|
|
78
|
+
| `rotate180` | `string` | `"Rotate 180°"` |
|
|
79
|
+
| `resetRotation` | `string` | `"Reset Rotation"` |
|
|
80
|
+
| `resetCrop` | `string` | `"Reset Crop"` |
|
|
81
|
+
| `emptyState` | `string` | `"Open an image to get started"` |
|
|
82
|
+
|
|
83
|
+
#### Event Data Types
|
|
84
|
+
|
|
85
|
+
**`CropData`**
|
|
86
|
+
```ts
|
|
87
|
+
{
|
|
88
|
+
x: number; // horizontal position of top-left corner
|
|
89
|
+
y: number; // vertical position of top-left corner
|
|
90
|
+
width: number; // width of the cropped area
|
|
91
|
+
height: number; // height of the cropped area
|
|
92
|
+
}
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
**`RotationData`**
|
|
96
|
+
```ts
|
|
97
|
+
{
|
|
98
|
+
rotation: number; // fine-tune rotation (-45° to 45°)
|
|
99
|
+
baseRotation: number; // coarse rotation (0°, 90°, 180°, 270°)
|
|
100
|
+
}
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
**`ChangeData`**
|
|
104
|
+
```ts
|
|
105
|
+
{
|
|
106
|
+
action: 'rotate' | 'crop';
|
|
107
|
+
crop: CropData;
|
|
108
|
+
rotation: RotationData;
|
|
109
|
+
}
|
|
110
|
+
```
|
|
48
111
|
|
|
49
112
|
### Usage
|
|
50
113
|
|
|
51
114
|
```tsx
|
|
52
|
-
import ImageCropper from '
|
|
115
|
+
import { ImageCropper, type ImageCropperRef } from '@bartgarbiak/image-cropper';
|
|
116
|
+
import '@bartgarbiak/image-cropper/style.css';
|
|
117
|
+
import { useRef, useState } from 'react';
|
|
118
|
+
|
|
119
|
+
function MyComponent() {
|
|
120
|
+
const [imageSrc, setImageSrc] = useState<string | null>(null);
|
|
121
|
+
const [canUndo, setCanUndo] = useState(false);
|
|
122
|
+
const [canRedo, setCanRedo] = useState(false);
|
|
123
|
+
const cropperRef = useRef<ImageCropperRef>(null);
|
|
124
|
+
|
|
125
|
+
const handleFileUpload = (e: React.ChangeEvent<HTMLInputElement>) => {
|
|
126
|
+
const file = e.target.files?.[0];
|
|
127
|
+
if (file) setImageSrc(URL.createObjectURL(file));
|
|
128
|
+
};
|
|
129
|
+
|
|
130
|
+
return (
|
|
131
|
+
<>
|
|
132
|
+
<input type="file" accept="image/*" onChange={handleFileUpload} />
|
|
133
|
+
|
|
134
|
+
<button onClick={() => cropperRef.current?.undo()} disabled={!canUndo}>Undo</button>
|
|
135
|
+
<button onClick={() => cropperRef.current?.redo()} disabled={!canRedo}>Redo</button>
|
|
136
|
+
|
|
137
|
+
<ImageCropper
|
|
138
|
+
ref={cropperRef}
|
|
139
|
+
imageSrc={imageSrc}
|
|
140
|
+
onHistoryChange={(u, r) => { setCanUndo(u); setCanRedo(r); }}
|
|
141
|
+
onCrop={(crop) => console.log('Crop:', crop)}
|
|
142
|
+
onRotate={(rotation) => console.log('Rotation:', rotation)}
|
|
143
|
+
onChange={(data) => console.log('Change:', data)}
|
|
144
|
+
/>
|
|
145
|
+
</>
|
|
146
|
+
);
|
|
147
|
+
}
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
> **Note on event timing** — `onCrop`, `onRotate`, and `onChange` are debounced: they fire only after the user has stopped interacting (dragging a corner, moving the crop, or adjusting the slider) for **500 ms**. Discrete button actions (Rotate 90°, Rotate 180°, Reset Rotation, Reset Crop) commit immediately without the delay. This same debounce controls when undo/redo history entries are created, so every entry represents a "resting" state rather than a frame in the middle of a drag.
|
|
151
|
+
|
|
152
|
+
**Custom min crop size**
|
|
153
|
+
```tsx
|
|
154
|
+
<ImageCropper imageSrc={imageSrc} minCropWidth={100} minCropHeight={100} />
|
|
155
|
+
```
|
|
53
156
|
|
|
54
|
-
|
|
55
|
-
|
|
157
|
+
**Custom labels (i18n)**
|
|
158
|
+
```tsx
|
|
159
|
+
<ImageCropper
|
|
160
|
+
imageSrc={imageSrc}
|
|
161
|
+
labels={{
|
|
162
|
+
rotation: 'Rotación',
|
|
163
|
+
rotate90: 'Girar 90°',
|
|
164
|
+
rotate180: 'Girar 180°',
|
|
165
|
+
resetRotation: 'Restablecer rotación',
|
|
166
|
+
resetCrop: 'Restablecer recorte',
|
|
167
|
+
emptyState: 'Abra una imagen para comenzar',
|
|
168
|
+
}}
|
|
169
|
+
/>
|
|
56
170
|
```
|
|
57
171
|
|
|
58
172
|
---
|
|
@@ -84,9 +198,46 @@ Resizing resets the crop offset to the centre.
|
|
|
84
198
|
|
|
85
199
|
Clicking and dragging inside the crop area moves the entire crop rectangle. The offset is clamped in real-time so that every corner of the crop stays within the rotated image boundary (`clampOffset`).
|
|
86
200
|
|
|
201
|
+
### 90° / 180° Rotation
|
|
202
|
+
|
|
203
|
+
Two buttons allow coarse rotation in 90° and 180° increments. The 90° button is automatically disabled when the resulting (swapped) image dimensions would violate the minimum crop size. Each coarse rotation resets the fine-tune slider and crop to their defaults.
|
|
204
|
+
|
|
205
|
+
### Undo / Redo
|
|
206
|
+
|
|
207
|
+
Every "settled" interaction is pushed to a history stack. An interaction is considered settled when the user stops acting for **500 ms** (slider, drag corner, drag move). Discrete actions (Rotate 90°, 180°, Reset Rotation, Reset Crop) commit to history immediately.
|
|
208
|
+
|
|
209
|
+
Control undo/redo from outside the component via a forwarded ref:
|
|
210
|
+
|
|
211
|
+
```tsx
|
|
212
|
+
const ref = useRef<ImageCropperRef>(null);
|
|
213
|
+
|
|
214
|
+
// call these from external buttons
|
|
215
|
+
ref.current?.undo();
|
|
216
|
+
ref.current?.redo();
|
|
217
|
+
|
|
218
|
+
// read availability
|
|
219
|
+
ref.current?.canUndo; // boolean
|
|
220
|
+
ref.current?.canRedo; // boolean
|
|
221
|
+
|
|
222
|
+
// inspect the full stack
|
|
223
|
+
ref.current?.getHistory(); // { past, present, future }
|
|
224
|
+
```
|
|
225
|
+
|
|
226
|
+
Use `onHistoryChange` to keep external UI (e.g. toolbar buttons) in sync without polling the ref:
|
|
227
|
+
|
|
228
|
+
```tsx
|
|
229
|
+
<ImageCropper
|
|
230
|
+
ref={ref}
|
|
231
|
+
onHistoryChange={(canUndo, canRedo) => {
|
|
232
|
+
setCanUndo(canUndo);
|
|
233
|
+
setCanRedo(canRedo);
|
|
234
|
+
}}
|
|
235
|
+
/>
|
|
236
|
+
```
|
|
237
|
+
|
|
87
238
|
### Reset Buttons
|
|
88
239
|
|
|
89
|
-
- **Reset Rotation** — sets rotation to 0° and resets crop size + position.
|
|
240
|
+
- **Reset Rotation** — sets both coarse and fine rotation to 0° and resets crop size + position.
|
|
90
241
|
- **Reset Crop** — restores the default (full-image) crop size and centres it.
|
|
91
242
|
|
|
92
243
|
---
|
|
@@ -120,16 +271,66 @@ where $W \times H$ is the displayed image size and $\theta$ is the rotation angl
|
|
|
120
271
|
|
|
121
272
|
## Types
|
|
122
273
|
|
|
123
|
-
|
|
124
|
-
type CornerPos = 'tl' | 'tr' | 'bl' | 'br';
|
|
125
|
-
|
|
126
|
-
type DragMode =
|
|
127
|
-
| { kind: 'corner'; corner: CornerPos }
|
|
128
|
-
| { kind: 'move'; startX: number; startY: number; startOffset: Point };
|
|
274
|
+
All types are exported from the package entry point.
|
|
129
275
|
|
|
130
|
-
|
|
276
|
+
```ts
|
|
277
|
+
interface ImageCropperProps {
|
|
278
|
+
imageSrc?: string | null;
|
|
131
279
|
minCropWidth?: number;
|
|
132
280
|
minCropHeight?: number;
|
|
281
|
+
labels?: ImageCropperLabels;
|
|
282
|
+
onCrop?: (data: CropData) => void;
|
|
283
|
+
onRotate?: (data: RotationData) => void;
|
|
284
|
+
onChange?: (data: ChangeData) => void;
|
|
285
|
+
/** Fired when canUndo / canRedo change — use this to drive external buttons. */
|
|
286
|
+
onHistoryChange?: (canUndo: boolean, canRedo: boolean) => void;
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
/** Exposed via forwardRef — access with useRef<ImageCropperRef>(null) */
|
|
290
|
+
interface ImageCropperRef {
|
|
291
|
+
undo: () => void;
|
|
292
|
+
redo: () => void;
|
|
293
|
+
canUndo: boolean;
|
|
294
|
+
canRedo: boolean;
|
|
295
|
+
getHistory: () => {
|
|
296
|
+
past: CropperState[];
|
|
297
|
+
present: CropperState;
|
|
298
|
+
future: CropperState[];
|
|
299
|
+
};
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
interface CropperState {
|
|
303
|
+
rotation: number;
|
|
304
|
+
baseRotation: number;
|
|
305
|
+
cropSize: { width: number; height: number } | null;
|
|
306
|
+
cropOffset: { x: number; y: number };
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
interface ImageCropperLabels {
|
|
310
|
+
rotation?: string;
|
|
311
|
+
rotate90?: string;
|
|
312
|
+
rotate180?: string;
|
|
313
|
+
resetRotation?: string;
|
|
314
|
+
resetCrop?: string;
|
|
315
|
+
emptyState?: string;
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
interface CropData {
|
|
319
|
+
x: number;
|
|
320
|
+
y: number;
|
|
321
|
+
width: number;
|
|
322
|
+
height: number;
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
interface RotationData {
|
|
326
|
+
rotation: number;
|
|
327
|
+
baseRotation: number;
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
interface ChangeData {
|
|
331
|
+
action: 'rotate' | 'crop';
|
|
332
|
+
crop: CropData;
|
|
333
|
+
rotation: RotationData;
|
|
133
334
|
}
|
|
134
335
|
|
|
135
336
|
interface Size { width: number; height: number; }
|
|
@@ -143,9 +344,9 @@ interface Point { x: number; y: number; }
|
|
|
143
344
|
| Layer | Library |
|
|
144
345
|
| ----------- | ---------------------- |
|
|
145
346
|
| UI | React 18 |
|
|
146
|
-
| Styling |
|
|
347
|
+
| Styling | Pure CSS (custom props) |
|
|
147
348
|
| Language | TypeScript 5 (strict) |
|
|
148
|
-
| Bundler | Vite 4
|
|
349
|
+
| Bundler | Vite 4 (library mode) |
|
|
149
350
|
|
|
150
351
|
---
|
|
151
352
|
|
|
@@ -1,14 +1,4 @@
|
|
|
1
|
+
import React from 'react';
|
|
1
2
|
import './ImageCropper.css';
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
minCropHeight?: number;
|
|
5
|
-
}
|
|
6
|
-
export interface Size {
|
|
7
|
-
width: number;
|
|
8
|
-
height: number;
|
|
9
|
-
}
|
|
10
|
-
export interface Point {
|
|
11
|
-
x: number;
|
|
12
|
-
y: number;
|
|
13
|
-
}
|
|
14
|
-
export declare function ImageCropper({ minCropWidth, minCropHeight, }: ImageCropperProps): import("react/jsx-runtime").JSX.Element;
|
|
3
|
+
import type { ImageCropperProps, ImageCropperRef } from './ImageCropper.types';
|
|
4
|
+
export declare const ImageCropper: React.ForwardRefExoticComponent<ImageCropperProps & React.RefAttributes<ImageCropperRef>>;
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
export type CornerPos = 'tl' | 'tr' | 'bl' | 'br';
|
|
2
|
+
export interface Size {
|
|
3
|
+
width: number;
|
|
4
|
+
height: number;
|
|
5
|
+
}
|
|
6
|
+
export interface Point {
|
|
7
|
+
x: number;
|
|
8
|
+
y: number;
|
|
9
|
+
}
|
|
10
|
+
export type DragMode = {
|
|
11
|
+
kind: 'corner';
|
|
12
|
+
corner: CornerPos;
|
|
13
|
+
} | {
|
|
14
|
+
kind: 'move';
|
|
15
|
+
startX: number;
|
|
16
|
+
startY: number;
|
|
17
|
+
startOffset: Point;
|
|
18
|
+
};
|
|
19
|
+
export interface ImageCropperLabels {
|
|
20
|
+
rotation?: string;
|
|
21
|
+
rotate90?: string;
|
|
22
|
+
rotate180?: string;
|
|
23
|
+
resetRotation?: string;
|
|
24
|
+
resetCrop?: string;
|
|
25
|
+
emptyState?: string;
|
|
26
|
+
}
|
|
27
|
+
export interface CropData {
|
|
28
|
+
x: number;
|
|
29
|
+
y: number;
|
|
30
|
+
width: number;
|
|
31
|
+
height: number;
|
|
32
|
+
}
|
|
33
|
+
export interface RotationData {
|
|
34
|
+
rotation: number;
|
|
35
|
+
baseRotation: number;
|
|
36
|
+
}
|
|
37
|
+
export interface ChangeData {
|
|
38
|
+
action: 'rotate' | 'crop';
|
|
39
|
+
crop: CropData;
|
|
40
|
+
rotation: RotationData;
|
|
41
|
+
}
|
|
42
|
+
export interface CropperState {
|
|
43
|
+
rotation: number;
|
|
44
|
+
baseRotation: number;
|
|
45
|
+
cropSize: Size | null;
|
|
46
|
+
cropOffset: Point;
|
|
47
|
+
}
|
|
48
|
+
export interface ImageCropperRef {
|
|
49
|
+
undo: () => void;
|
|
50
|
+
redo: () => void;
|
|
51
|
+
canUndo: boolean;
|
|
52
|
+
canRedo: boolean;
|
|
53
|
+
getHistory: () => {
|
|
54
|
+
past: CropperState[];
|
|
55
|
+
present: CropperState;
|
|
56
|
+
future: CropperState[];
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
export interface ImageCropperProps {
|
|
60
|
+
imageSrc?: string | null;
|
|
61
|
+
minCropWidth?: number;
|
|
62
|
+
minCropHeight?: number;
|
|
63
|
+
labels?: ImageCropperLabels;
|
|
64
|
+
onCrop?: (data: CropData) => void;
|
|
65
|
+
onRotate?: (data: RotationData) => void;
|
|
66
|
+
onChange?: (data: ChangeData) => void;
|
|
67
|
+
onHistoryChange?: (canUndo: boolean, canRedo: boolean) => void;
|
|
68
|
+
}
|
package/dist/index.cjs
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
"use strict";Object.defineProperty(exports,Symbol.toStringTag,{value:"Module"});const
|
|
1
|
+
"use strict";Object.defineProperty(exports,Symbol.toStringTag,{value:"Module"});const i=require("react/jsx-runtime"),e=require("react");function ht(m){const[n,l]=e.useState({past:[],present:m,future:[]}),[v,x]=e.useState(m),f=e.useRef(m),j=e.useCallback(t=>{f.current=t,x(t)},[]),R=e.useCallback(t=>{const o=t??f.current;t!==void 0&&(f.current=t,x(t)),l(h=>JSON.stringify(o)===JSON.stringify(h.present)?h:{past:[...h.past,h.present],present:o,future:[]})},[]),u=e.useCallback(()=>{l(t=>{if(t.past.length===0)return t;const o=t.past[t.past.length-1],h=t.past.slice(0,t.past.length-1);return f.current=o,x(o),{past:h,present:o,future:[t.present,...t.future]}})},[]),a=e.useCallback(()=>{l(t=>{if(t.future.length===0)return t;const o=t.future[0],h=t.future.slice(1);return f.current=o,x(o),{past:[...t.past,t.present],present:o,future:h}})},[]),M=e.useCallback(t=>{f.current=t,x(t),l({past:[],present:t,future:[]})},[]),p=e.useCallback(()=>({past:n.past,present:n.present,future:n.future}),[n]);return{state:v,committed:n.present,canUndo:n.past.length>0,canRedo:n.future.length>0,stage:j,commit:R,undo:u,redo:a,reset:M,getHistory:p}}function K(m,n,l){if(m===0||n===0)return{width:0,height:0};const v=Math.abs(l)*(Math.PI/180);if(v<1e-9)return{width:m,height:n};const x=Math.cos(v),f=Math.sin(v),j=Math.min(m/(m*x+n*f),n/(m*f+n*x));return{width:j*m,height:j*n}}function $(m,n,l,v,x,f,j){const R=Math.abs(x)*(Math.PI/180),u=Math.cos(R),a=Math.sin(R),M=l/2,p=v/2,t=f/2,o=j/2;let h=Math.max(m/2,t),r=Math.max(n/2,o);if(R<1e-9)h=Math.min(h,M),r=Math.min(r,p);else{let c=1/0;if(u>1e-9&&(c=Math.min(c,(M-r*a)/u)),a>1e-9&&(c=Math.min(c,(p-r*u)/a)),c<t){let y=1/0;a>1e-9&&(y=Math.min(y,(M-t*u)/a)),u>1e-9&&(y=Math.min(y,(p-t*a)/u)),r=Math.max(Math.min(r,y),o),h=t}else h=Math.min(h,c);let d=1/0;a>1e-9&&(d=Math.min(d,(M-h*u)/a)),u>1e-9&&(d=Math.min(d,(p-h*a)/u)),r=Math.max(Math.min(r,d),o)}return{width:h*2,height:r*2}}function dt(m,n,l,v,x){let f=0,j=45;for(let R=0;R<50;R++){const u=(f+j)/2,a=l?$(l.width,l.height,m,n,u,v,x):K(m,n,u);a.width>=v&&a.height>=x?f=u:j=u}return Math.round(f*10)/10}function G(m,n,l,v,x,f,j){const R=j*(Math.PI/180),u=Math.cos(R),a=Math.sin(R),M=x/2,p=f/2,t=l/2,o=v/2,h=[[-t,-o],[t,-o],[-t,o],[t,o]];let r=m,c=n;for(let d=0;d<8;d++){let y=!0;for(const[z,w]of h){const N=r+z,S=c+w,O=N*u+S*a,E=-N*a+S*u;if(Math.abs(O)>M+.5){const D=O>0?O-M:O+M;r-=D*u,c-=D*a,y=!1}if(Math.abs(E)>p+.5){const D=E>0?E-p:E+p;r+=D*a,c-=D*u,y=!1}}if(y)break}return{x:r,y:c}}const ft={rotation:"Rotation",rotate90:"Rotate 90°",rotate180:"Rotate 180°",resetRotation:"Reset Rotation",resetCrop:"Reset Crop",emptyState:"Open an image to get started"},Q=e.forwardRef(({imageSrc:m,minCropWidth:n=250,minCropHeight:l=250,labels:v,onCrop:x,onRotate:f,onChange:j,onHistoryChange:R},u)=>{const a=e.useMemo(()=>({...ft,...v}),[v]),M=m,[p,t]=e.useState({width:0,height:0}),[o,h]=e.useState(null),r=ht({rotation:0,baseRotation:0,cropSize:null,cropOffset:{x:0,y:0}}),{rotation:c,baseRotation:d,cropSize:y,cropOffset:z}=r.state,w=e.useRef(null),N=e.useCallback(s=>{r.stage({...r.state,...s}),w.current&&clearTimeout(w.current),w.current=setTimeout(()=>{w.current=null,r.commit()},500)},[r]),S=e.useCallback(s=>{w.current&&(clearTimeout(w.current),w.current=null),r.commit({...r.state,...s})},[r]),O=e.useRef(null),E=e.useRef(null),D=e.useCallback(()=>{if(!O.current)return;const s=O.current.offsetWidth,g=O.current.offsetHeight;t(I=>I.width===s&&I.height===g?I:{width:s,height:g})},[]);e.useEffect(()=>{const s=O.current;if(!s)return;const g=new ResizeObserver(D);return g.observe(s),()=>g.disconnect()},[M,D]),e.useEffect(()=>{w.current&&(clearTimeout(w.current),w.current=null),r.reset({rotation:0,baseRotation:0,cropSize:null,cropOffset:{x:0,y:0}})},[M]);const b=e.useMemo(()=>d===90||d===270?{width:p.height,height:p.width}:p,[p,d]),k=e.useMemo(()=>y?$(y.width,y.height,b.width,b.height,c,n,l):K(b.width,b.height,c),[y,b,c,n,l]),U=e.useCallback((s,g)=>{g.preventDefault(),g.stopPropagation(),h({kind:"corner",corner:s})},[]),V=e.useCallback(s=>{s.preventDefault(),s.stopPropagation(),h({kind:"move",startX:s.clientX,startY:s.clientY,startOffset:{...z}})},[z]);e.useEffect(()=>{if(!o)return;const s=o.kind==="move"?"move":{tl:"nw-resize",tr:"ne-resize",bl:"sw-resize",br:"se-resize"}[o.corner];document.body.style.cursor=s,document.body.style.userSelect="none";const g=C=>{const A=E.current;if(!A)return;if(o.kind==="move"){const ct=C.clientX-o.startX,at=C.clientY-o.startY,it=o.startOffset.x+ct,lt=o.startOffset.y+at,ut=G(it,lt,k.width,k.height,b.width,b.height,c);N({cropOffset:ut});return}const X=A.getBoundingClientRect(),et=X.left+X.width/2,st=X.top+X.height/2;let L=C.clientX-et,F=C.clientY-st;const{corner:Y}=o;(Y==="tl"||Y==="bl")&&(L=-L),(Y==="tl"||Y==="tr")&&(F=-F);const rt=Math.max(L*2,20),nt=Math.max(F*2,20),ot=$(rt,nt,b.width,b.height,c,n,l);N({cropSize:ot,cropOffset:{x:0,y:0}})},I=()=>h(null);return document.addEventListener("mousemove",g),document.addEventListener("mouseup",I),()=>{document.removeEventListener("mousemove",g),document.removeEventListener("mouseup",I),document.body.style.cursor="",document.body.style.userSelect=""}},[o,b,c,k,n,l,N]);const T=e.useMemo(()=>b.width>0&&b.height>0?dt(b.width,b.height,y,n,l):45,[b,y,n,l]);e.useEffect(()=>{const s=Math.max(-T,Math.min(T,c));s!==c&&N({rotation:s})},[T,c,N]);const P=e.useMemo(()=>G(z.x,z.y,k.width,k.height,b.width,b.height,c),[z,k,b,c]),q=e.useMemo(()=>({x:P.x-k.width/2,y:P.y-k.height/2,width:k.width,height:k.height}),[P,k]),B=e.useMemo(()=>({rotation:c,baseRotation:d}),[c,d]),J=e.useRef(null);e.useEffect(()=>{if(!M)return;const s=J.current;J.current=r.committed;const g=!s||r.committed.rotation!==s.rotation||r.committed.baseRotation!==s.baseRotation;x==null||x(q),f==null||f(B),j==null||j({action:g?"rotate":"crop",crop:q,rotation:B})},[r.committed]),e.useEffect(()=>{R==null||R(r.canUndo,r.canRedo)},[r.canUndo,r.canRedo,R]),e.useImperativeHandle(u,()=>({undo:()=>{w.current&&(clearTimeout(w.current),w.current=null),r.undo()},redo:()=>{w.current&&(clearTimeout(w.current),w.current=null),r.redo()},canUndo:r.canUndo,canRedo:r.canRedo,getHistory:r.getHistory}),[r]);const Z=e.useCallback(()=>{S({cropSize:null,cropOffset:{x:0,y:0}})},[S]),_=e.useCallback(s=>{const g=parseFloat(s.target.value);N({rotation:g})},[N]),H=e.useMemo(()=>{const s=(d+90)%360,g=s===90||s===270,I=g?p.height:p.width,C=g?p.width:p.height;return I>=n&&C>=l},[p,d,n,l]),W=e.useCallback(()=>{S({baseRotation:(d+90)%360,rotation:0,cropSize:null,cropOffset:{x:0,y:0}})},[d,S]),tt=e.useCallback(()=>{S({baseRotation:(d+180)%360,rotation:0,cropSize:null,cropOffset:{x:0,y:0}})},[d,S]);return i.jsxs("div",{className:"cropper-container",children:[i.jsx("div",{className:"cropper-workspace",ref:E,children:M?i.jsxs(i.Fragment,{children:[i.jsx("img",{className:"cropper-image",ref:O,src:M,style:{transform:`rotate(${d+c}deg)`},onLoad:D,alt:"preview"}),p.width>0&&i.jsxs("div",{className:"cropper-overlay",style:{width:k.width,height:k.height,transform:`translate(calc(-50% + ${P.x}px), calc(-50% + ${P.y}px))`},children:[i.jsx("div",{className:"cropper-move-handle",onMouseDown:V}),i.jsx("div",{className:"cropper-corner cropper-corner--tl",onMouseDown:s=>U("tl",s)}),i.jsx("div",{className:"cropper-corner cropper-corner--tr",onMouseDown:s=>U("tr",s)}),i.jsx("div",{className:"cropper-corner cropper-corner--bl",onMouseDown:s=>U("bl",s)}),i.jsx("div",{className:"cropper-corner cropper-corner--br",onMouseDown:s=>U("br",s)})]})]}):i.jsx("div",{className:"cropper-empty",children:i.jsx("span",{children:a.emptyState})})}),M&&i.jsxs("div",{className:"cropper-controls",children:[i.jsx("div",{className:"cropper-label",children:a.rotation}),i.jsxs("div",{className:"cropper-slider-row",children:[i.jsxs("span",{className:"cropper-range-label",children:["-",T,"°"]}),i.jsx("input",{className:"cropper-slider",type:"range",min:-T,max:T,step:.1,value:Math.max(-T,Math.min(T,c)),onChange:_}),i.jsxs("span",{className:"cropper-range-label",children:[T,"°"]})]}),i.jsxs("div",{className:"cropper-angle",children:[c.toFixed(1),"°"]}),i.jsxs("div",{className:"cropper-button-row",children:[i.jsx("button",{className:"cropper-button",onClick:W,disabled:!H,children:a.rotate90}),i.jsx("button",{className:"cropper-button",onClick:tt,children:a.rotate180})]}),i.jsxs("div",{className:"cropper-button-row",children:[(c!==0||d!==0)&&i.jsx("button",{className:"cropper-button",onClick:()=>{S({rotation:0,baseRotation:0,cropSize:null,cropOffset:{x:0,y:0}})},children:a.resetRotation}),(y||z.x!==0||z.y!==0)&&i.jsx("button",{className:"cropper-button",onClick:Z,children:a.resetCrop})]})]})]})});Q.displayName="ImageCropper";exports.ImageCropper=Q;
|
|
2
2
|
//# sourceMappingURL=index.cjs.map
|
package/dist/index.cjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.cjs","sources":["../src/components/ImageCropper.tsx"],"sourcesContent":["import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react';\nimport './ImageCropper.css';\n\n/* ───────────────────────── Types ────────────────────────────────── */\n\ntype CornerPos = 'tl' | 'tr' | 'bl' | 'br';\n\ntype DragMode =\n | { kind: 'corner'; corner: CornerPos }\n | { kind: 'move'; startX: number; startY: number; startOffset: Point };\n\nexport interface ImageCropperProps {\n minCropWidth?: number;\n minCropHeight?: number;\n}\n\nexport interface Size {\n width: number;\n height: number;\n}\n\nexport interface Point {\n x: number;\n y: number;\n}\n\n/* ───────────────────────── Geometry helpers ─────────────────────── */\n\n/**\n * Largest axis-aligned rectangle with aspect ratio W/H that fits\n * entirely inside the image rotated by rotationDeg (centred).\n */\nfunction computeCropSize(W: number, H: number, rotationDeg: number): Size {\n if (W === 0 || H === 0) return { width: 0, height: 0 };\n const theta = Math.abs(rotationDeg) * (Math.PI / 180);\n if (theta < 1e-9) return { width: W, height: H };\n const cosT = Math.cos(theta);\n const sinT = Math.sin(theta);\n const s = Math.min(W / (W * cosT + H * sinT), H / (W * sinT + H * cosT));\n return { width: s * W, height: s * H };\n}\n\n/**\n * Clamp crop dimensions so that a centred crop of that size fits\n * inside the rotated image.\n */\nfunction clampCropDims(\n cropW: number,\n cropH: number,\n imgW: number,\n imgH: number,\n rotationDeg: number,\n minW: number,\n minH: number,\n): Size {\n const theta = Math.abs(rotationDeg) * (Math.PI / 180);\n const cosT = Math.cos(theta);\n const sinT = Math.sin(theta);\n const hiW = imgW / 2;\n const hiH = imgH / 2;\n const mhw = minW / 2;\n const mhh = minH / 2;\n\n let hw = Math.max(cropW / 2, mhw);\n let hh = Math.max(cropH / 2, mhh);\n\n if (theta < 1e-9) {\n hw = Math.min(hw, hiW);\n hh = Math.min(hh, hiH);\n } else {\n let maxHW = Infinity;\n if (cosT > 1e-9) maxHW = Math.min(maxHW, (hiW - hh * sinT) / cosT);\n if (sinT > 1e-9) maxHW = Math.min(maxHW, (hiH - hh * cosT) / sinT);\n\n if (maxHW < mhw) {\n let maxHH2 = Infinity;\n if (sinT > 1e-9) maxHH2 = Math.min(maxHH2, (hiW - mhw * cosT) / sinT);\n if (cosT > 1e-9) maxHH2 = Math.min(maxHH2, (hiH - mhw * sinT) / cosT);\n hh = Math.max(Math.min(hh, maxHH2), mhh);\n hw = mhw;\n } else {\n hw = Math.min(hw, maxHW);\n }\n\n let maxHH = Infinity;\n if (sinT > 1e-9) maxHH = Math.min(maxHH, (hiW - hw * cosT) / sinT);\n if (cosT > 1e-9) maxHH = Math.min(maxHH, (hiH - hw * sinT) / cosT);\n hh = Math.max(Math.min(hh, maxHH), mhh);\n }\n\n return { width: hw * 2, height: hh * 2 };\n}\n\n/**\n * Binary-search for the maximum |θ| (up to 45°) where the effective\n * crop still meets min-dimension constraints.\n */\nfunction findMaxRotation(\n imgW: number,\n imgH: number,\n customCrop: Size | null,\n minW: number,\n minH: number,\n): number {\n let lo = 0;\n let hi = 45;\n for (let i = 0; i < 50; i++) {\n const mid = (lo + hi) / 2;\n const c = customCrop\n ? clampCropDims(customCrop.width, customCrop.height, imgW, imgH, mid, minW, minH)\n : computeCropSize(imgW, imgH, mid);\n if (c.width >= minW && c.height >= minH) lo = mid;\n else hi = mid;\n }\n return Math.round(lo * 10) / 10;\n}\n\n/**\n * Clamp the crop offset so every corner of the axis-aligned crop\n * rectangle stays inside the rotated image.\n *\n * A point (px, py) in screen-space is inside the rotated image iff\n * |px·cos θ + py·sin θ| ≤ imgW/2\n * |−px·sin θ + py·cos θ| ≤ imgH/2\n *\n * We check all four corners and iteratively push the offset inward.\n */\nfunction clampOffset(\n ox: number,\n oy: number,\n cropW: number,\n cropH: number,\n imgW: number,\n imgH: number,\n rotationDeg: number,\n): Point {\n const theta = rotationDeg * (Math.PI / 180);\n const cosT = Math.cos(theta);\n const sinT = Math.sin(theta);\n const hiW = imgW / 2;\n const hiH = imgH / 2;\n const hw = cropW / 2;\n const hh = cropH / 2;\n\n const corners: [number, number][] = [\n [-hw, -hh],\n [hw, -hh],\n [-hw, hh],\n [hw, hh],\n ];\n\n let cx = ox;\n let cy = oy;\n\n for (let iter = 0; iter < 8; iter++) {\n let ok = true;\n for (const [dx, dy] of corners) {\n const px = cx + dx;\n const py = cy + dy;\n const u = px * cosT + py * sinT;\n const v = -px * sinT + py * cosT;\n\n if (Math.abs(u) > hiW + 0.5) {\n const excess = u > 0 ? u - hiW : u + hiW;\n cx -= excess * cosT;\n cy -= excess * sinT;\n ok = false;\n }\n if (Math.abs(v) > hiH + 0.5) {\n const excess = v > 0 ? v - hiH : v + hiH;\n cx += excess * sinT;\n cy -= excess * cosT;\n ok = false;\n }\n }\n if (ok) break;\n }\n\n return { x: cx, y: cy };\n}\n\n/* ───────────────────────── Component ────────────────────────────── */\n\nexport function ImageCropper({\n minCropWidth = 250,\n minCropHeight = 250,\n}: ImageCropperProps) {\n const [imageSrc, setImageSrc] = useState<string | null>(null);\n const [rotation, setRotation] = useState(0);\n const [displaySize, setDisplaySize] = useState<Size>({ width: 0, height: 0 });\n const [cropSize, setCropSize] = useState<Size | null>(null);\n const [cropOffset, setCropOffset] = useState<Point>({ x: 0, y: 0 });\n const [drag, setDrag] = useState<DragMode | null>(null);\n\n const imgRef = useRef<HTMLImageElement>(null);\n const workspaceRef = useRef<HTMLDivElement>(null);\n\n /* ── Upload ── */\n const handleUpload = useCallback(\n (e: React.ChangeEvent<HTMLInputElement>) => {\n const file = e.target.files?.[0];\n if (!file) return;\n if (imageSrc) URL.revokeObjectURL(imageSrc);\n setImageSrc(URL.createObjectURL(file));\n setRotation(0);\n setCropSize(null);\n setCropOffset({ x: 0, y: 0 });\n },\n [imageSrc],\n );\n\n /* ── Track rendered image size ── */\n const syncDisplaySize = useCallback(() => {\n if (!imgRef.current) return;\n const w = imgRef.current.offsetWidth;\n const h = imgRef.current.offsetHeight;\n setDisplaySize((prev) =>\n prev.width === w && prev.height === h ? prev : { width: w, height: h },\n );\n }, []);\n\n useEffect(() => {\n const el = imgRef.current;\n if (!el) return;\n const ro = new ResizeObserver(syncDisplaySize);\n ro.observe(el);\n return () => ro.disconnect();\n }, [imageSrc, syncDisplaySize]);\n\n /* ── Effective crop dimensions (needed by drag handlers) ── */\n const effectiveCrop = useMemo<Size>(() => {\n if (cropSize)\n return clampCropDims(\n cropSize.width, cropSize.height,\n displaySize.width, displaySize.height,\n rotation, minCropWidth, minCropHeight,\n );\n return computeCropSize(displaySize.width, displaySize.height, rotation);\n }, [cropSize, displaySize, rotation, minCropWidth, minCropHeight]);\n\n /* ── Start corner resize ── */\n const handleCornerMouseDown = useCallback(\n (corner: CornerPos, e: React.MouseEvent) => {\n e.preventDefault();\n e.stopPropagation();\n setDrag({ kind: 'corner', corner });\n },\n [],\n );\n\n /* ── Start move ── */\n const handleMoveMouseDown = useCallback(\n (e: React.MouseEvent) => {\n e.preventDefault();\n e.stopPropagation();\n setDrag({\n kind: 'move',\n startX: e.clientX,\n startY: e.clientY,\n startOffset: { ...cropOffset },\n });\n },\n [cropOffset],\n );\n\n /* ── Drag effect (resize + move) ── */\n useEffect(() => {\n if (!drag) return;\n\n const cursor =\n drag.kind === 'move'\n ? 'move'\n : ({ tl: 'nw-resize', tr: 'ne-resize', bl: 'sw-resize', br: 'se-resize' } as const)[\n drag.corner\n ];\n\n document.body.style.cursor = cursor;\n document.body.style.userSelect = 'none';\n\n const handleMouseMove = (e: MouseEvent) => {\n const ws = workspaceRef.current;\n if (!ws) return;\n\n if (drag.kind === 'move') {\n const dx = e.clientX - drag.startX;\n const dy = e.clientY - drag.startY;\n const rawX = drag.startOffset.x + dx;\n const rawY = drag.startOffset.y + dy;\n setCropOffset(\n clampOffset(\n rawX, rawY,\n effectiveCrop.width, effectiveCrop.height,\n displaySize.width, displaySize.height,\n rotation,\n ),\n );\n return;\n }\n\n /* Corner resize — resets offset to keep things simple */\n const rect = ws.getBoundingClientRect();\n const cx = rect.left + rect.width / 2;\n const cy = rect.top + rect.height / 2;\n\n let mx = e.clientX - cx;\n let my = e.clientY - cy;\n\n const { corner } = drag;\n if (corner === 'tl' || corner === 'bl') mx = -mx;\n if (corner === 'tl' || corner === 'tr') my = -my;\n\n const desiredW = Math.max(mx * 2, 20);\n const desiredH = Math.max(my * 2, 20);\n\n setCropSize(\n clampCropDims(\n desiredW, desiredH,\n displaySize.width, displaySize.height,\n rotation, minCropWidth, minCropHeight,\n ),\n );\n setCropOffset({ x: 0, y: 0 });\n };\n\n const handleMouseUp = () => setDrag(null);\n\n document.addEventListener('mousemove', handleMouseMove);\n document.addEventListener('mouseup', handleMouseUp);\n return () => {\n document.removeEventListener('mousemove', handleMouseMove);\n document.removeEventListener('mouseup', handleMouseUp);\n document.body.style.cursor = '';\n document.body.style.userSelect = '';\n };\n }, [drag, displaySize, rotation, effectiveCrop, minCropWidth, minCropHeight]);\n\n /* ── Max rotation ── */\n const maxRotation = useMemo(\n () =>\n displaySize.width > 0 && displaySize.height > 0\n ? findMaxRotation(displaySize.width, displaySize.height, cropSize, minCropWidth, minCropHeight)\n : 45,\n [displaySize, cropSize, minCropWidth, minCropHeight],\n );\n\n useEffect(() => {\n setRotation((prev) => Math.max(-maxRotation, Math.min(maxRotation, prev)));\n }, [maxRotation]);\n\n /* ── Clamped offset ── */\n const effectiveOffset = useMemo<Point>(\n () =>\n clampOffset(\n cropOffset.x, cropOffset.y,\n effectiveCrop.width, effectiveCrop.height,\n displaySize.width, displaySize.height,\n rotation,\n ),\n [cropOffset, effectiveCrop, displaySize, rotation],\n );\n\n /* ── Resets ── */\n const resetCrop = useCallback(() => {\n setCropSize(null);\n setCropOffset({ x: 0, y: 0 });\n }, []);\n\n const handleRotationChange = useCallback(\n (e: React.ChangeEvent<HTMLInputElement>) => {\n const newRot = parseFloat(e.target.value);\n setRotation(newRot);\n },\n [],\n );\n\n /* ── Render ── */\n return (\n <div className=\"cropper-container\">\n <input type=\"file\" accept=\"image/*\" onChange={handleUpload} />\n\n <div className=\"cropper-workspace\" ref={workspaceRef}>\n {imageSrc ? (\n <>\n <img\n className=\"cropper-image\"\n ref={imgRef}\n src={imageSrc}\n style={{ transform: `rotate(${rotation}deg)` }}\n onLoad={syncDisplaySize}\n alt=\"preview\"\n />\n\n {displaySize.width > 0 && (\n <div\n className=\"cropper-overlay\"\n style={{\n width: effectiveCrop.width,\n height: effectiveCrop.height,\n transform: `translate(calc(-50% + ${effectiveOffset.x}px), calc(-50% + ${effectiveOffset.y}px))`,\n }}\n >\n <div className=\"cropper-move-handle\" onMouseDown={handleMoveMouseDown} />\n <div className=\"cropper-corner cropper-corner--tl\" onMouseDown={(e) => handleCornerMouseDown('tl', e)} />\n <div className=\"cropper-corner cropper-corner--tr\" onMouseDown={(e) => handleCornerMouseDown('tr', e)} />\n <div className=\"cropper-corner cropper-corner--bl\" onMouseDown={(e) => handleCornerMouseDown('bl', e)} />\n <div className=\"cropper-corner cropper-corner--br\" onMouseDown={(e) => handleCornerMouseDown('br', e)} />\n </div>\n )}\n </>\n ) : (\n <div className=\"cropper-empty\">\n <span>Open an image to get started</span>\n </div>\n )}\n </div>\n\n {imageSrc && (\n <div className=\"cropper-controls\">\n <div className=\"cropper-label\">Rotation</div>\n <div className=\"cropper-slider-row\">\n <span className=\"cropper-range-label\">-{maxRotation}°</span>\n <input\n className=\"cropper-slider\"\n type=\"range\"\n min={-maxRotation}\n max={maxRotation}\n step={0.1}\n value={Math.max(-maxRotation, Math.min(maxRotation, rotation))}\n onChange={handleRotationChange}\n />\n <span className=\"cropper-range-label\">{maxRotation}°</span>\n </div>\n <div className=\"cropper-angle\">{rotation.toFixed(1)}°</div>\n <div className=\"cropper-button-row\">\n {rotation !== 0 && (\n <button\n className=\"cropper-button\"\n onClick={() => {\n setRotation(0);\n setCropSize(null);\n setCropOffset({ x: 0, y: 0 });\n }}\n >\n Reset Rotation\n </button>\n )}\n {(cropSize || cropOffset.x !== 0 || cropOffset.y !== 0) && (\n <button className=\"cropper-button\" onClick={resetCrop}>Reset Crop</button>\n )}\n </div>\n </div>\n )}\n </div>\n );\n}\n"],"names":["computeCropSize","W","H","rotationDeg","theta","cosT","sinT","s","clampCropDims","cropW","cropH","imgW","imgH","minW","minH","hiW","hiH","mhw","mhh","hw","hh","maxHW","maxHH2","maxHH","findMaxRotation","customCrop","lo","hi","i","mid","c","clampOffset","ox","oy","corners","cx","cy","iter","ok","dx","dy","px","py","u","v","excess","ImageCropper","minCropWidth","minCropHeight","imageSrc","setImageSrc","useState","rotation","setRotation","displaySize","setDisplaySize","cropSize","setCropSize","cropOffset","setCropOffset","drag","setDrag","imgRef","useRef","workspaceRef","handleUpload","useCallback","file","_a","syncDisplaySize","w","h","prev","useEffect","el","ro","effectiveCrop","useMemo","handleCornerMouseDown","corner","e","handleMoveMouseDown","cursor","handleMouseMove","ws","rawX","rawY","rect","mx","my","desiredW","desiredH","handleMouseUp","maxRotation","effectiveOffset","resetCrop","handleRotationChange","newRot","jsxs","jsx","Fragment"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"index.cjs","sources":["../src/utils/useHistory.ts","../src/components/helpers/computeCropSize.ts","../src/components/helpers/clampCropDims.ts","../src/components/helpers/findMaxRotation.ts","../src/components/helpers/clampOffset.ts","../src/components/ImageCropper.tsx"],"sourcesContent":["import { useCallback, useRef, useState } from 'react';\n\nexport interface HistoryState<T> {\n past: T[];\n present: T;\n future: T[];\n}\n\nexport interface HistoryAPI<T> {\n /** Live display state — updates immediately during interactions */\n state: T;\n /** Last committed state — what undo/redo navigates */\n committed: T;\n canUndo: boolean;\n canRedo: boolean;\n /** Update display state without creating a history entry */\n stage: (newState: T) => void;\n /** Commit staged (or provided) state to history */\n commit: (newState?: T) => void;\n undo: () => void;\n redo: () => void;\n reset: (initialState: T) => void;\n getHistory: () => { past: T[]; present: T; future: T[] };\n}\n\nexport function useHistory<T>(initialState: T): HistoryAPI<T> {\n const [history, setHistory] = useState<HistoryState<T>>({\n past: [],\n present: initialState,\n future: [],\n });\n\n const [staged, setStaged] = useState<T>(initialState);\n // Ref so callbacks always see the latest staged value without being recreated\n const stagedRef = useRef<T>(initialState);\n\n const stage = useCallback((newState: T) => {\n stagedRef.current = newState;\n setStaged(newState);\n }, []);\n\n const commit = useCallback((newState?: T) => {\n const toCommit = newState ?? stagedRef.current;\n if (newState !== undefined) {\n stagedRef.current = newState;\n setStaged(newState);\n }\n setHistory((h) => {\n if (JSON.stringify(toCommit) === JSON.stringify(h.present)) return h;\n return {\n past: [...h.past, h.present],\n present: toCommit,\n future: [],\n };\n });\n }, []);\n\n const undo = useCallback(() => {\n setHistory((prev) => {\n if (prev.past.length === 0) return prev;\n const previous = prev.past[prev.past.length - 1];\n const newPast = prev.past.slice(0, prev.past.length - 1);\n // Sync staged to the restored state\n stagedRef.current = previous;\n setStaged(previous);\n return {\n past: newPast,\n present: previous,\n future: [prev.present, ...prev.future],\n };\n });\n }, []);\n\n const redo = useCallback(() => {\n setHistory((prev) => {\n if (prev.future.length === 0) return prev;\n const next = prev.future[0];\n const newFuture = prev.future.slice(1);\n stagedRef.current = next;\n setStaged(next);\n return {\n past: [...prev.past, prev.present],\n present: next,\n future: newFuture,\n };\n });\n }, []);\n\n const reset = useCallback((initialState: T) => {\n stagedRef.current = initialState;\n setStaged(initialState);\n setHistory({\n past: [],\n present: initialState,\n future: [],\n });\n }, []);\n\n const getHistory = useCallback(\n () => ({\n past: history.past,\n present: history.present,\n future: history.future,\n }),\n [history],\n );\n\n return {\n state: staged,\n committed: history.present,\n canUndo: history.past.length > 0,\n canRedo: history.future.length > 0,\n stage,\n commit,\n undo,\n redo,\n reset,\n getHistory,\n };\n}\n","import type { Size } from '../ImageCropper.types';\n\nexport function computeCropSize(W: number, H: number, rotationDeg: number): Size {\n if (W === 0 || H === 0) return { width: 0, height: 0 };\n const theta = Math.abs(rotationDeg) * (Math.PI / 180);\n if (theta < 1e-9) return { width: W, height: H };\n const cosT = Math.cos(theta);\n const sinT = Math.sin(theta);\n const s = Math.min(W / (W * cosT + H * sinT), H / (W * sinT + H * cosT));\n return { width: s * W, height: s * H };\n}\n","import type { Size } from '../ImageCropper.types';\n\nexport function clampCropDims(\n cropW: number,\n cropH: number,\n imgW: number,\n imgH: number,\n rotationDeg: number,\n minW: number,\n minH: number,\n): Size {\n const theta = Math.abs(rotationDeg) * (Math.PI / 180);\n const cosT = Math.cos(theta);\n const sinT = Math.sin(theta);\n const hiW = imgW / 2;\n const hiH = imgH / 2;\n const mhw = minW / 2;\n const mhh = minH / 2;\n\n let hw = Math.max(cropW / 2, mhw);\n let hh = Math.max(cropH / 2, mhh);\n\n if (theta < 1e-9) {\n hw = Math.min(hw, hiW);\n hh = Math.min(hh, hiH);\n } else {\n let maxHW = Infinity;\n if (cosT > 1e-9) maxHW = Math.min(maxHW, (hiW - hh * sinT) / cosT);\n if (sinT > 1e-9) maxHW = Math.min(maxHW, (hiH - hh * cosT) / sinT);\n\n if (maxHW < mhw) {\n let maxHH2 = Infinity;\n if (sinT > 1e-9) maxHH2 = Math.min(maxHH2, (hiW - mhw * cosT) / sinT);\n if (cosT > 1e-9) maxHH2 = Math.min(maxHH2, (hiH - mhw * sinT) / cosT);\n hh = Math.max(Math.min(hh, maxHH2), mhh);\n hw = mhw;\n } else {\n hw = Math.min(hw, maxHW);\n }\n\n let maxHH = Infinity;\n if (sinT > 1e-9) maxHH = Math.min(maxHH, (hiW - hw * cosT) / sinT);\n if (cosT > 1e-9) maxHH = Math.min(maxHH, (hiH - hw * sinT) / cosT);\n hh = Math.max(Math.min(hh, maxHH), mhh);\n }\n\n return { width: hw * 2, height: hh * 2 };\n}\n","import type { Size } from '../ImageCropper.types';\nimport { clampCropDims } from './clampCropDims';\nimport { computeCropSize } from './computeCropSize';\n\nexport function findMaxRotation(\n imgW: number,\n imgH: number,\n customCrop: Size | null,\n minW: number,\n minH: number,\n): number {\n let lo = 0;\n let hi = 45;\n for (let i = 0; i < 50; i++) {\n const mid = (lo + hi) / 2;\n const c = customCrop\n ? clampCropDims(customCrop.width, customCrop.height, imgW, imgH, mid, minW, minH)\n : computeCropSize(imgW, imgH, mid);\n if (c.width >= minW && c.height >= minH) lo = mid;\n else hi = mid;\n }\n return Math.round(lo * 10) / 10;\n}\n","import type { Point } from '../ImageCropper.types';\n\nexport function clampOffset(\n ox: number,\n oy: number,\n cropW: number,\n cropH: number,\n imgW: number,\n imgH: number,\n rotationDeg: number,\n): Point {\n const theta = rotationDeg * (Math.PI / 180);\n const cosT = Math.cos(theta);\n const sinT = Math.sin(theta);\n const hiW = imgW / 2;\n const hiH = imgH / 2;\n const hw = cropW / 2;\n const hh = cropH / 2;\n\n const corners: [number, number][] = [\n [-hw, -hh],\n [hw, -hh],\n [-hw, hh],\n [hw, hh],\n ];\n\n let cx = ox;\n let cy = oy;\n\n for (let iter = 0; iter < 8; iter++) {\n let ok = true;\n for (const [dx, dy] of corners) {\n const px = cx + dx;\n const py = cy + dy;\n const u = px * cosT + py * sinT;\n const v = -px * sinT + py * cosT;\n\n if (Math.abs(u) > hiW + 0.5) {\n const excess = u > 0 ? u - hiW : u + hiW;\n cx -= excess * cosT;\n cy -= excess * sinT;\n ok = false;\n }\n if (Math.abs(v) > hiH + 0.5) {\n const excess = v > 0 ? v - hiH : v + hiH;\n cx += excess * sinT;\n cy -= excess * cosT;\n ok = false;\n }\n }\n if (ok) break;\n }\n\n return { x: cx, y: cy };\n}\n","import React, { useCallback, useEffect, useMemo, useRef, useState, forwardRef, useImperativeHandle } from 'react';\nimport './ImageCropper.css';\nimport type { CornerPos, DragMode, Size, Point, ImageCropperLabels, ImageCropperProps, CropData, RotationData, ChangeData, CropperState, ImageCropperRef } from './ImageCropper.types';\nimport { useHistory } from '../utils/useHistory';\n\n/* ───────────────────────── Default labels ───────────────────────── */\n\nconst defaultLabels: Required<ImageCropperLabels> = {\n rotation: 'Rotation',\n rotate90: 'Rotate 90°',\n rotate180: 'Rotate 180°',\n resetRotation: 'Reset Rotation',\n resetCrop: 'Reset Crop',\n emptyState: 'Open an image to get started',\n};\n\n/* ───────────────────────── Geometry helpers ─────────────────────── */\n/* helpers moved to ./helpers/*.ts */\nimport { computeCropSize } from './helpers/computeCropSize';\nimport { clampCropDims } from './helpers/clampCropDims';\nimport { findMaxRotation } from './helpers/findMaxRotation';\nimport { clampOffset } from './helpers/clampOffset';\n\n/* ───────────────────────── Component ────────────────────────────── */\n\nexport const ImageCropper = forwardRef<ImageCropperRef, ImageCropperProps>((\n {\n imageSrc: externalImageSrc,\n minCropWidth = 250,\n minCropHeight = 250,\n labels: userLabels,\n onCrop,\n onRotate,\n onChange,\n onHistoryChange,\n },\n ref,\n) => {\n const labels = useMemo<Required<ImageCropperLabels>>(\n () => ({ ...defaultLabels, ...userLabels }),\n [userLabels],\n );\n\n const imageSrc = externalImageSrc;\n const [displaySize, setDisplaySize] = useState<Size>({ width: 0, height: 0 });\n const [drag, setDrag] = useState<DragMode | null>(null);\n\n const history = useHistory<CropperState>({\n rotation: 0,\n baseRotation: 0,\n cropSize: null,\n cropOffset: { x: 0, y: 0 },\n });\n\n const { rotation, baseRotation, cropSize, cropOffset } = history.state;\n\n const commitTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);\n\n const updateState = useCallback(\n (updates: Partial<CropperState>) => {\n history.stage({ ...history.state, ...updates });\n if (commitTimerRef.current) clearTimeout(commitTimerRef.current);\n commitTimerRef.current = setTimeout(() => {\n commitTimerRef.current = null;\n history.commit();\n }, 500);\n },\n [history],\n );\n\n // For discrete actions (buttons) — commit immediately, bypassing the debounce\n const commitNow = useCallback(\n (updates: Partial<CropperState>) => {\n if (commitTimerRef.current) { clearTimeout(commitTimerRef.current); commitTimerRef.current = null; }\n history.commit({ ...history.state, ...updates });\n },\n [history],\n );\n\n const imgRef = useRef<HTMLImageElement>(null);\n const workspaceRef = useRef<HTMLDivElement>(null);\n\n /* ── Track rendered image size ── */\n const syncDisplaySize = useCallback(() => {\n if (!imgRef.current) return;\n const w = imgRef.current.offsetWidth;\n const h = imgRef.current.offsetHeight;\n setDisplaySize((prev) =>\n prev.width === w && prev.height === h ? prev : { width: w, height: h },\n );\n }, []);\n\n useEffect(() => {\n const el = imgRef.current;\n if (!el) return;\n const ro = new ResizeObserver(syncDisplaySize);\n ro.observe(el);\n return () => ro.disconnect();\n }, [imageSrc, syncDisplaySize]);\n\n /* ── Reset when imageSrc changes ── */\n useEffect(() => {\n if (commitTimerRef.current) { clearTimeout(commitTimerRef.current); commitTimerRef.current = null; }\n history.reset({\n rotation: 0,\n baseRotation: 0,\n cropSize: null,\n cropOffset: { x: 0, y: 0 },\n });\n }, [imageSrc]); // eslint-disable-line react-hooks/exhaustive-deps\n\n /* ── Effective image dimensions (swap W/H for 90°/270° base rotation) ── */\n const effectiveDims = useMemo<Size>(() => {\n const swapped = baseRotation === 90 || baseRotation === 270;\n return swapped\n ? { width: displaySize.height, height: displaySize.width }\n : displaySize;\n }, [displaySize, baseRotation]);\n\n /* ── Effective crop dimensions (needed by drag handlers) ── */\n const effectiveCrop = useMemo<Size>(() => {\n if (cropSize)\n return clampCropDims(\n cropSize.width, cropSize.height,\n effectiveDims.width, effectiveDims.height,\n rotation, minCropWidth, minCropHeight,\n );\n return computeCropSize(effectiveDims.width, effectiveDims.height, rotation);\n }, [cropSize, effectiveDims, rotation, minCropWidth, minCropHeight]);\n\n /* ── Start corner resize ── */\n const handleCornerMouseDown = useCallback(\n (corner: CornerPos, e: React.MouseEvent) => {\n e.preventDefault();\n e.stopPropagation();\n setDrag({ kind: 'corner', corner });\n },\n [],\n );\n\n /* ── Start move ── */\n const handleMoveMouseDown = useCallback(\n (e: React.MouseEvent) => {\n e.preventDefault();\n e.stopPropagation();\n setDrag({\n kind: 'move',\n startX: e.clientX,\n startY: e.clientY,\n startOffset: { ...cropOffset },\n });\n },\n [cropOffset],\n );\n\n /* ── Drag effect (resize + move) ── */\n useEffect(() => {\n if (!drag) return;\n\n const cursor =\n drag.kind === 'move'\n ? 'move'\n : ({ tl: 'nw-resize', tr: 'ne-resize', bl: 'sw-resize', br: 'se-resize' } as const)[\n drag.corner\n ];\n\n document.body.style.cursor = cursor;\n document.body.style.userSelect = 'none';\n\n const handleMouseMove = (e: MouseEvent) => {\n const ws = workspaceRef.current;\n if (!ws) return;\n\n if (drag.kind === 'move') {\n const dx = e.clientX - drag.startX;\n const dy = e.clientY - drag.startY;\n const rawX = drag.startOffset.x + dx;\n const rawY = drag.startOffset.y + dy;\n const newOffset = clampOffset(\n rawX, rawY,\n effectiveCrop.width, effectiveCrop.height,\n effectiveDims.width, effectiveDims.height,\n rotation,\n );\n updateState({ cropOffset: newOffset });\n return;\n }\n\n /* Corner resize — resets offset to keep things simple */\n const rect = ws.getBoundingClientRect();\n const cx = rect.left + rect.width / 2;\n const cy = rect.top + rect.height / 2;\n\n let mx = e.clientX - cx;\n let my = e.clientY - cy;\n\n const { corner } = drag;\n if (corner === 'tl' || corner === 'bl') mx = -mx;\n if (corner === 'tl' || corner === 'tr') my = -my;\n\n const desiredW = Math.max(mx * 2, 20);\n const desiredH = Math.max(my * 2, 20);\n\n const newCropSize = clampCropDims(\n desiredW, desiredH,\n effectiveDims.width, effectiveDims.height,\n rotation, minCropWidth, minCropHeight,\n );\n updateState({ cropSize: newCropSize, cropOffset: { x: 0, y: 0 } });\n };\n\n const handleMouseUp = () => setDrag(null);\n\n document.addEventListener('mousemove', handleMouseMove);\n document.addEventListener('mouseup', handleMouseUp);\n return () => {\n document.removeEventListener('mousemove', handleMouseMove);\n document.removeEventListener('mouseup', handleMouseUp);\n document.body.style.cursor = '';\n document.body.style.userSelect = '';\n };\n }, [drag, effectiveDims, rotation, effectiveCrop, minCropWidth, minCropHeight, updateState]);\n\n /* ── Max rotation ── */\n const maxRotation = useMemo(\n () =>\n effectiveDims.width > 0 && effectiveDims.height > 0\n ? findMaxRotation(effectiveDims.width, effectiveDims.height, cropSize, minCropWidth, minCropHeight)\n : 45,\n [effectiveDims, cropSize, minCropWidth, minCropHeight],\n );\n\n useEffect(() => {\n const clamped = Math.max(-maxRotation, Math.min(maxRotation, rotation));\n if (clamped !== rotation) {\n updateState({ rotation: clamped });\n }\n }, [maxRotation, rotation, updateState]);\n\n /* ── Clamped offset ── */\n const effectiveOffset = useMemo<Point>(\n () =>\n clampOffset(\n cropOffset.x, cropOffset.y,\n effectiveCrop.width, effectiveCrop.height,\n effectiveDims.width, effectiveDims.height,\n rotation,\n ),\n [cropOffset, effectiveCrop, effectiveDims, rotation],\n );\n\n /* ── Current crop data ── */\n const cropData = useMemo<CropData>(\n () => ({\n x: effectiveOffset.x - effectiveCrop.width / 2,\n y: effectiveOffset.y - effectiveCrop.height / 2,\n width: effectiveCrop.width,\n height: effectiveCrop.height,\n }),\n [effectiveOffset, effectiveCrop],\n );\n\n const rotationData = useMemo<RotationData>(\n () => ({ rotation, baseRotation }),\n [rotation, baseRotation],\n );\n\n /* ── Emit events on commit (debounced interactions, undo/redo, instant actions) ── */\n const prevCommittedRef = useRef<CropperState | null>(null);\n useEffect(() => {\n if (!imageSrc) return;\n const prev = prevCommittedRef.current;\n prevCommittedRef.current = history.committed;\n const rotChanged =\n !prev ||\n history.committed.rotation !== prev.rotation ||\n history.committed.baseRotation !== prev.baseRotation;\n onCrop?.(cropData);\n onRotate?.(rotationData);\n onChange?.({\n action: rotChanged ? 'rotate' : 'crop',\n crop: cropData,\n rotation: rotationData,\n });\n }, [history.committed]); // eslint-disable-line react-hooks/exhaustive-deps\n\n useEffect(() => {\n onHistoryChange?.(history.canUndo, history.canRedo);\n }, [history.canUndo, history.canRedo, onHistoryChange]);\n\n /* ── Expose ref API ── */\n useImperativeHandle(\n ref,\n () => ({\n undo: () => {\n if (commitTimerRef.current) { clearTimeout(commitTimerRef.current); commitTimerRef.current = null; }\n history.undo();\n },\n redo: () => {\n if (commitTimerRef.current) { clearTimeout(commitTimerRef.current); commitTimerRef.current = null; }\n history.redo();\n },\n canUndo: history.canUndo,\n canRedo: history.canRedo,\n getHistory: history.getHistory,\n }),\n [history],\n );\n\n /* ── Resets ── */\n const resetCrop = useCallback(() => {\n commitNow({ cropSize: null, cropOffset: { x: 0, y: 0 } });\n }, [commitNow]);\n\n const handleRotationChange = useCallback(\n (e: React.ChangeEvent<HTMLInputElement>) => {\n const newRot = parseFloat(e.target.value);\n updateState({ rotation: newRot });\n },\n [updateState],\n );\n\n /* ── 90°/180° rotation ── */\n const can90 = useMemo(() => {\n const newBase = (baseRotation + 90) % 360;\n const swapped = newBase === 90 || newBase === 270;\n const w = swapped ? displaySize.height : displaySize.width;\n const h = swapped ? displaySize.width : displaySize.height;\n return w >= minCropWidth && h >= minCropHeight;\n }, [displaySize, baseRotation, minCropWidth, minCropHeight]);\n\n const handleRotate90 = useCallback(() => {\n commitNow({\n baseRotation: (baseRotation + 90) % 360,\n rotation: 0,\n cropSize: null,\n cropOffset: { x: 0, y: 0 },\n });\n }, [baseRotation, commitNow]);\n\n const handleRotate180 = useCallback(() => {\n commitNow({\n baseRotation: (baseRotation + 180) % 360,\n rotation: 0,\n cropSize: null,\n cropOffset: { x: 0, y: 0 },\n });\n }, [baseRotation, commitNow]);\n\n /* ── Render ── */\n return (\n <div className=\"cropper-container\">\n <div className=\"cropper-workspace\" ref={workspaceRef}>\n {imageSrc ? (\n <>\n <img\n className=\"cropper-image\"\n ref={imgRef}\n src={imageSrc}\n style={{ transform: `rotate(${baseRotation + rotation}deg)` }}\n onLoad={syncDisplaySize}\n alt=\"preview\"\n />\n\n {displaySize.width > 0 && (\n <div\n className=\"cropper-overlay\"\n style={{\n width: effectiveCrop.width,\n height: effectiveCrop.height,\n transform: `translate(calc(-50% + ${effectiveOffset.x}px), calc(-50% + ${effectiveOffset.y}px))`,\n }}\n >\n <div className=\"cropper-move-handle\" onMouseDown={handleMoveMouseDown} />\n <div className=\"cropper-corner cropper-corner--tl\" onMouseDown={(e) => handleCornerMouseDown('tl', e)} />\n <div className=\"cropper-corner cropper-corner--tr\" onMouseDown={(e) => handleCornerMouseDown('tr', e)} />\n <div className=\"cropper-corner cropper-corner--bl\" onMouseDown={(e) => handleCornerMouseDown('bl', e)} />\n <div className=\"cropper-corner cropper-corner--br\" onMouseDown={(e) => handleCornerMouseDown('br', e)} />\n </div>\n )}\n </>\n ) : (\n <div className=\"cropper-empty\">\n <span>{labels.emptyState}</span>\n </div>\n )}\n </div>\n\n {imageSrc && (\n <div className=\"cropper-controls\">\n <div className=\"cropper-label\">{labels.rotation}</div>\n <div className=\"cropper-slider-row\">\n <span className=\"cropper-range-label\">-{maxRotation}°</span>\n <input\n className=\"cropper-slider\"\n type=\"range\"\n min={-maxRotation}\n max={maxRotation}\n step={0.1}\n value={Math.max(-maxRotation, Math.min(maxRotation, rotation))}\n onChange={handleRotationChange}\n />\n <span className=\"cropper-range-label\">{maxRotation}°</span>\n </div>\n <div className=\"cropper-angle\">{rotation.toFixed(1)}°</div>\n <div className=\"cropper-button-row\">\n <button className=\"cropper-button\" onClick={handleRotate90} disabled={!can90}>\n {labels.rotate90}\n </button>\n <button className=\"cropper-button\" onClick={handleRotate180}>\n {labels.rotate180}\n </button>\n </div>\n <div className=\"cropper-button-row\">\n {(rotation !== 0 || baseRotation !== 0) && (\n <button\n className=\"cropper-button\"\n onClick={() => {\n commitNow({\n rotation: 0,\n baseRotation: 0,\n cropSize: null,\n cropOffset: { x: 0, y: 0 },\n });\n }}\n >\n {labels.resetRotation}\n </button>\n )}\n {(cropSize || cropOffset.x !== 0 || cropOffset.y !== 0) && (\n <button className=\"cropper-button\" onClick={resetCrop}>{labels.resetCrop}</button>\n )}\n </div>\n </div>\n )}\n </div>\n );\n});\n\nImageCropper.displayName = 'ImageCropper';\n"],"names":["useHistory","initialState","history","setHistory","useState","staged","setStaged","stagedRef","useRef","stage","useCallback","newState","commit","toCommit","undo","prev","previous","newPast","redo","next","newFuture","reset","getHistory","computeCropSize","W","H","rotationDeg","theta","cosT","sinT","s","clampCropDims","cropW","cropH","imgW","imgH","minW","minH","hiW","hiH","mhw","mhh","hw","hh","maxHW","maxHH2","maxHH","findMaxRotation","customCrop","lo","hi","i","mid","c","clampOffset","ox","oy","corners","cx","cy","iter","ok","dx","dy","px","py","u","v","excess","defaultLabels","ImageCropper","forwardRef","externalImageSrc","minCropWidth","minCropHeight","userLabels","onCrop","onRotate","onChange","onHistoryChange","ref","labels","useMemo","imageSrc","displaySize","setDisplaySize","drag","setDrag","rotation","baseRotation","cropSize","cropOffset","commitTimerRef","updateState","updates","commitNow","imgRef","workspaceRef","syncDisplaySize","w","h","useEffect","el","ro","effectiveDims","effectiveCrop","handleCornerMouseDown","corner","e","handleMoveMouseDown","cursor","handleMouseMove","ws","rawX","rawY","newOffset","rect","mx","my","desiredW","desiredH","newCropSize","handleMouseUp","maxRotation","clamped","effectiveOffset","cropData","rotationData","prevCommittedRef","rotChanged","useImperativeHandle","resetCrop","handleRotationChange","newRot","can90","newBase","swapped","handleRotate90","handleRotate180","jsxs","jsx","Fragment"],"mappings":"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"}
|
package/dist/index.d.ts
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
export { ImageCropper } from './components/ImageCropper';
|
|
2
|
-
export type { ImageCropperProps, Size, Point } from './components/ImageCropper';
|
|
2
|
+
export type { ImageCropperProps, ImageCropperLabels, ImageCropperRef, Size, Point, CropData, RotationData, ChangeData } from './components/ImageCropper.types';
|