346 lines
8.5 KiB
TypeScript
346 lines
8.5 KiB
TypeScript
import React, { FC, useEffect, useRef, useState } from "react";
|
||
import { saveAs } from "file-saver";
|
||
import ReactCrop, { Crop } from "react-image-crop";
|
||
import "react-image-crop/dist/ReactCrop.css";
|
||
import {
|
||
Box,
|
||
Button,
|
||
Modal,
|
||
Slider,
|
||
Typography,
|
||
useMediaQuery,
|
||
useTheme,
|
||
} from "@mui/material";
|
||
|
||
import quiz from "../../assets/quiz-template-6.png";
|
||
import { ResetIcon } from "@icons/ResetIcon";
|
||
|
||
interface Iprops {
|
||
opened: boolean;
|
||
onClose: () => void;
|
||
picture?: string | ArrayBuffer;
|
||
}
|
||
|
||
export const CroppingModal: FC<Iprops> = ({ opened, onClose, picture }) => {
|
||
const theme = useTheme();
|
||
const isMobile = useMediaQuery(theme.breakpoints.down(786));
|
||
|
||
const style = {
|
||
position: "absolute" as "absolute",
|
||
top: "50%",
|
||
left: "50%",
|
||
transform: "translate(-50%, -50%)",
|
||
width: isMobile ? "343px" : "620px",
|
||
bgcolor: "background.paper",
|
||
boxShadow: 24,
|
||
padding: "20px",
|
||
borderRadius: "8px",
|
||
};
|
||
|
||
const styleSlider = {
|
||
width: isMobile ? "350px" : "250px",
|
||
color: "#7E2AEA",
|
||
height: "12px",
|
||
"& .MuiSlider-track": {
|
||
border: "none",
|
||
},
|
||
"& .MuiSlider-rail": {
|
||
backgroundColor: "#F2F3F7",
|
||
border: `1px solid "#9A9AAF"`,
|
||
},
|
||
"& .MuiSlider-thumb": {
|
||
height: 26,
|
||
width: 26,
|
||
border: `6px solid #7E2AEA`,
|
||
backgroundColor: "white",
|
||
boxShadow: `0px 0px 0px 3px white,
|
||
0px 4px 4px 3px #C3C8DD`,
|
||
"&:focus, &:hover, &.Mui-active, &.Mui-focusVisible": {
|
||
boxShadow: `0px 0px 0px 3px white,
|
||
0px 4px 4px 3px #C3C8DD`,
|
||
},
|
||
},
|
||
};
|
||
|
||
const [src, setSrc] = useState<string | ArrayBuffer | null>(quiz);
|
||
const [crop, setCrop] = useState<Crop>({
|
||
unit: "px",
|
||
y: 0,
|
||
x: 0,
|
||
width: 100,
|
||
height: 100,
|
||
});
|
||
const [completedCrop, setCompletedCrop] = useState<Crop | null>(null);
|
||
const [imageSize, setImageSize] = useState(580);
|
||
const [darken, setDarken] = useState(0);
|
||
const fileInputRef = useRef<HTMLInputElement>(null);
|
||
|
||
console.log(src);
|
||
|
||
useEffect(() => {
|
||
if (picture) {
|
||
setSrc(picture);
|
||
}
|
||
}, [picture]);
|
||
|
||
const onCropComplete = (crop: Crop) => {
|
||
setCompletedCrop(crop);
|
||
};
|
||
|
||
const handleFileChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||
if (e.target.files && e.target.files.length > 0) {
|
||
const file = e.target.files[0];
|
||
const reader = new FileReader();
|
||
reader.onload = (event) => {
|
||
if (event.target && event.target.result) {
|
||
setSrc(event.target.result);
|
||
}
|
||
};
|
||
reader.readAsDataURL(file);
|
||
}
|
||
};
|
||
|
||
const handleDownloadClick = async () => {
|
||
if (completedCrop && src) {
|
||
const croppedImageUrl = await getCroppedAndDarkenedImg(
|
||
src,
|
||
completedCrop,
|
||
"cropped.jpeg",
|
||
darken
|
||
);
|
||
saveAs(croppedImageUrl, "cropped-image.jpeg");
|
||
}
|
||
};
|
||
|
||
const getCroppedAndDarkenedImg = (
|
||
image: string | ArrayBuffer,
|
||
crop: Crop,
|
||
fileName: string,
|
||
darken: number
|
||
): Promise<string> => {
|
||
const img = new Image();
|
||
img.src = image as string;
|
||
|
||
let scaleX = 360 / 580;
|
||
let scaleY = 219 / 320;
|
||
|
||
if (img.naturalWidth) {
|
||
scaleX = img.naturalWidth / 580;
|
||
}
|
||
|
||
if (img.naturalHeight) {
|
||
scaleY = img.naturalHeight / 320;
|
||
}
|
||
|
||
console.log(scaleX);
|
||
|
||
console.log(scaleY);
|
||
|
||
const canvas = document.createElement("canvas");
|
||
canvas.width = crop.width!;
|
||
canvas.height = crop.height!;
|
||
const ctx = canvas.getContext("2d");
|
||
|
||
if (!ctx) {
|
||
throw new Error("Canvas context is null");
|
||
}
|
||
|
||
ctx.drawImage(
|
||
img,
|
||
crop.x! * scaleX,
|
||
crop.y! * scaleY,
|
||
crop.width! * scaleX,
|
||
crop.height! * scaleY,
|
||
0,
|
||
0,
|
||
crop.width!,
|
||
crop.height!
|
||
);
|
||
|
||
const imageData = ctx.getImageData(0, 0, crop.width!, crop.height!);
|
||
|
||
const newImageData = imageData.data.map((value, index) => {
|
||
if ((index + 1) % 4 === 0) {
|
||
return value;
|
||
}
|
||
|
||
return value * (1 - darken / 100);
|
||
});
|
||
|
||
imageData.data.set(newImageData);
|
||
|
||
ctx.putImageData(imageData, 0, 0);
|
||
|
||
return new Promise<string>((resolve, reject) => {
|
||
canvas.toBlob((blob) => {
|
||
if (!blob) {
|
||
reject(new Error("Canvas is empty"));
|
||
return;
|
||
}
|
||
const file = new File([blob], fileName, { type: "image/jpeg" });
|
||
const imageUrl = window.URL.createObjectURL(file);
|
||
resolve(imageUrl);
|
||
}, "image/jpeg");
|
||
});
|
||
};
|
||
|
||
return (
|
||
<Modal
|
||
open={opened}
|
||
onClose={onClose}
|
||
aria-labelledby="modal-modal-title"
|
||
aria-describedby="modal-modal-description"
|
||
>
|
||
<Box sx={style}>
|
||
<Box
|
||
sx={{
|
||
height: "320px",
|
||
padding: "10px",
|
||
backgroundSize: "cover",
|
||
backgroundRepeat: "no-repeat",
|
||
|
||
display: "flex",
|
||
alignItems: "center",
|
||
justifyContent: "center",
|
||
}}
|
||
>
|
||
<ReactCrop
|
||
crop={crop}
|
||
onChange={(newCrop) => setCrop(newCrop)}
|
||
onComplete={onCropComplete}
|
||
>
|
||
{src && (
|
||
<img
|
||
src={src as string}
|
||
style={{
|
||
filter: `brightness(${100 - darken}%)`,
|
||
maxWidth: "580px",
|
||
}}
|
||
width={580 * (imageSize / 200)}
|
||
height={320}
|
||
alt="Crop"
|
||
/>
|
||
)}
|
||
</ReactCrop>
|
||
</Box>
|
||
|
||
<Box
|
||
sx={{
|
||
color: "#7E2AEA",
|
||
display: "flex",
|
||
alignItems: "center",
|
||
justifyContent: "center",
|
||
fontSize: "16xp",
|
||
fontWeight: "600",
|
||
marginBottom: "50px",
|
||
}}
|
||
>
|
||
<Typography sx={{ color: "#7E2AEA", lineHeight: "0px" }}>
|
||
{Math.round(crop.width)}
|
||
</Typography>
|
||
x
|
||
<Typography sx={{ color: "#7E2AEA", lineHeight: "0px" }}>
|
||
{Math.round(crop.width)}
|
||
</Typography>
|
||
px
|
||
</Box>
|
||
|
||
<Box
|
||
sx={{
|
||
display: isMobile ? "block" : "flex",
|
||
alignItems: "end",
|
||
justifyContent: "space-between",
|
||
}}
|
||
>
|
||
<ResetIcon
|
||
onClick={() => {
|
||
setCrop((prevCrop: Crop) => ({
|
||
...prevCrop,
|
||
unit: "px",
|
||
x: 210,
|
||
y: 10,
|
||
width: 210,
|
||
height: 300,
|
||
}));
|
||
|
||
setDarken(0);
|
||
setImageSize(580);
|
||
}}
|
||
style={{ marginBottom: "10px", cursor: "pointer" }}
|
||
/>
|
||
<Box>
|
||
<Typography sx={{ color: "#9A9AAF", fontSize: "16px" }}>
|
||
Размер
|
||
</Typography>
|
||
<Slider
|
||
sx={styleSlider}
|
||
value={imageSize}
|
||
min={50}
|
||
max={200}
|
||
step={1}
|
||
onChange={(_, newValue) => setImageSize(newValue as number)}
|
||
/>
|
||
</Box>
|
||
<Box>
|
||
<Typography sx={{ color: "#9A9AAF", fontSize: "16px" }}>
|
||
Затемнение
|
||
</Typography>
|
||
<Slider
|
||
sx={styleSlider}
|
||
value={darken}
|
||
min={0}
|
||
max={100}
|
||
step={1}
|
||
onChange={(_, newValue) => setDarken(newValue as number)}
|
||
/>
|
||
</Box>
|
||
</Box>
|
||
<Box
|
||
sx={{
|
||
marginTop: "40px",
|
||
width: "100%",
|
||
display: "flex",
|
||
justifyContent: "end",
|
||
}}
|
||
>
|
||
<input
|
||
ref={fileInputRef}
|
||
type="file"
|
||
accept="image/*"
|
||
id="fileInput"
|
||
style={{ display: "none" }}
|
||
onChange={handleFileChange}
|
||
/>
|
||
<Button
|
||
onClick={() => fileInputRef.current?.click()}
|
||
disableRipple
|
||
sx={{
|
||
width: "215px",
|
||
height: "48px",
|
||
color: "#7E2AEA",
|
||
borderRadius: "8px",
|
||
border: "1px solid #7E2AEA",
|
||
marginRight: "10px",
|
||
}}
|
||
>
|
||
Загрузить оригинал
|
||
</Button>
|
||
<Button
|
||
onClick={handleDownloadClick}
|
||
disableRipple
|
||
sx={{
|
||
width: "149px",
|
||
height: "48px",
|
||
color: "white",
|
||
background: "#7E2AEA",
|
||
borderRadius: "8px",
|
||
}}
|
||
>
|
||
Обрезать
|
||
</Button>
|
||
</Box>
|
||
</Box>
|
||
</Modal>
|
||
);
|
||
};
|