@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 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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"}
@@ -0,0 +1,2 @@
1
+ export { ImageCropper } from './components/ImageCropper';
2
+ export type { ImageCropperProps, Size, Point } from './components/ImageCropper';
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
+ }