@bartgarbiak/image-cropper 1.0.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 +154 -0
- package/dist/components/ImageCropper.d.ts +14 -0
- package/dist/index.cjs +2 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.mjs +266 -0
- package/dist/index.mjs.map +1 -0
- package/dist/style.css +1 -0
- package/package.json +56 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Bartlomiej Garbiak
|
|
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 all
|
|
13
|
+
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 THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
# ImageCropper — Documentation
|
|
2
|
+
|
|
3
|
+
A React component for interactive image rotation and cropping. Built with TypeScript, React 18, styled-components, and Vite.
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## Quick Start
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
npm install
|
|
11
|
+
npm run dev # http://localhost:5173
|
|
12
|
+
npm run build # production build → dist/
|
|
13
|
+
npm run preview # preview production build
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
Requires **Node ≥ 18**.
|
|
17
|
+
|
|
18
|
+
---
|
|
19
|
+
|
|
20
|
+
## Project Structure
|
|
21
|
+
|
|
22
|
+
```
|
|
23
|
+
cropper/
|
|
24
|
+
├── index.html # Entry HTML (loads src/main.tsx)
|
|
25
|
+
├── package.json
|
|
26
|
+
├── tsconfig.json
|
|
27
|
+
├── vite.config.js
|
|
28
|
+
├── docs/
|
|
29
|
+
│ └── README.md # ← you are here
|
|
30
|
+
└── src/
|
|
31
|
+
├── main.tsx # ReactDOM root
|
|
32
|
+
├── App.tsx # Global styles + renders <ImageCropper>
|
|
33
|
+
├── vite-env.d.ts # Vite client type reference
|
|
34
|
+
└── components/
|
|
35
|
+
└── ImageCropper.tsx # Core component (all logic)
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
---
|
|
39
|
+
|
|
40
|
+
## `<ImageCropper>` Component
|
|
41
|
+
|
|
42
|
+
### Props
|
|
43
|
+
|
|
44
|
+
| Prop | Type | Default | Description |
|
|
45
|
+
| --------------- | -------- | ------- | ----------------------------------------------- |
|
|
46
|
+
| `minCropWidth` | `number` | `250` | Minimum crop rectangle width in pixels. |
|
|
47
|
+
| `minCropHeight` | `number` | `250` | Minimum crop rectangle height in pixels. |
|
|
48
|
+
|
|
49
|
+
### Usage
|
|
50
|
+
|
|
51
|
+
```tsx
|
|
52
|
+
import ImageCropper from './components/ImageCropper';
|
|
53
|
+
|
|
54
|
+
<ImageCropper /> // defaults: 250 × 250 min
|
|
55
|
+
<ImageCropper minCropWidth={100} minCropHeight={100} />
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
---
|
|
59
|
+
|
|
60
|
+
## Features
|
|
61
|
+
|
|
62
|
+
### Image Upload
|
|
63
|
+
|
|
64
|
+
Click the file input to load any image. The image is displayed inside a centred workspace area, scaled to fit within 70 % of the available space.
|
|
65
|
+
|
|
66
|
+
### Rotation (−45° to +45°)
|
|
67
|
+
|
|
68
|
+
A slider controls the rotation angle in 0.1° increments. The allowed range shrinks automatically when the current crop dimensions would become smaller than the minimum — the slider limits are recalculated via binary search (`findMaxRotation`).
|
|
69
|
+
|
|
70
|
+
### Crop Overlay
|
|
71
|
+
|
|
72
|
+
An axis-aligned rectangle is drawn on top of the rotated image. At zero rotation and no manual resize it matches the full image size. As rotation increases the crop shrinks to remain inside the image boundary.
|
|
73
|
+
|
|
74
|
+
### Corner Resize (drag)
|
|
75
|
+
|
|
76
|
+
Each corner has a 12 × 12 px hit target. Dragging a corner resizes the crop symmetrically (the crop stays centred when resizing). Dimensions are clamped so:
|
|
77
|
+
|
|
78
|
+
- They never go below `minCropWidth` / `minCropHeight`.
|
|
79
|
+
- All four corners remain inside the rotated image boundary.
|
|
80
|
+
|
|
81
|
+
Resizing resets the crop offset to the centre.
|
|
82
|
+
|
|
83
|
+
### Crop Move (drag)
|
|
84
|
+
|
|
85
|
+
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
|
+
|
|
87
|
+
### Reset Buttons
|
|
88
|
+
|
|
89
|
+
- **Reset Rotation** — sets rotation to 0° and resets crop size + position.
|
|
90
|
+
- **Reset Crop** — restores the default (full-image) crop size and centres it.
|
|
91
|
+
|
|
92
|
+
---
|
|
93
|
+
|
|
94
|
+
## Geometry Model
|
|
95
|
+
|
|
96
|
+
All constraint math operates in a coordinate system centred on the image centre.
|
|
97
|
+
|
|
98
|
+
### Rotated-image containment
|
|
99
|
+
|
|
100
|
+
A screen-space point $(p_x, p_y)$ is inside the rotated image iff:
|
|
101
|
+
|
|
102
|
+
$$
|
|
103
|
+
|p_x \cos\theta + p_y \sin\theta| \le \frac{W}{2}
|
|
104
|
+
\quad\text{and}\quad
|
|
105
|
+
|-p_x \sin\theta + p_y \cos\theta| \le \frac{H}{2}
|
|
106
|
+
$$
|
|
107
|
+
|
|
108
|
+
where $W \times H$ is the displayed image size and $\theta$ is the rotation angle.
|
|
109
|
+
|
|
110
|
+
### Key functions
|
|
111
|
+
|
|
112
|
+
| Function | Purpose |
|
|
113
|
+
| --- | --- |
|
|
114
|
+
| `computeCropSize(W, H, θ)` | Largest axis-aligned rectangle with the image's aspect ratio that fits inside the rotated image (centred). |
|
|
115
|
+
| `clampCropDims(cW, cH, iW, iH, θ, minW, minH)` | Clamp arbitrary crop dimensions so a centred crop of that size fits inside the rotated image, respecting minimums. |
|
|
116
|
+
| `findMaxRotation(iW, iH, crop, minW, minH)` | Binary search (50 iterations) for the largest $|\theta| \le 45°$ where the effective crop still meets the minimum size constraints. |
|
|
117
|
+
| `clampOffset(ox, oy, cW, cH, iW, iH, θ)` | Iteratively push an offset $(o_x, o_y)$ inward until all four corners of the offset crop rectangle pass the containment test above. |
|
|
118
|
+
|
|
119
|
+
---
|
|
120
|
+
|
|
121
|
+
## Types
|
|
122
|
+
|
|
123
|
+
```ts
|
|
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 };
|
|
129
|
+
|
|
130
|
+
interface Props {
|
|
131
|
+
minCropWidth?: number;
|
|
132
|
+
minCropHeight?: number;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
interface Size { width: number; height: number; }
|
|
136
|
+
interface Point { x: number; y: number; }
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
---
|
|
140
|
+
|
|
141
|
+
## Tech Stack
|
|
142
|
+
|
|
143
|
+
| Layer | Library |
|
|
144
|
+
| ----------- | ---------------------- |
|
|
145
|
+
| UI | React 18 |
|
|
146
|
+
| Styling | styled-components 6 |
|
|
147
|
+
| Language | TypeScript 5 (strict) |
|
|
148
|
+
| Bundler | Vite 4 |
|
|
149
|
+
|
|
150
|
+
---
|
|
151
|
+
|
|
152
|
+
## License
|
|
153
|
+
|
|
154
|
+
[MIT](LICENSE)
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import './ImageCropper.css';
|
|
2
|
+
export interface ImageCropperProps {
|
|
3
|
+
minCropWidth?: number;
|
|
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;
|
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
"use strict";Object.defineProperty(exports,Symbol.toStringTag,{value:"Module"});const n=require("react/jsx-runtime"),l=require("react");function X(a,i,u){if(a===0||i===0)return{width:0,height:0};const w=Math.abs(u)*(Math.PI/180);if(w<1e-9)return{width:a,height:i};const o=Math.cos(w),x=Math.sin(w),t=Math.min(a/(a*o+i*x),i/(a*x+i*o));return{width:t*a,height:t*i}}function T(a,i,u,w,o,x,t){const j=Math.abs(o)*(Math.PI/180),s=Math.cos(j),r=Math.sin(j),p=u/2,M=w/2,c=x/2,v=t/2;let f=Math.max(a/2,c),d=Math.max(i/2,v);if(j<1e-9)f=Math.min(f,p),d=Math.min(d,M);else{let g=1/0;if(s>1e-9&&(g=Math.min(g,(p-d*r)/s)),r>1e-9&&(g=Math.min(g,(M-d*s)/r)),g<c){let h=1/0;r>1e-9&&(h=Math.min(h,(p-c*s)/r)),s>1e-9&&(h=Math.min(h,(M-c*r)/s)),d=Math.max(Math.min(d,h),v),f=c}else f=Math.min(f,g);let y=1/0;r>1e-9&&(y=Math.min(y,(p-f*s)/r)),s>1e-9&&(y=Math.min(y,(M-f*r)/s)),d=Math.max(Math.min(d,y),v)}return{width:f*2,height:d*2}}function K(a,i,u,w,o){let x=0,t=45;for(let j=0;j<50;j++){const s=(x+t)/2,r=u?T(u.width,u.height,a,i,s,w,o):X(a,i,s);r.width>=w&&r.height>=o?x=s:t=s}return Math.round(x*10)/10}function U(a,i,u,w,o,x,t){const j=t*(Math.PI/180),s=Math.cos(j),r=Math.sin(j),p=o/2,M=x/2,c=u/2,v=w/2,f=[[-c,-v],[c,-v],[-c,v],[c,v]];let d=a,g=i;for(let y=0;y<8;y++){let h=!0;for(const[R,I]of f){const b=d+R,D=g+I,S=b*s+D*r,k=-b*r+D*s;if(Math.abs(S)>p+.5){const e=S>0?S-p:S+p;d-=e*s,g-=e*r,h=!1}if(Math.abs(k)>M+.5){const e=k>0?k-M:k+M;d+=e*r,g-=e*s,h=!1}}if(h)break}return{x:d,y:g}}function Q({minCropWidth:a=250,minCropHeight:i=250}){const[u,w]=l.useState(null),[o,x]=l.useState(0),[t,j]=l.useState({width:0,height:0}),[s,r]=l.useState(null),[p,M]=l.useState({x:0,y:0}),[c,v]=l.useState(null),f=l.useRef(null),d=l.useRef(null),g=l.useCallback(e=>{var N;const m=(N=e.target.files)==null?void 0:N[0];m&&(u&&URL.revokeObjectURL(u),w(URL.createObjectURL(m)),x(0),r(null),M({x:0,y:0}))},[u]),y=l.useCallback(()=>{if(!f.current)return;const e=f.current.offsetWidth,m=f.current.offsetHeight;j(N=>N.width===e&&N.height===m?N:{width:e,height:m})},[]);l.useEffect(()=>{const e=f.current;if(!e)return;const m=new ResizeObserver(y);return m.observe(e),()=>m.disconnect()},[u,y]);const h=l.useMemo(()=>s?T(s.width,s.height,t.width,t.height,o,a,i):X(t.width,t.height,o),[s,t,o,a,i]),R=l.useCallback((e,m)=>{m.preventDefault(),m.stopPropagation(),v({kind:"corner",corner:e})},[]),I=l.useCallback(e=>{e.preventDefault(),e.stopPropagation(),v({kind:"move",startX:e.clientX,startY:e.clientY,startOffset:{...p}})},[p]);l.useEffect(()=>{if(!c)return;const e=c.kind==="move"?"move":{tl:"nw-resize",tr:"ne-resize",bl:"sw-resize",br:"se-resize"}[c.corner];document.body.style.cursor=e,document.body.style.userSelect="none";const m=O=>{const P=d.current;if(!P)return;if(c.kind==="move"){const B=O.clientX-c.startX,A=O.clientY-c.startY,G=c.startOffset.x+B,J=c.startOffset.y+A;M(U(G,J,h.width,h.height,t.width,t.height,o));return}const z=P.getBoundingClientRect(),Y=z.left+z.width/2,$=z.top+z.height/2;let L=O.clientX-Y,E=O.clientY-$;const{corner:C}=c;(C==="tl"||C==="bl")&&(L=-L),(C==="tl"||C==="tr")&&(E=-E);const F=Math.max(L*2,20),q=Math.max(E*2,20);r(T(F,q,t.width,t.height,o,a,i)),M({x:0,y:0})},N=()=>v(null);return document.addEventListener("mousemove",m),document.addEventListener("mouseup",N),()=>{document.removeEventListener("mousemove",m),document.removeEventListener("mouseup",N),document.body.style.cursor="",document.body.style.userSelect=""}},[c,t,o,h,a,i]);const b=l.useMemo(()=>t.width>0&&t.height>0?K(t.width,t.height,s,a,i):45,[t,s,a,i]);l.useEffect(()=>{x(e=>Math.max(-b,Math.min(b,e)))},[b]);const D=l.useMemo(()=>U(p.x,p.y,h.width,h.height,t.width,t.height,o),[p,h,t,o]),S=l.useCallback(()=>{r(null),M({x:0,y:0})},[]),k=l.useCallback(e=>{const m=parseFloat(e.target.value);x(m)},[]);return n.jsxs("div",{className:"cropper-container",children:[n.jsx("input",{type:"file",accept:"image/*",onChange:g}),n.jsx("div",{className:"cropper-workspace",ref:d,children:u?n.jsxs(n.Fragment,{children:[n.jsx("img",{className:"cropper-image",ref:f,src:u,style:{transform:`rotate(${o}deg)`},onLoad:y,alt:"preview"}),t.width>0&&n.jsxs("div",{className:"cropper-overlay",style:{width:h.width,height:h.height,transform:`translate(calc(-50% + ${D.x}px), calc(-50% + ${D.y}px))`},children:[n.jsx("div",{className:"cropper-move-handle",onMouseDown:I}),n.jsx("div",{className:"cropper-corner cropper-corner--tl",onMouseDown:e=>R("tl",e)}),n.jsx("div",{className:"cropper-corner cropper-corner--tr",onMouseDown:e=>R("tr",e)}),n.jsx("div",{className:"cropper-corner cropper-corner--bl",onMouseDown:e=>R("bl",e)}),n.jsx("div",{className:"cropper-corner cropper-corner--br",onMouseDown:e=>R("br",e)})]})]}):n.jsx("div",{className:"cropper-empty",children:n.jsx("span",{children:"Open an image to get started"})})}),u&&n.jsxs("div",{className:"cropper-controls",children:[n.jsx("div",{className:"cropper-label",children:"Rotation"}),n.jsxs("div",{className:"cropper-slider-row",children:[n.jsxs("span",{className:"cropper-range-label",children:["-",b,"°"]}),n.jsx("input",{className:"cropper-slider",type:"range",min:-b,max:b,step:.1,value:Math.max(-b,Math.min(b,o)),onChange:k}),n.jsxs("span",{className:"cropper-range-label",children:[b,"°"]})]}),n.jsxs("div",{className:"cropper-angle",children:[o.toFixed(1),"°"]}),n.jsxs("div",{className:"cropper-button-row",children:[o!==0&&n.jsx("button",{className:"cropper-button",onClick:()=>{x(0),r(null),M({x:0,y:0})},children:"Reset Rotation"}),(s||p.x!==0||p.y!==0)&&n.jsx("button",{className:"cropper-button",onClick:S,children:"Reset Crop"})]})]})]})}exports.ImageCropper=Q;
|
|
2
|
+
//# sourceMappingURL=index.cjs.map
|
|
@@ -0,0 +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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"}
|
package/dist/index.d.ts
ADDED
package/dist/index.mjs
ADDED
|
@@ -0,0 +1,266 @@
|
|
|
1
|
+
import { jsxs as R, jsx as M, Fragment as H } from "react/jsx-runtime";
|
|
2
|
+
import { useState as O, useRef as F, useCallback as S, useEffect as E, useMemo as P } from "react";
|
|
3
|
+
function q(c, a, l) {
|
|
4
|
+
if (c === 0 || a === 0)
|
|
5
|
+
return { width: 0, height: 0 };
|
|
6
|
+
const g = Math.abs(l) * (Math.PI / 180);
|
|
7
|
+
if (g < 1e-9)
|
|
8
|
+
return { width: c, height: a };
|
|
9
|
+
const o = Math.cos(g), m = Math.sin(g), t = Math.min(c / (c * o + a * m), a / (c * m + a * o));
|
|
10
|
+
return { width: t * c, height: t * a };
|
|
11
|
+
}
|
|
12
|
+
function j(c, a, l, g, o, m, t) {
|
|
13
|
+
const b = Math.abs(o) * (Math.PI / 180), n = Math.cos(b), s = Math.sin(b), d = l / 2, f = g / 2, r = m / 2, v = t / 2;
|
|
14
|
+
let p = Math.max(c / 2, r), h = Math.max(a / 2, v);
|
|
15
|
+
if (b < 1e-9)
|
|
16
|
+
p = Math.min(p, d), h = Math.min(h, f);
|
|
17
|
+
else {
|
|
18
|
+
let w = 1 / 0;
|
|
19
|
+
if (n > 1e-9 && (w = Math.min(w, (d - h * s) / n)), s > 1e-9 && (w = Math.min(w, (f - h * n) / s)), w < r) {
|
|
20
|
+
let i = 1 / 0;
|
|
21
|
+
s > 1e-9 && (i = Math.min(i, (d - r * n) / s)), n > 1e-9 && (i = Math.min(i, (f - r * s) / n)), h = Math.max(Math.min(h, i), v), p = r;
|
|
22
|
+
} else
|
|
23
|
+
p = Math.min(p, w);
|
|
24
|
+
let x = 1 / 0;
|
|
25
|
+
s > 1e-9 && (x = Math.min(x, (d - p * n) / s)), n > 1e-9 && (x = Math.min(x, (f - p * s) / n)), h = Math.max(Math.min(h, x), v);
|
|
26
|
+
}
|
|
27
|
+
return { width: p * 2, height: h * 2 };
|
|
28
|
+
}
|
|
29
|
+
function W(c, a, l, g, o) {
|
|
30
|
+
let m = 0, t = 45;
|
|
31
|
+
for (let b = 0; b < 50; b++) {
|
|
32
|
+
const n = (m + t) / 2, s = l ? j(l.width, l.height, c, a, n, g, o) : q(c, a, n);
|
|
33
|
+
s.width >= g && s.height >= o ? m = n : t = n;
|
|
34
|
+
}
|
|
35
|
+
return Math.round(m * 10) / 10;
|
|
36
|
+
}
|
|
37
|
+
function B(c, a, l, g, o, m, t) {
|
|
38
|
+
const b = t * (Math.PI / 180), n = Math.cos(b), s = Math.sin(b), d = o / 2, f = m / 2, r = l / 2, v = g / 2, p = [
|
|
39
|
+
[-r, -v],
|
|
40
|
+
[r, -v],
|
|
41
|
+
[-r, v],
|
|
42
|
+
[r, v]
|
|
43
|
+
];
|
|
44
|
+
let h = c, w = a;
|
|
45
|
+
for (let x = 0; x < 8; x++) {
|
|
46
|
+
let i = !0;
|
|
47
|
+
for (const [D, X] of p) {
|
|
48
|
+
const y = h + D, I = w + X, k = y * n + I * s, z = -y * s + I * n;
|
|
49
|
+
if (Math.abs(k) > d + 0.5) {
|
|
50
|
+
const e = k > 0 ? k - d : k + d;
|
|
51
|
+
h -= e * n, w -= e * s, i = !1;
|
|
52
|
+
}
|
|
53
|
+
if (Math.abs(z) > f + 0.5) {
|
|
54
|
+
const e = z > 0 ? z - f : z + f;
|
|
55
|
+
h += e * s, w -= e * n, i = !1;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
if (i)
|
|
59
|
+
break;
|
|
60
|
+
}
|
|
61
|
+
return { x: h, y: w };
|
|
62
|
+
}
|
|
63
|
+
function ne({
|
|
64
|
+
minCropWidth: c = 250,
|
|
65
|
+
minCropHeight: a = 250
|
|
66
|
+
}) {
|
|
67
|
+
const [l, g] = O(null), [o, m] = O(0), [t, b] = O({ width: 0, height: 0 }), [n, s] = O(null), [d, f] = O({ x: 0, y: 0 }), [r, v] = O(null), p = F(null), h = F(null), w = S(
|
|
68
|
+
(e) => {
|
|
69
|
+
var N;
|
|
70
|
+
const u = (N = e.target.files) == null ? void 0 : N[0];
|
|
71
|
+
u && (l && URL.revokeObjectURL(l), g(URL.createObjectURL(u)), m(0), s(null), f({ x: 0, y: 0 }));
|
|
72
|
+
},
|
|
73
|
+
[l]
|
|
74
|
+
), x = S(() => {
|
|
75
|
+
if (!p.current)
|
|
76
|
+
return;
|
|
77
|
+
const e = p.current.offsetWidth, u = p.current.offsetHeight;
|
|
78
|
+
b(
|
|
79
|
+
(N) => N.width === e && N.height === u ? N : { width: e, height: u }
|
|
80
|
+
);
|
|
81
|
+
}, []);
|
|
82
|
+
E(() => {
|
|
83
|
+
const e = p.current;
|
|
84
|
+
if (!e)
|
|
85
|
+
return;
|
|
86
|
+
const u = new ResizeObserver(x);
|
|
87
|
+
return u.observe(e), () => u.disconnect();
|
|
88
|
+
}, [l, x]);
|
|
89
|
+
const i = P(() => n ? j(
|
|
90
|
+
n.width,
|
|
91
|
+
n.height,
|
|
92
|
+
t.width,
|
|
93
|
+
t.height,
|
|
94
|
+
o,
|
|
95
|
+
c,
|
|
96
|
+
a
|
|
97
|
+
) : q(t.width, t.height, o), [n, t, o, c, a]), D = S(
|
|
98
|
+
(e, u) => {
|
|
99
|
+
u.preventDefault(), u.stopPropagation(), v({ kind: "corner", corner: e });
|
|
100
|
+
},
|
|
101
|
+
[]
|
|
102
|
+
), X = S(
|
|
103
|
+
(e) => {
|
|
104
|
+
e.preventDefault(), e.stopPropagation(), v({
|
|
105
|
+
kind: "move",
|
|
106
|
+
startX: e.clientX,
|
|
107
|
+
startY: e.clientY,
|
|
108
|
+
startOffset: { ...d }
|
|
109
|
+
});
|
|
110
|
+
},
|
|
111
|
+
[d]
|
|
112
|
+
);
|
|
113
|
+
E(() => {
|
|
114
|
+
if (!r)
|
|
115
|
+
return;
|
|
116
|
+
const e = r.kind === "move" ? "move" : { tl: "nw-resize", tr: "ne-resize", bl: "sw-resize", br: "se-resize" }[r.corner];
|
|
117
|
+
document.body.style.cursor = e, document.body.style.userSelect = "none";
|
|
118
|
+
const u = (L) => {
|
|
119
|
+
const $ = h.current;
|
|
120
|
+
if (!$)
|
|
121
|
+
return;
|
|
122
|
+
if (r.kind === "move") {
|
|
123
|
+
const Q = L.clientX - r.startX, V = L.clientY - r.startY, Z = r.startOffset.x + Q, _ = r.startOffset.y + V;
|
|
124
|
+
f(
|
|
125
|
+
B(
|
|
126
|
+
Z,
|
|
127
|
+
_,
|
|
128
|
+
i.width,
|
|
129
|
+
i.height,
|
|
130
|
+
t.width,
|
|
131
|
+
t.height,
|
|
132
|
+
o
|
|
133
|
+
)
|
|
134
|
+
);
|
|
135
|
+
return;
|
|
136
|
+
}
|
|
137
|
+
const T = $.getBoundingClientRect(), A = T.left + T.width / 2, G = T.top + T.height / 2;
|
|
138
|
+
let Y = L.clientX - A, C = L.clientY - G;
|
|
139
|
+
const { corner: U } = r;
|
|
140
|
+
(U === "tl" || U === "bl") && (Y = -Y), (U === "tl" || U === "tr") && (C = -C);
|
|
141
|
+
const J = Math.max(Y * 2, 20), K = Math.max(C * 2, 20);
|
|
142
|
+
s(
|
|
143
|
+
j(
|
|
144
|
+
J,
|
|
145
|
+
K,
|
|
146
|
+
t.width,
|
|
147
|
+
t.height,
|
|
148
|
+
o,
|
|
149
|
+
c,
|
|
150
|
+
a
|
|
151
|
+
)
|
|
152
|
+
), f({ x: 0, y: 0 });
|
|
153
|
+
}, N = () => v(null);
|
|
154
|
+
return document.addEventListener("mousemove", u), document.addEventListener("mouseup", N), () => {
|
|
155
|
+
document.removeEventListener("mousemove", u), document.removeEventListener("mouseup", N), document.body.style.cursor = "", document.body.style.userSelect = "";
|
|
156
|
+
};
|
|
157
|
+
}, [r, t, o, i, c, a]);
|
|
158
|
+
const y = P(
|
|
159
|
+
() => t.width > 0 && t.height > 0 ? W(t.width, t.height, n, c, a) : 45,
|
|
160
|
+
[t, n, c, a]
|
|
161
|
+
);
|
|
162
|
+
E(() => {
|
|
163
|
+
m((e) => Math.max(-y, Math.min(y, e)));
|
|
164
|
+
}, [y]);
|
|
165
|
+
const I = P(
|
|
166
|
+
() => B(
|
|
167
|
+
d.x,
|
|
168
|
+
d.y,
|
|
169
|
+
i.width,
|
|
170
|
+
i.height,
|
|
171
|
+
t.width,
|
|
172
|
+
t.height,
|
|
173
|
+
o
|
|
174
|
+
),
|
|
175
|
+
[d, i, t, o]
|
|
176
|
+
), k = S(() => {
|
|
177
|
+
s(null), f({ x: 0, y: 0 });
|
|
178
|
+
}, []), z = S(
|
|
179
|
+
(e) => {
|
|
180
|
+
const u = parseFloat(e.target.value);
|
|
181
|
+
m(u);
|
|
182
|
+
},
|
|
183
|
+
[]
|
|
184
|
+
);
|
|
185
|
+
return /* @__PURE__ */ R("div", { className: "cropper-container", children: [
|
|
186
|
+
/* @__PURE__ */ M("input", { type: "file", accept: "image/*", onChange: w }),
|
|
187
|
+
/* @__PURE__ */ M("div", { className: "cropper-workspace", ref: h, children: l ? /* @__PURE__ */ R(H, { children: [
|
|
188
|
+
/* @__PURE__ */ M(
|
|
189
|
+
"img",
|
|
190
|
+
{
|
|
191
|
+
className: "cropper-image",
|
|
192
|
+
ref: p,
|
|
193
|
+
src: l,
|
|
194
|
+
style: { transform: `rotate(${o}deg)` },
|
|
195
|
+
onLoad: x,
|
|
196
|
+
alt: "preview"
|
|
197
|
+
}
|
|
198
|
+
),
|
|
199
|
+
t.width > 0 && /* @__PURE__ */ R(
|
|
200
|
+
"div",
|
|
201
|
+
{
|
|
202
|
+
className: "cropper-overlay",
|
|
203
|
+
style: {
|
|
204
|
+
width: i.width,
|
|
205
|
+
height: i.height,
|
|
206
|
+
transform: `translate(calc(-50% + ${I.x}px), calc(-50% + ${I.y}px))`
|
|
207
|
+
},
|
|
208
|
+
children: [
|
|
209
|
+
/* @__PURE__ */ M("div", { className: "cropper-move-handle", onMouseDown: X }),
|
|
210
|
+
/* @__PURE__ */ M("div", { className: "cropper-corner cropper-corner--tl", onMouseDown: (e) => D("tl", e) }),
|
|
211
|
+
/* @__PURE__ */ M("div", { className: "cropper-corner cropper-corner--tr", onMouseDown: (e) => D("tr", e) }),
|
|
212
|
+
/* @__PURE__ */ M("div", { className: "cropper-corner cropper-corner--bl", onMouseDown: (e) => D("bl", e) }),
|
|
213
|
+
/* @__PURE__ */ M("div", { className: "cropper-corner cropper-corner--br", onMouseDown: (e) => D("br", e) })
|
|
214
|
+
]
|
|
215
|
+
}
|
|
216
|
+
)
|
|
217
|
+
] }) : /* @__PURE__ */ M("div", { className: "cropper-empty", children: /* @__PURE__ */ M("span", { children: "Open an image to get started" }) }) }),
|
|
218
|
+
l && /* @__PURE__ */ R("div", { className: "cropper-controls", children: [
|
|
219
|
+
/* @__PURE__ */ M("div", { className: "cropper-label", children: "Rotation" }),
|
|
220
|
+
/* @__PURE__ */ R("div", { className: "cropper-slider-row", children: [
|
|
221
|
+
/* @__PURE__ */ R("span", { className: "cropper-range-label", children: [
|
|
222
|
+
"-",
|
|
223
|
+
y,
|
|
224
|
+
"°"
|
|
225
|
+
] }),
|
|
226
|
+
/* @__PURE__ */ M(
|
|
227
|
+
"input",
|
|
228
|
+
{
|
|
229
|
+
className: "cropper-slider",
|
|
230
|
+
type: "range",
|
|
231
|
+
min: -y,
|
|
232
|
+
max: y,
|
|
233
|
+
step: 0.1,
|
|
234
|
+
value: Math.max(-y, Math.min(y, o)),
|
|
235
|
+
onChange: z
|
|
236
|
+
}
|
|
237
|
+
),
|
|
238
|
+
/* @__PURE__ */ R("span", { className: "cropper-range-label", children: [
|
|
239
|
+
y,
|
|
240
|
+
"°"
|
|
241
|
+
] })
|
|
242
|
+
] }),
|
|
243
|
+
/* @__PURE__ */ R("div", { className: "cropper-angle", children: [
|
|
244
|
+
o.toFixed(1),
|
|
245
|
+
"°"
|
|
246
|
+
] }),
|
|
247
|
+
/* @__PURE__ */ R("div", { className: "cropper-button-row", children: [
|
|
248
|
+
o !== 0 && /* @__PURE__ */ M(
|
|
249
|
+
"button",
|
|
250
|
+
{
|
|
251
|
+
className: "cropper-button",
|
|
252
|
+
onClick: () => {
|
|
253
|
+
m(0), s(null), f({ x: 0, y: 0 });
|
|
254
|
+
},
|
|
255
|
+
children: "Reset Rotation"
|
|
256
|
+
}
|
|
257
|
+
),
|
|
258
|
+
(n || d.x !== 0 || d.y !== 0) && /* @__PURE__ */ M("button", { className: "cropper-button", onClick: k, children: "Reset Crop" })
|
|
259
|
+
] })
|
|
260
|
+
] })
|
|
261
|
+
] });
|
|
262
|
+
}
|
|
263
|
+
export {
|
|
264
|
+
ne as ImageCropper
|
|
265
|
+
};
|
|
266
|
+
//# sourceMappingURL=index.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.mjs","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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"}
|
package/dist/style.css
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
:root{--cropper-border-color: #000;--cropper-overlay-color: rgba(0, 0, 0, .5);--cropper-corner-border-color: #000;--cropper-corner-bg: transparent;--cropper-text-color: inherit;--cropper-label-size: .8rem;--cropper-angle-size: 1.5rem;--cropper-corner-size: 12px}.cropper-container{display:flex;flex-direction:column;align-items:center;min-height:100vh;padding:16px;gap:16px}.cropper-workspace{position:relative;width:100%;max-width:900px;height:65vh;overflow:hidden;display:flex;justify-content:center;align-items:center}.cropper-image{display:block;max-width:70%;max-height:70%;-webkit-user-select:none;user-select:none;-webkit-user-drag:none}.cropper-overlay{position:absolute;left:50%;top:50%;border:2px solid var(--cropper-border-color);box-shadow:0 0 0 9999px var(--cropper-overlay-color);pointer-events:none;z-index:2}.cropper-move-handle{position:absolute;top:0;right:0;bottom:0;left:0;cursor:move;pointer-events:auto;z-index:2}.cropper-corner{position:absolute;width:var(--cropper-corner-size);height:var(--cropper-corner-size);border:2px solid var(--cropper-corner-border-color);background:var(--cropper-corner-bg);pointer-events:auto;z-index:3}.cropper-corner--tl{top:-6px;left:-6px;cursor:nw-resize}.cropper-corner--tr{top:-6px;right:-6px;cursor:ne-resize}.cropper-corner--bl{bottom:-6px;left:-6px;cursor:sw-resize}.cropper-corner--br{bottom:-6px;right:-6px;cursor:se-resize}.cropper-controls{display:flex;flex-direction:column;align-items:center;gap:12px;width:100%;max-width:500px;padding:12px 16px}.cropper-label{font-size:var(--cropper-label-size);text-transform:uppercase;letter-spacing:1.2px;color:var(--cropper-text-color)}.cropper-slider-row{display:flex;align-items:center;gap:12px;width:100%}.cropper-range-label{font-size:var(--cropper-label-size);min-width:36px;text-align:center;color:var(--cropper-text-color)}.cropper-slider{flex:1}.cropper-angle{font-size:var(--cropper-angle-size);font-weight:600;color:var(--cropper-text-color)}.cropper-button-row{display:flex;gap:10px}.cropper-button{padding:6px 12px;cursor:pointer}.cropper-empty{display:flex;flex-direction:column;align-items:center;gap:14px;font-size:.95rem;color:var(--cropper-text-color)}
|
package/package.json
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@bartgarbiak/image-cropper",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"license": "MIT",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"description": "A React component for interactive image rotation and cropping",
|
|
7
|
+
"keywords": ["react", "image", "cropper", "rotation", "crop"],
|
|
8
|
+
"repository": {
|
|
9
|
+
"type": "git",
|
|
10
|
+
"url": "git+https://github.com/bartgarbiak/image-cropper.git"
|
|
11
|
+
},
|
|
12
|
+
"main": "dist/index.cjs",
|
|
13
|
+
"module": "dist/index.mjs",
|
|
14
|
+
"types": "dist/index.d.ts",
|
|
15
|
+
"exports": {
|
|
16
|
+
".": {
|
|
17
|
+
"import": {
|
|
18
|
+
"types": "./dist/index.d.ts",
|
|
19
|
+
"default": "./dist/index.mjs"
|
|
20
|
+
},
|
|
21
|
+
"require": {
|
|
22
|
+
"types": "./dist/index.d.ts",
|
|
23
|
+
"default": "./dist/index.cjs"
|
|
24
|
+
}
|
|
25
|
+
},
|
|
26
|
+
"./style.css": "./dist/style.css"
|
|
27
|
+
},
|
|
28
|
+
"files": [
|
|
29
|
+
"dist",
|
|
30
|
+
"README.md",
|
|
31
|
+
"LICENSE"
|
|
32
|
+
],
|
|
33
|
+
"sideEffects": [
|
|
34
|
+
"**/*.css"
|
|
35
|
+
],
|
|
36
|
+
"scripts": {
|
|
37
|
+
"dev": "vite serve demo",
|
|
38
|
+
"build": "vite build && tsc -p tsconfig.build.json",
|
|
39
|
+
"preview": "vite preview",
|
|
40
|
+
"typecheck": "tsc --noEmit",
|
|
41
|
+
"prepublishOnly": "npm run build"
|
|
42
|
+
},
|
|
43
|
+
"peerDependencies": {
|
|
44
|
+
"react": ">=17.0.0",
|
|
45
|
+
"react-dom": ">=17.0.0"
|
|
46
|
+
},
|
|
47
|
+
"devDependencies": {
|
|
48
|
+
"@types/react": "^19.2.14",
|
|
49
|
+
"@types/react-dom": "^19.2.3",
|
|
50
|
+
"@vitejs/plugin-react": "^3.1.0",
|
|
51
|
+
"react": "^18.2.0",
|
|
52
|
+
"react-dom": "^18.2.0",
|
|
53
|
+
"typescript": "^5.9.3",
|
|
54
|
+
"vite": "^4.5.0"
|
|
55
|
+
}
|
|
56
|
+
}
|