frontPanel/src/ui_kit/Modal/CropModal.tsx

285 lines
10 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import { CropIcon } from "@icons/CropIcon";
import { ResetIcon } from "@icons/ResetIcon";
import {
Box,
Button,
IconButton,
Modal,
Slider,
SxProps,
Theme,
Typography,
useMediaQuery,
useTheme,
} from "@mui/material";
import { closeCropModal, resetToOriginalImage, setCropModalImageUrl, useCropModalStore } from "@root/cropModal";
import { FC, useRef, useState } from "react";
import ReactCrop, { Crop, PixelCrop } from "react-image-crop";
import "react-image-crop/dist/ReactCrop.css";
import { canvasPreview } from "./utils/canvasPreview";
const styleSlider: SxProps<Theme> = {
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`,
},
},
};
interface Props {
onSaveImageClick?: (imageUrl: string) => void;
}
export const CropModal: FC<Props> = ({ onSaveImageClick }) => {
const theme = useTheme();
const isCropModalOpen = useCropModalStore(state => state.isCropModalOpen);
const imageUrl = useCropModalStore(state => state.imageUrl);
const [crop, setCrop] = useState<Crop>();
const [completedCrop, setCompletedCrop] = useState<PixelCrop>();
const [darken, setDarken] = useState(0);
const [rotate, setRotate] = useState(0);
const [width, setWidth] = useState<number>(0);
const cropImageElementRef = useRef<HTMLImageElement>(null);
const isMobile = useMediaQuery(theme.breakpoints.down(786));
const handleCropClick = async () => {
if (!completedCrop) throw new Error("No completed crop");
if (!cropImageElementRef.current) throw new Error("No image");
const canvasCopy = document.createElement("canvas");
const ctx = canvasCopy.getContext("2d");
if (!ctx) throw new Error("No 2d context");
canvasCopy.width = completedCrop.width;
canvasCopy.height = completedCrop.height;
ctx.filter = `brightness(${100 - darken}%)`;
await canvasPreview(cropImageElementRef.current, canvasCopy, completedCrop, rotate);
canvasCopy.toBlob((blob) => {
if (!blob) {
throw new Error("Failed to create blob");
}
const newImageUrl = URL.createObjectURL(blob);
setCropModalImageUrl(newImageUrl);
setCrop(undefined);
setCompletedCrop(undefined);
});
};
function handleSaveClick() {
if (imageUrl) onSaveImageClick?.(imageUrl);
setCrop(undefined);
setCompletedCrop(undefined);
closeCropModal();
}
const getImageSize = () => {
if (cropImageElementRef.current) {
const imageWidth = cropImageElementRef.current.naturalWidth;
const imageHeight = cropImageElementRef.current.naturalHeight;
const aspect = imageWidth / imageHeight;
if (aspect <= 1.333) {
setWidth(240);
}
if (aspect >= 1.5) {
setWidth(580);
}
if (aspect >= 1.778) {
setWidth(580);
}
}
};
return (
<Modal
open={isCropModalOpen}
onClose={closeCropModal}
aria-labelledby="modal-modal-title"
aria-describedby="modal-modal-description"
>
<Box sx={{
position: "absolute",
top: "50%",
left: "50%",
transform: "translate(-50%, -50%)",
bgcolor: "background.paper",
boxShadow: 24,
padding: "20px",
borderRadius: "8px",
width: isMobile ? "343px" : "620px",
}}>
<Box
sx={{
height: "320px",
padding: "10px",
backgroundSize: "cover",
backgroundRepeat: "no-repeat",
display: "flex",
alignItems: "center",
justifyContent: "center",
}}
>
{imageUrl && (
<ReactCrop
crop={crop}
onChange={(_, percentCrop) => setCrop(percentCrop)}
onComplete={(c) => setCompletedCrop(c)}
maxWidth={500}
minWidth={50}
maxHeight={320}
minHeight={50}
>
<img
onLoad={getImageSize}
ref={cropImageElementRef}
alt="Crop me"
src={imageUrl}
style={{
filter: `brightness(${100 - darken}%)`,
transform: ` rotate(${rotate}deg)`,
maxWidth: "580px",
maxHeight: "320px",
}}
width={width}
/>
</ReactCrop>
)}
</Box>
<Box
sx={{
color: "#7E2AEA",
display: "flex",
alignItems: "center",
justifyContent: "center",
fontSize: "16xp",
fontWeight: "600",
marginBottom: "50px",
}}
>
<Typography sx={{ color: "#7E2AEA", lineHeight: "0px" }}>
{crop?.width ? Math.round(crop.width) + "px" : ""}
</Typography>
<Typography sx={{ color: "#7E2AEA", lineHeight: "0px" }}>
{crop?.height ? Math.round(crop.height) + "px" : ""}
</Typography>
</Box>
<Box
sx={{
display: isMobile ? "block" : "flex",
alignItems: "end",
justifyContent: "space-between",
}}
>
<IconButton onClick={() => setRotate(r => (r + 90) % 360)}>
<ResetIcon />
</IconButton>
<Box>
<Typography sx={{ color: "#9A9AAF", fontSize: "16px" }}>
Размер
</Typography>
<Slider
sx={[styleSlider, {
width: isMobile ? "350px" : "250px",
}]}
value={width}
min={50}
max={580}
step={1}
onChange={(_, newValue) => {
setWidth(newValue as number);
}}
/>
</Box>
<Box>
<Typography sx={{ color: "#9A9AAF", fontSize: "16px" }}>
Затемнение
</Typography>
<Slider
sx={[styleSlider, {
width: isMobile ? "350px" : "250px",
}]}
value={darken}
min={0}
max={100}
step={1}
onChange={(_, newValue) => setDarken(newValue as number)}
/>
</Box>
</Box>
<Box
sx={{
marginTop: "40px",
width: "100%",
display: "flex",
}}
>
<Button
onClick={handleSaveClick}
disableRipple
sx={{
height: "48px",
color: "#7E2AEA",
borderRadius: "8px",
border: "1px solid #7E2AEA",
marginRight: "10px",
px: "20px",
}}
>Сохранить</Button>
<Button
onClick={resetToOriginalImage}
disableRipple
sx={{
width: "215px",
height: "48px",
color: "#7E2AEA",
borderRadius: "8px",
border: "1px solid #7E2AEA",
marginRight: "10px",
ml: "auto",
}}
>
Загрузить оригинал
</Button>
<Button
onClick={handleCropClick}
disableRipple
variant="contained"
disabled={!completedCrop}
sx={{
padding: "10px 20px",
borderRadius: "8px",
background: theme.palette.brightPurple.main,
fontSize: "18px",
}}
>
<CropIcon />
Обрезать
</Button>
</Box>
</Box>
</Modal>
);
};