@skalfa/skalfa-component 1.0.52 → 1.0.53
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.
|
@@ -17,6 +17,8 @@ export declare const InputImageComponent: React.FC<InputImageProps>;
|
|
|
17
17
|
interface CropperProps {
|
|
18
18
|
src: string;
|
|
19
19
|
aspect: number;
|
|
20
|
+
fileName?: string;
|
|
21
|
+
fileType?: string;
|
|
20
22
|
onDone?: (file: File) => void;
|
|
21
23
|
}
|
|
22
24
|
export declare const CanvasCropper: React.FC<CropperProps>;
|
|
@@ -16,6 +16,7 @@ const InputImageComponent = ({ name, label, tip, value, disabled, aspect, invali
|
|
|
16
16
|
const [preview, setPreview] = (0, react_1.useState)("");
|
|
17
17
|
const [drag, setDrag] = (0, react_1.useState)(false);
|
|
18
18
|
const [cropSrc, setCropSrc] = (0, react_1.useState)(null);
|
|
19
|
+
const [cropFile, setCropFile] = (0, react_1.useState)(null);
|
|
19
20
|
const [openCrop, setOpenCrop] = (0, react_1.useState)(false);
|
|
20
21
|
const inputHandler = (0, _utils_1.useInputHandler)(name, value, validations, register, true, unregister);
|
|
21
22
|
const [invalidMessage, setInvalidMessage] = (0, _utils_1.useValidation)(inputHandler.value, validations, invalid, inputHandler.idle);
|
|
@@ -35,6 +36,7 @@ const InputImageComponent = ({ name, label, tip, value, disabled, aspect, invali
|
|
|
35
36
|
}
|
|
36
37
|
}, [value]);
|
|
37
38
|
const openCropper = (file) => {
|
|
39
|
+
setCropFile(file);
|
|
38
40
|
const reader = new FileReader();
|
|
39
41
|
reader.onload = () => {
|
|
40
42
|
setCropSrc(reader.result);
|
|
@@ -68,10 +70,14 @@ const InputImageComponent = ({ name, label, tip, value, disabled, aspect, invali
|
|
|
68
70
|
};
|
|
69
71
|
const onCropCancel = () => {
|
|
70
72
|
setOpenCrop(false);
|
|
73
|
+
setCropFile(null);
|
|
74
|
+
setCropSrc(null);
|
|
71
75
|
inputRef.current.value = "";
|
|
72
76
|
};
|
|
73
77
|
const remove = () => {
|
|
74
78
|
setPreview("");
|
|
79
|
+
setCropFile(null);
|
|
80
|
+
setCropSrc(null);
|
|
75
81
|
onChange?.(null);
|
|
76
82
|
inputRef.current && (inputRef.current.value = "");
|
|
77
83
|
};
|
|
@@ -99,12 +105,11 @@ const InputImageComponent = ({ name, label, tip, value, disabled, aspect, invali
|
|
|
99
105
|
}, onDragOver: (e) => e.preventDefault(), onDragEnter: (e) => {
|
|
100
106
|
e.preventDefault();
|
|
101
107
|
setDrag(true);
|
|
102
|
-
}, onDragLeave: () => setDrag(false), onDrop: onDropFile, children: [preview && !containerAspect ? ((0, jsx_runtime_1.jsxs)("div", { className: "relative group w-full h-full flex items-center justify-center", children: [(0, jsx_runtime_1.jsx)("img", { src: preview, alt: "Preview", className: "w-full h-auto max-h-[400px] object-contain rounded-lg" }), (0, jsx_runtime_1.jsxs)("div", { className: "absolute inset-0 bg-black/40 opacity-0 group-hover:opacity-100 transition-opacity duration-200 flex flex-col items-center justify-center text-white rounded-lg", children: [(0, jsx_runtime_1.jsx)(skalfa_icon_1.Icon, { icon: "solid/images", className: "text-3xl mb-2" }), (0, jsx_runtime_1.jsx)("p", { className: "text-xs text-center", children: "Klik atau seret untuk ganti Gambar" })] })] })) : ((0, jsx_runtime_1.jsxs)("div", { className: "input-image-dropzone-content", children: [(0, jsx_runtime_1.jsx)(skalfa_icon_1.Icon, { icon: drag ? "solid/hand-holding" : "solid/images", className: "text-3xl" }), (0, jsx_runtime_1.jsx)("p", { className: "input-image-dropzone-text", children: drag ? "Letakkan di sini" : preview ? "Klik atau seret untuk ganti Gambar" : "Klik atau seret gambar" })] })), (0, jsx_runtime_1.jsx)("input", { id: randomId, ref: inputRef, type: "file", accept: "image/*", className: "hidden", disabled: disabled, onChange: onUpload })] }) }), preview && !disabled && ((0, jsx_runtime_1.jsx)(Button_component_1.ButtonComponent, { icon: "solid/times", paint: "danger", size: "sm", className: "absolute top-10 right-4", onClick: remove })), invalidMessage && ((0, jsx_runtime_1.jsx)("small", { className: (0, _utils_1.cn)("input-error-message", (0, _utils_1.pcn)(className, "error")), children: invalidMessage })), !isSm ? ((0, jsx_runtime_1.jsx)(Modal_component_1.ModalComponent, { show: openCrop, onClose: onCropCancel, title: "Sesuaikan Gambar", className: "w-fit max-w-[90vw]", children: cropSrc && ((0, jsx_runtime_1.jsx)("div", { className: "p-4", children: (0, jsx_runtime_1.jsx)(exports.CanvasCropper, { src: cropSrc, aspect: eval(currentAspect.replace(":", "/")), onDone: onCropDone }) })) })) : ((0, jsx_runtime_1.jsx)(BottomSheet_component_1.BottomSheetComponent, { show: openCrop, onClose: onCropCancel, size: 400, children: cropSrc && ((0, jsx_runtime_1.jsx)("div", { className: "p-4", children: (0, jsx_runtime_1.jsx)(exports.CanvasCropper, { src: cropSrc, aspect: eval(currentAspect.replace(":", "/")), onDone: onCropDone }) })) }))] }));
|
|
108
|
+
}, onDragLeave: () => setDrag(false), onDrop: onDropFile, children: [preview && !containerAspect ? ((0, jsx_runtime_1.jsxs)("div", { className: "relative group w-full h-full flex items-center justify-center", children: [(0, jsx_runtime_1.jsx)("img", { src: preview, alt: "Preview", className: "w-full h-auto max-h-[400px] object-contain rounded-lg" }), (0, jsx_runtime_1.jsxs)("div", { className: "absolute inset-0 bg-black/40 opacity-0 group-hover:opacity-100 transition-opacity duration-200 flex flex-col items-center justify-center text-white rounded-lg", children: [(0, jsx_runtime_1.jsx)(skalfa_icon_1.Icon, { icon: "solid/images", className: "text-3xl mb-2" }), (0, jsx_runtime_1.jsx)("p", { className: "text-xs text-center", children: "Klik atau seret untuk ganti Gambar" })] })] })) : ((0, jsx_runtime_1.jsxs)("div", { className: "input-image-dropzone-content", children: [(0, jsx_runtime_1.jsx)(skalfa_icon_1.Icon, { icon: drag ? "solid/hand-holding" : "solid/images", className: "text-3xl" }), (0, jsx_runtime_1.jsx)("p", { className: "input-image-dropzone-text", children: drag ? "Letakkan di sini" : preview ? "Klik atau seret untuk ganti Gambar" : "Klik atau seret gambar" })] })), (0, jsx_runtime_1.jsx)("input", { id: randomId, ref: inputRef, type: "file", accept: "image/*", className: "hidden", disabled: disabled, onChange: onUpload })] }) }), preview && !disabled && ((0, jsx_runtime_1.jsx)(Button_component_1.ButtonComponent, { icon: "solid/times", paint: "danger", size: "sm", className: "absolute top-10 right-4", onClick: remove })), invalidMessage && ((0, jsx_runtime_1.jsx)("small", { className: (0, _utils_1.cn)("input-error-message", (0, _utils_1.pcn)(className, "error")), children: invalidMessage })), !isSm ? ((0, jsx_runtime_1.jsx)(Modal_component_1.ModalComponent, { show: openCrop, onClose: onCropCancel, title: "Sesuaikan Gambar", className: "w-fit max-w-[90vw]", children: cropSrc && ((0, jsx_runtime_1.jsx)("div", { className: "p-4", children: (0, jsx_runtime_1.jsx)(exports.CanvasCropper, { src: cropSrc, aspect: eval(currentAspect.replace(":", "/")), fileName: cropFile?.name, fileType: cropFile?.type, onDone: onCropDone }) })) })) : ((0, jsx_runtime_1.jsx)(BottomSheet_component_1.BottomSheetComponent, { show: openCrop, onClose: onCropCancel, size: 400, children: cropSrc && ((0, jsx_runtime_1.jsx)("div", { className: "p-4", children: (0, jsx_runtime_1.jsx)(exports.CanvasCropper, { src: cropSrc, aspect: eval(currentAspect.replace(":", "/")), fileName: cropFile?.name, fileType: cropFile?.type, onDone: onCropDone }) })) }))] }));
|
|
103
109
|
};
|
|
104
110
|
exports.InputImageComponent = InputImageComponent;
|
|
105
|
-
const CanvasCropper = ({ src, aspect, onDone, }) => {
|
|
111
|
+
const CanvasCropper = ({ src, aspect, fileName, fileType, onDone, }) => {
|
|
106
112
|
const canvasRef = (0, react_1.useRef)(null);
|
|
107
|
-
const previewRef = (0, react_1.useRef)(null);
|
|
108
113
|
const pinchDistRef = (0, react_1.useRef)(0);
|
|
109
114
|
const pinchStartZoomRef = (0, react_1.useRef)(1);
|
|
110
115
|
const [img, setImg] = (0, react_1.useState)(null);
|
|
@@ -130,8 +135,8 @@ const CanvasCropper = ({ src, aspect, onDone, }) => {
|
|
|
130
135
|
canvas.width = CROP_W;
|
|
131
136
|
canvas.height = CROP_SIZE;
|
|
132
137
|
ctx.clearRect(0, 0, canvas.width, canvas.height);
|
|
133
|
-
const scaledW = img.
|
|
134
|
-
const scaledH = img.
|
|
138
|
+
const scaledW = img.naturalWidth * zoom;
|
|
139
|
+
const scaledH = img.naturalHeight * zoom;
|
|
135
140
|
const x = pos.x - scaledW / 2 + CROP_W / 2;
|
|
136
141
|
const y = pos.y - scaledH / 2 + CROP_SIZE / 2;
|
|
137
142
|
ctx.drawImage(img, x, y, scaledW, scaledH);
|
|
@@ -178,19 +183,50 @@ const CanvasCropper = ({ src, aspect, onDone, }) => {
|
|
|
178
183
|
const performCrop = () => {
|
|
179
184
|
if (!img)
|
|
180
185
|
return;
|
|
181
|
-
const
|
|
182
|
-
const
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
const
|
|
186
|
-
const
|
|
187
|
-
const
|
|
188
|
-
const
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
186
|
+
const scaleFactor = 1 / zoom;
|
|
187
|
+
const scaledW = img.naturalWidth * zoom;
|
|
188
|
+
const scaledH = img.naturalHeight * zoom;
|
|
189
|
+
const cropDisplayX = (scaledW - CROP_W) / 2 - pos.x;
|
|
190
|
+
const cropDisplayY = (scaledH - CROP_SIZE) / 2 - pos.y;
|
|
191
|
+
const sourceX = cropDisplayX * scaleFactor;
|
|
192
|
+
const sourceY = cropDisplayY * scaleFactor;
|
|
193
|
+
const sourceW = CROP_W * scaleFactor;
|
|
194
|
+
const sourceH = CROP_SIZE * scaleFactor;
|
|
195
|
+
const sx = Math.max(0, Math.min(img.naturalWidth - 1, sourceX));
|
|
196
|
+
const sy = Math.max(0, Math.min(img.naturalHeight - 1, sourceY));
|
|
197
|
+
const sw = Math.min(sourceW, img.naturalWidth - sx);
|
|
198
|
+
const sh = Math.min(sourceH, img.naturalHeight - sy);
|
|
199
|
+
let targetW = Math.round(sw);
|
|
200
|
+
let targetH = Math.round(sh);
|
|
201
|
+
const MAX_DIMENSION = 2560;
|
|
202
|
+
if (targetW > MAX_DIMENSION || targetH > MAX_DIMENSION) {
|
|
203
|
+
if (targetW >= targetH) {
|
|
204
|
+
targetH = Math.round((targetH * MAX_DIMENSION) / targetW);
|
|
205
|
+
targetW = MAX_DIMENSION;
|
|
206
|
+
}
|
|
207
|
+
else {
|
|
208
|
+
targetW = Math.round((targetW * MAX_DIMENSION) / targetH);
|
|
209
|
+
targetH = MAX_DIMENSION;
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
const outputCanvas = document.createElement("canvas");
|
|
213
|
+
outputCanvas.width = targetW;
|
|
214
|
+
outputCanvas.height = targetH;
|
|
215
|
+
const octx = outputCanvas.getContext("2d");
|
|
216
|
+
if (!octx)
|
|
217
|
+
return;
|
|
218
|
+
octx.imageSmoothingEnabled = true;
|
|
219
|
+
octx.imageSmoothingQuality = "high";
|
|
220
|
+
octx.drawImage(img, sx, sy, sw, sh, 0, 0, targetW, targetH);
|
|
221
|
+
const type = fileType || "image/jpeg";
|
|
222
|
+
const name = fileName || (type === "image/png" ? "cropped.png" : "cropped.jpg");
|
|
223
|
+
const quality = type === "image/png" ? undefined : 0.92;
|
|
224
|
+
outputCanvas.toBlob((blob) => {
|
|
225
|
+
if (!blob)
|
|
226
|
+
return;
|
|
227
|
+
const file = new File([blob], name, { type, lastModified: Date.now() });
|
|
192
228
|
onDone?.(file);
|
|
193
|
-
});
|
|
229
|
+
}, type, quality);
|
|
194
230
|
};
|
|
195
231
|
const handleWheel = (e) => {
|
|
196
232
|
e.preventDefault();
|
|
@@ -276,6 +312,6 @@ const CanvasCropper = ({ src, aspect, onDone, }) => {
|
|
|
276
312
|
setDragging(false);
|
|
277
313
|
pinchDistRef.current = 0;
|
|
278
314
|
};
|
|
279
|
-
return ((0, jsx_runtime_1.jsxs)("div", { style: { touchAction: "none" }, children: [(0, jsx_runtime_1.jsxs)("div", { className: "input-image-cropper-wrapper", style: { width: CROP_W, height: CROP_SIZE }, onMouseMove: handleMove, onMouseUp: handleUp, onMouseLeave: handleUp, onWheel: handleWheel, onTouchStart: handleTouchStart, onTouchMove: handleTouchMove, onTouchEnd: handleTouchEnd, children: [(0, jsx_runtime_1.jsx)("canvas", { ref: canvasRef, width: CROP_W, height: CROP_SIZE, onMouseDown: handleDown, className: "cursor-move" }), (0, jsx_runtime_1.jsxs)("div", { className: "input-image-cropper-grid-container", style: { width: CROP_W, height: CROP_SIZE }, children: [(0, jsx_runtime_1.jsx)("div", { className: "input-image-cropper-grid-border" }), (0, jsx_runtime_1.jsxs)("div", { className: "absolute inset-0", children: [(0, jsx_runtime_1.jsx)("div", { className: "input-image-cropper-grid-line-h input-image-cropper-grid-line-h-1" }), (0, jsx_runtime_1.jsx)("div", { className: "input-image-cropper-grid-line-h input-image-cropper-grid-line-h-2" }), (0, jsx_runtime_1.jsx)("div", { className: "input-image-cropper-grid-line-v input-image-cropper-grid-line-v-1" }), (0, jsx_runtime_1.jsx)("div", { className: "input-image-cropper-grid-line-v input-image-cropper-grid-line-v-2" })] })] })] }), (0, jsx_runtime_1.jsx)("div", { className: "p-4 pb-2", children: (0, jsx_runtime_1.jsx)(Button_component_1.ButtonComponent, { label: "Selesai", variant: "outline", onClick: performCrop, block: true }) })
|
|
315
|
+
return ((0, jsx_runtime_1.jsxs)("div", { style: { touchAction: "none" }, children: [(0, jsx_runtime_1.jsxs)("div", { className: "input-image-cropper-wrapper", style: { width: CROP_W, height: CROP_SIZE }, onMouseMove: handleMove, onMouseUp: handleUp, onMouseLeave: handleUp, onWheel: handleWheel, onTouchStart: handleTouchStart, onTouchMove: handleTouchMove, onTouchEnd: handleTouchEnd, children: [(0, jsx_runtime_1.jsx)("canvas", { ref: canvasRef, width: CROP_W, height: CROP_SIZE, onMouseDown: handleDown, className: "cursor-move" }), (0, jsx_runtime_1.jsxs)("div", { className: "input-image-cropper-grid-container", style: { width: CROP_W, height: CROP_SIZE }, children: [(0, jsx_runtime_1.jsx)("div", { className: "input-image-cropper-grid-border" }), (0, jsx_runtime_1.jsxs)("div", { className: "absolute inset-0", children: [(0, jsx_runtime_1.jsx)("div", { className: "input-image-cropper-grid-line-h input-image-cropper-grid-line-h-1" }), (0, jsx_runtime_1.jsx)("div", { className: "input-image-cropper-grid-line-h input-image-cropper-grid-line-h-2" }), (0, jsx_runtime_1.jsx)("div", { className: "input-image-cropper-grid-line-v input-image-cropper-grid-line-v-1" }), (0, jsx_runtime_1.jsx)("div", { className: "input-image-cropper-grid-line-v input-image-cropper-grid-line-v-2" })] })] })] }), (0, jsx_runtime_1.jsx)("div", { className: "p-4 pb-2", children: (0, jsx_runtime_1.jsx)(Button_component_1.ButtonComponent, { label: "Selesai", variant: "outline", onClick: performCrop, block: true }) })] }));
|
|
280
316
|
};
|
|
281
317
|
exports.CanvasCropper = CanvasCropper;
|
package/package.json
CHANGED
|
@@ -54,6 +54,7 @@ export const InputImageComponent: React.FC<InputImageProps> = ({
|
|
|
54
54
|
const [preview, setPreview] = useState("");
|
|
55
55
|
const [drag, setDrag] = useState(false);
|
|
56
56
|
const [cropSrc, setCropSrc] = useState<string | null>(null);
|
|
57
|
+
const [cropFile, setCropFile] = useState<File | null>(null);
|
|
57
58
|
const [openCrop, setOpenCrop] = useState(false);
|
|
58
59
|
|
|
59
60
|
const inputHandler = useInputHandler(name, value, validations, register, true, unregister);
|
|
@@ -77,6 +78,7 @@ export const InputImageComponent: React.FC<InputImageProps> = ({
|
|
|
77
78
|
}, [value]);
|
|
78
79
|
|
|
79
80
|
const openCropper = (file: File) => {
|
|
81
|
+
setCropFile(file);
|
|
80
82
|
const reader = new FileReader();
|
|
81
83
|
reader.onload = () => {
|
|
82
84
|
setCropSrc(reader.result as string);
|
|
@@ -113,11 +115,15 @@ export const InputImageComponent: React.FC<InputImageProps> = ({
|
|
|
113
115
|
|
|
114
116
|
const onCropCancel = () => {
|
|
115
117
|
setOpenCrop(false);
|
|
118
|
+
setCropFile(null);
|
|
119
|
+
setCropSrc(null);
|
|
116
120
|
inputRef.current!.value = "";
|
|
117
121
|
};
|
|
118
122
|
|
|
119
123
|
const remove = () => {
|
|
120
124
|
setPreview("");
|
|
125
|
+
setCropFile(null);
|
|
126
|
+
setCropSrc(null);
|
|
121
127
|
onChange?.(null);
|
|
122
128
|
inputRef.current && (inputRef.current.value = "");
|
|
123
129
|
};
|
|
@@ -243,6 +249,8 @@ export const InputImageComponent: React.FC<InputImageProps> = ({
|
|
|
243
249
|
<CanvasCropper
|
|
244
250
|
src={cropSrc}
|
|
245
251
|
aspect={eval(currentAspect.replace(":", "/"))}
|
|
252
|
+
fileName={cropFile?.name}
|
|
253
|
+
fileType={cropFile?.type}
|
|
246
254
|
onDone={onCropDone}
|
|
247
255
|
/>
|
|
248
256
|
</div>
|
|
@@ -255,6 +263,8 @@ export const InputImageComponent: React.FC<InputImageProps> = ({
|
|
|
255
263
|
<CanvasCropper
|
|
256
264
|
src={cropSrc}
|
|
257
265
|
aspect={eval(currentAspect.replace(":", "/"))}
|
|
266
|
+
fileName={cropFile?.name}
|
|
267
|
+
fileType={cropFile?.type}
|
|
258
268
|
onDone={onCropDone}
|
|
259
269
|
/>
|
|
260
270
|
</div>
|
|
@@ -269,18 +279,21 @@ export const InputImageComponent: React.FC<InputImageProps> = ({
|
|
|
269
279
|
|
|
270
280
|
|
|
271
281
|
interface CropperProps {
|
|
272
|
-
src
|
|
273
|
-
aspect
|
|
274
|
-
|
|
282
|
+
src : string;
|
|
283
|
+
aspect : number;
|
|
284
|
+
fileName ?: string;
|
|
285
|
+
fileType ?: string;
|
|
286
|
+
onDone ?: (file: File) => void;
|
|
275
287
|
}
|
|
276
288
|
|
|
277
289
|
export const CanvasCropper: React.FC<CropperProps> = ({
|
|
278
290
|
src,
|
|
279
291
|
aspect,
|
|
292
|
+
fileName,
|
|
293
|
+
fileType,
|
|
280
294
|
onDone,
|
|
281
295
|
}) => {
|
|
282
296
|
const canvasRef = useRef<HTMLCanvasElement>(null);
|
|
283
|
-
const previewRef = useRef<HTMLCanvasElement>(null);
|
|
284
297
|
const pinchDistRef = useRef(0);
|
|
285
298
|
const pinchStartZoomRef = useRef(1);
|
|
286
299
|
|
|
@@ -314,8 +327,8 @@ export const CanvasCropper: React.FC<CropperProps> = ({
|
|
|
314
327
|
|
|
315
328
|
ctx.clearRect(0, 0, canvas.width, canvas.height);
|
|
316
329
|
|
|
317
|
-
const scaledW = img.
|
|
318
|
-
const scaledH = img.
|
|
330
|
+
const scaledW = img.naturalWidth * zoom;
|
|
331
|
+
const scaledH = img.naturalHeight * zoom;
|
|
319
332
|
|
|
320
333
|
const x = pos.x - scaledW / 2 + CROP_W / 2;
|
|
321
334
|
const y = pos.y - scaledH / 2 + CROP_SIZE / 2;
|
|
@@ -376,24 +389,63 @@ export const CanvasCropper: React.FC<CropperProps> = ({
|
|
|
376
389
|
const performCrop = () => {
|
|
377
390
|
if (!img) return;
|
|
378
391
|
|
|
379
|
-
const
|
|
380
|
-
const pctx = preview.getContext("2d")!;
|
|
392
|
+
const scaleFactor = 1 / zoom;
|
|
381
393
|
|
|
382
|
-
|
|
383
|
-
|
|
394
|
+
const scaledW = img.naturalWidth * zoom;
|
|
395
|
+
const scaledH = img.naturalHeight * zoom;
|
|
384
396
|
|
|
385
|
-
const
|
|
386
|
-
const
|
|
397
|
+
const cropDisplayX = (scaledW - CROP_W) / 2 - pos.x;
|
|
398
|
+
const cropDisplayY = (scaledH - CROP_SIZE) / 2 - pos.y;
|
|
387
399
|
|
|
388
|
-
const
|
|
389
|
-
const
|
|
400
|
+
const sourceX = cropDisplayX * scaleFactor;
|
|
401
|
+
const sourceY = cropDisplayY * scaleFactor;
|
|
402
|
+
const sourceW = CROP_W * scaleFactor;
|
|
403
|
+
const sourceH = CROP_SIZE * scaleFactor;
|
|
390
404
|
|
|
391
|
-
|
|
405
|
+
const sx = Math.max(0, Math.min(img.naturalWidth - 1, sourceX));
|
|
406
|
+
const sy = Math.max(0, Math.min(img.naturalHeight - 1, sourceY));
|
|
407
|
+
const sw = Math.min(sourceW, img.naturalWidth - sx);
|
|
408
|
+
const sh = Math.min(sourceH, img.naturalHeight - sy);
|
|
392
409
|
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
410
|
+
let targetW = Math.round(sw);
|
|
411
|
+
let targetH = Math.round(sh);
|
|
412
|
+
|
|
413
|
+
const MAX_DIMENSION = 2560;
|
|
414
|
+
if (targetW > MAX_DIMENSION || targetH > MAX_DIMENSION) {
|
|
415
|
+
if (targetW >= targetH) {
|
|
416
|
+
targetH = Math.round((targetH * MAX_DIMENSION) / targetW);
|
|
417
|
+
targetW = MAX_DIMENSION;
|
|
418
|
+
} else {
|
|
419
|
+
targetW = Math.round((targetW * MAX_DIMENSION) / targetH);
|
|
420
|
+
targetH = MAX_DIMENSION;
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
const outputCanvas = document.createElement("canvas");
|
|
425
|
+
outputCanvas.width = targetW;
|
|
426
|
+
outputCanvas.height = targetH;
|
|
427
|
+
|
|
428
|
+
const octx = outputCanvas.getContext("2d");
|
|
429
|
+
if (!octx) return;
|
|
430
|
+
|
|
431
|
+
octx.imageSmoothingEnabled = true;
|
|
432
|
+
octx.imageSmoothingQuality = "high";
|
|
433
|
+
|
|
434
|
+
octx.drawImage(img, sx, sy, sw, sh, 0, 0, targetW, targetH);
|
|
435
|
+
|
|
436
|
+
const type = fileType || "image/jpeg";
|
|
437
|
+
const name = fileName || (type === "image/png" ? "cropped.png" : "cropped.jpg");
|
|
438
|
+
const quality = type === "image/png" ? undefined : 0.92;
|
|
439
|
+
|
|
440
|
+
outputCanvas.toBlob(
|
|
441
|
+
(blob) => {
|
|
442
|
+
if (!blob) return;
|
|
443
|
+
const file = new File([blob], name, { type, lastModified: Date.now() });
|
|
444
|
+
onDone?.(file);
|
|
445
|
+
},
|
|
446
|
+
type,
|
|
447
|
+
quality
|
|
448
|
+
);
|
|
397
449
|
};
|
|
398
450
|
|
|
399
451
|
|
|
@@ -557,8 +609,6 @@ export const CanvasCropper: React.FC<CropperProps> = ({
|
|
|
557
609
|
block
|
|
558
610
|
/>
|
|
559
611
|
</div>
|
|
560
|
-
|
|
561
|
-
<canvas ref={previewRef} className="hidden" />
|
|
562
612
|
</div>
|
|
563
613
|
);
|
|
564
614
|
};
|