Freehand photo editor cropping mode (#1538)
This commit is contained in:
commit
3b271768de
6 changed files with 571 additions and 60 deletions
|
@ -624,5 +624,8 @@
|
|||
"FASTER_UPLOAD_DESCRIPTION": "Route uploads through nearby servers",
|
||||
"MAGIC_SEARCH_STATUS": "Magic Search Status",
|
||||
"INDEXED_ITEMS": "Indexed items",
|
||||
"CACHE_DIRECTORY": "Cache folder"
|
||||
"CACHE_DIRECTORY": "Cache folder",
|
||||
"FREEHAND": "Freehand",
|
||||
"APPLY_CROP": "Apply Crop",
|
||||
"PHOTO_EDIT_REQUIRED_TO_SAVE": "At least one transformation or color adjustment must be performed before saving."
|
||||
}
|
||||
|
|
|
@ -0,0 +1,133 @@
|
|||
import { EnteMenuItem } from 'components/Menu/EnteMenuItem';
|
||||
import { MenuItemGroup } from 'components/Menu/MenuItemGroup';
|
||||
import MenuSectionTitle from 'components/Menu/MenuSectionTitle';
|
||||
import { useContext } from 'react';
|
||||
import { ImageEditorOverlayContext } from './';
|
||||
import { CropBoxProps } from './';
|
||||
import type { MutableRefObject } from 'react';
|
||||
import { t } from 'i18next';
|
||||
import CropIcon from '@mui/icons-material/Crop';
|
||||
|
||||
interface IProps {
|
||||
previewScale: number;
|
||||
cropBoxProps: CropBoxProps;
|
||||
cropBoxRef: MutableRefObject<HTMLDivElement>;
|
||||
resetCropBox: () => void;
|
||||
}
|
||||
|
||||
export const cropRegionOfCanvas = (
|
||||
canvas: HTMLCanvasElement,
|
||||
topLeftX: number,
|
||||
topLeftY: number,
|
||||
bottomRightX: number,
|
||||
bottomRightY: number,
|
||||
scale: number = 1
|
||||
) => {
|
||||
const context = canvas.getContext('2d');
|
||||
if (!context || !canvas) return;
|
||||
context.imageSmoothingEnabled = false;
|
||||
|
||||
const width = (bottomRightX - topLeftX) * scale;
|
||||
const height = (bottomRightY - topLeftY) * scale;
|
||||
|
||||
const img = new Image();
|
||||
img.src = canvas.toDataURL();
|
||||
img.onload = () => {
|
||||
context.clearRect(0, 0, canvas.width, canvas.height);
|
||||
|
||||
canvas.width = width;
|
||||
canvas.height = height;
|
||||
|
||||
context.drawImage(
|
||||
img,
|
||||
topLeftX,
|
||||
topLeftY,
|
||||
width,
|
||||
height,
|
||||
0,
|
||||
0,
|
||||
width,
|
||||
height
|
||||
);
|
||||
};
|
||||
};
|
||||
|
||||
export const getCropRegionArgs = (
|
||||
cropBoxEle: HTMLDivElement,
|
||||
canvasEle: HTMLCanvasElement
|
||||
) => {
|
||||
// get the bounding rectangle of the crop box
|
||||
const cropBoxRect = cropBoxEle.getBoundingClientRect();
|
||||
// Get the bounding rectangle of the canvas
|
||||
const canvasRect = canvasEle.getBoundingClientRect();
|
||||
|
||||
// calculate the scale of the canvas display relative to its actual dimensions
|
||||
const displayScale = canvasEle.width / canvasRect.width;
|
||||
|
||||
// calculate the coordinates of the crop box relative to the canvas and adjust for any scrolling by adding scroll offsets
|
||||
const x1 =
|
||||
(cropBoxRect.left - canvasRect.left + window.scrollX) * displayScale;
|
||||
const y1 =
|
||||
(cropBoxRect.top - canvasRect.top + window.scrollY) * displayScale;
|
||||
const x2 = x1 + cropBoxRect.width * displayScale;
|
||||
const y2 = y1 + cropBoxRect.height * displayScale;
|
||||
|
||||
return {
|
||||
x1,
|
||||
x2,
|
||||
y1,
|
||||
y2,
|
||||
};
|
||||
};
|
||||
|
||||
const CropMenu = (props: IProps) => {
|
||||
const {
|
||||
canvasRef,
|
||||
originalSizeCanvasRef,
|
||||
canvasLoading,
|
||||
setCanvasLoading,
|
||||
setTransformationPerformed,
|
||||
setCurrentTab,
|
||||
} = useContext(ImageEditorOverlayContext);
|
||||
|
||||
return (
|
||||
<>
|
||||
<MenuSectionTitle title={t('FREEHAND')} />
|
||||
<MenuItemGroup
|
||||
style={{
|
||||
marginBottom: '0.5rem',
|
||||
}}>
|
||||
<EnteMenuItem
|
||||
disabled={canvasLoading}
|
||||
startIcon={<CropIcon />}
|
||||
onClick={() => {
|
||||
if (!props.cropBoxRef.current || !canvasRef.current)
|
||||
return;
|
||||
|
||||
const { x1, x2, y1, y2 } = getCropRegionArgs(
|
||||
props.cropBoxRef.current,
|
||||
canvasRef.current
|
||||
);
|
||||
setCanvasLoading(true);
|
||||
setTransformationPerformed(true);
|
||||
cropRegionOfCanvas(canvasRef.current, x1, y1, x2, y2);
|
||||
cropRegionOfCanvas(
|
||||
originalSizeCanvasRef.current,
|
||||
x1 / props.previewScale,
|
||||
y1 / props.previewScale,
|
||||
x2 / props.previewScale,
|
||||
y2 / props.previewScale
|
||||
);
|
||||
props.resetCropBox();
|
||||
setCanvasLoading(false);
|
||||
|
||||
setCurrentTab('transform');
|
||||
}}
|
||||
label={t('APPLY_CROP')}
|
||||
/>
|
||||
</MenuItemGroup>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default CropMenu;
|
|
@ -0,0 +1,118 @@
|
|||
import { CropBoxProps } from './';
|
||||
import type { Ref, Dispatch, SetStateAction, CSSProperties } from 'react';
|
||||
import { forwardRef } from 'react';
|
||||
|
||||
const handleStyle: CSSProperties = {
|
||||
position: 'absolute',
|
||||
height: '10px',
|
||||
width: '10px',
|
||||
backgroundColor: 'white',
|
||||
border: '1px solid black',
|
||||
};
|
||||
|
||||
const seHandleStyle: CSSProperties = {
|
||||
...handleStyle,
|
||||
right: '-5px',
|
||||
bottom: '-5px',
|
||||
cursor: 'se-resize',
|
||||
};
|
||||
|
||||
interface IProps {
|
||||
cropBox: CropBoxProps;
|
||||
setIsDragging: Dispatch<SetStateAction<boolean>>;
|
||||
}
|
||||
|
||||
const FreehandCropRegion = forwardRef(
|
||||
(props: IProps, ref: Ref<HTMLDivElement>) => {
|
||||
return (
|
||||
<>
|
||||
{/* Top overlay */}
|
||||
<div
|
||||
style={{
|
||||
position: 'absolute',
|
||||
top: 0,
|
||||
left: 0,
|
||||
right: 0,
|
||||
height: props.cropBox.y + 'px', // height up to the top of the crop box
|
||||
backgroundColor: 'rgba(0,0,0,0.5)',
|
||||
pointerEvents: 'none',
|
||||
}}></div>
|
||||
|
||||
{/* Bottom overlay */}
|
||||
<div
|
||||
style={{
|
||||
position: 'absolute',
|
||||
bottom: 0,
|
||||
left: 0,
|
||||
right: 0,
|
||||
height: `calc(100% - ${
|
||||
props.cropBox.y + props.cropBox.height
|
||||
}px)`, // height from the bottom of the crop box to the bottom of the canvas
|
||||
backgroundColor: 'rgba(0,0,0,0.5)',
|
||||
pointerEvents: 'none',
|
||||
}}></div>
|
||||
|
||||
{/* Left overlay */}
|
||||
<div
|
||||
style={{
|
||||
position: 'absolute',
|
||||
top: props.cropBox.y + 'px',
|
||||
left: 0,
|
||||
width: props.cropBox.x + 'px', // width up to the left side of the crop box
|
||||
height: props.cropBox.height + 'px', // same height as the crop box
|
||||
backgroundColor: 'rgba(0,0,0,0.5)',
|
||||
pointerEvents: 'none',
|
||||
}}></div>
|
||||
|
||||
{/* Right overlay */}
|
||||
<div
|
||||
style={{
|
||||
position: 'absolute',
|
||||
top: props.cropBox.y + 'px',
|
||||
right: 0,
|
||||
width: `calc(100% - ${
|
||||
props.cropBox.x + props.cropBox.width
|
||||
}px)`, // width from the right side of the crop box to the right side of the canvas
|
||||
height: props.cropBox.height + 'px', // same height as the crop box
|
||||
backgroundColor: 'rgba(0,0,0,0.5)',
|
||||
pointerEvents: 'none',
|
||||
}}></div>
|
||||
|
||||
<div
|
||||
style={{
|
||||
display: 'grid',
|
||||
position: 'absolute',
|
||||
left: props.cropBox.x + 'px',
|
||||
top: props.cropBox.y + 'px',
|
||||
width: props.cropBox.width + 'px',
|
||||
height: props.cropBox.height + 'px',
|
||||
border: '1px solid white',
|
||||
gridTemplateColumns: '1fr 1fr 1fr',
|
||||
gridTemplateRows: '1fr 1fr 1fr',
|
||||
gap: '0px',
|
||||
zIndex: 30, // make sure the crop box is above the overlays
|
||||
}}
|
||||
ref={ref}>
|
||||
{Array.from({ length: 9 }).map((_, index) => (
|
||||
<div
|
||||
key={index}
|
||||
style={{
|
||||
border: '1px solid white',
|
||||
boxSizing: 'border-box',
|
||||
pointerEvents: 'none',
|
||||
}}></div>
|
||||
))}
|
||||
|
||||
<div
|
||||
style={seHandleStyle}
|
||||
onMouseDown={(e) => {
|
||||
e.preventDefault();
|
||||
props.setIsDragging(true);
|
||||
}}></div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
export default FreehandCropRegion;
|
|
@ -89,6 +89,7 @@ const TransformMenu = () => {
|
|||
);
|
||||
};
|
||||
};
|
||||
|
||||
const flipCanvas = (
|
||||
canvas: HTMLCanvasElement,
|
||||
direction: 'vertical' | 'horizontal'
|
||||
|
|
|
@ -29,6 +29,7 @@ import mime from 'mime-types';
|
|||
import CloseIcon from '@mui/icons-material/Close';
|
||||
import { HorizontalFlex } from '@ente/shared/components/Container';
|
||||
import TransformMenu from './TransformMenu';
|
||||
import CropMenu from './CropMenu';
|
||||
import ColoursMenu from './ColoursMenu';
|
||||
import { FileWithCollection } from 'types/upload';
|
||||
import uploadManager from 'services/upload/uploadManager';
|
||||
|
@ -44,6 +45,12 @@ import { getEditorCloseConfirmationMessage } from 'utils/ui';
|
|||
import { logError } from '@ente/shared/sentry';
|
||||
import { getFileType } from 'services/typeDetectionService';
|
||||
import { downloadUsingAnchor } from '@ente/shared/utils';
|
||||
import { CORNER_THRESHOLD, FILTER_DEFAULT_VALUES } from 'constants/photoEditor';
|
||||
import FreehandCropRegion from './FreehandCropRegion';
|
||||
import EnteButton from '@ente/shared/components/EnteButton';
|
||||
import { CenteredFlex } from '@ente/shared/components/Container';
|
||||
import CropIcon from '@mui/icons-material/Crop';
|
||||
import { cropRegionOfCanvas, getCropRegionArgs } from './CropMenu';
|
||||
|
||||
interface IProps {
|
||||
file: EnteFile;
|
||||
|
@ -59,16 +66,11 @@ export const ImageEditorOverlayContext = createContext(
|
|||
setTransformationPerformed: Dispatch<SetStateAction<boolean>>;
|
||||
setCanvasLoading: Dispatch<SetStateAction<boolean>>;
|
||||
canvasLoading: boolean;
|
||||
setCurrentTab: Dispatch<SetStateAction<OperationTab>>;
|
||||
}
|
||||
);
|
||||
|
||||
const filterDefaultValues = {
|
||||
brightness: 100,
|
||||
contrast: 100,
|
||||
blur: 0,
|
||||
saturation: 100,
|
||||
invert: false,
|
||||
};
|
||||
type OperationTab = 'crop' | 'transform' | 'colours';
|
||||
|
||||
const getEditedFileName = (fileName: string) => {
|
||||
const fileNameParts = fileName.split('.');
|
||||
|
@ -77,6 +79,13 @@ const getEditedFileName = (fileName: string) => {
|
|||
return editedFileName;
|
||||
};
|
||||
|
||||
export interface CropBoxProps {
|
||||
x: number;
|
||||
y: number;
|
||||
width: number;
|
||||
height: number;
|
||||
}
|
||||
|
||||
const ImageEditorOverlay = (props: IProps) => {
|
||||
const appContext = useContext(AppContext);
|
||||
|
||||
|
@ -88,19 +97,17 @@ const ImageEditorOverlay = (props: IProps) => {
|
|||
|
||||
const [currentRotationAngle, setCurrentRotationAngle] = useState(0);
|
||||
|
||||
const [currentTab, setCurrentTab] = useState<'transform' | 'colours'>(
|
||||
'transform'
|
||||
);
|
||||
const [currentTab, setCurrentTab] = useState<OperationTab>('transform');
|
||||
|
||||
const [brightness, setBrightness] = useState(
|
||||
filterDefaultValues.brightness
|
||||
FILTER_DEFAULT_VALUES.brightness
|
||||
);
|
||||
const [contrast, setContrast] = useState(filterDefaultValues.contrast);
|
||||
const [blur, setBlur] = useState(filterDefaultValues.blur);
|
||||
const [contrast, setContrast] = useState(FILTER_DEFAULT_VALUES.contrast);
|
||||
const [blur, setBlur] = useState(FILTER_DEFAULT_VALUES.blur);
|
||||
const [saturation, setSaturation] = useState(
|
||||
filterDefaultValues.saturation
|
||||
FILTER_DEFAULT_VALUES.saturation
|
||||
);
|
||||
const [invert, setInvert] = useState(filterDefaultValues.invert);
|
||||
const [invert, setInvert] = useState(FILTER_DEFAULT_VALUES.invert);
|
||||
|
||||
const [transformationPerformed, setTransformationPerformed] =
|
||||
useState(false);
|
||||
|
@ -110,6 +117,149 @@ const ImageEditorOverlay = (props: IProps) => {
|
|||
|
||||
const [showControlsDrawer, setShowControlsDrawer] = useState(true);
|
||||
|
||||
const [previewCanvasScale, setPreviewCanvasScale] = useState(0);
|
||||
|
||||
const [cropBox, setCropBox] = useState<CropBoxProps>({
|
||||
x: 0,
|
||||
y: 0,
|
||||
width: 100,
|
||||
height: 100,
|
||||
});
|
||||
|
||||
const [startX, setStartX] = useState(0);
|
||||
const [startY, setStartY] = useState(0);
|
||||
|
||||
const [beforeGrowthHeight, setBeforeGrowthHeight] = useState(0);
|
||||
const [beforeGrowthWidth, setBeforeGrowthWidth] = useState(0);
|
||||
|
||||
const [isDragging, setIsDragging] = useState(false);
|
||||
const [isGrowing, setIsGrowing] = useState(false);
|
||||
|
||||
const cropBoxRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
const getCanvasBoundsOffsets = () => {
|
||||
const canvasBounds = {
|
||||
height: canvasRef.current.height,
|
||||
width: canvasRef.current.width,
|
||||
};
|
||||
const parentBounds = parentRef.current.getBoundingClientRect();
|
||||
|
||||
// calculate the offset created by centering the canvas in its parent
|
||||
const offsetX = (parentBounds.width - canvasBounds.width) / 2;
|
||||
const offsetY = (parentBounds.height - canvasBounds.height) / 2;
|
||||
|
||||
return {
|
||||
offsetY,
|
||||
offsetX,
|
||||
canvasBounds,
|
||||
parentBounds,
|
||||
};
|
||||
};
|
||||
|
||||
const handleDragStart = (e) => {
|
||||
if (currentTab !== 'crop') return;
|
||||
|
||||
const rect = cropBoxRef.current.getBoundingClientRect();
|
||||
const offsetX = e.pageX - rect.left - rect.width / 2;
|
||||
const offsetY = e.pageY - rect.top - rect.height / 2;
|
||||
|
||||
// check if the cursor is near the corners of the box
|
||||
const isNearLeftOrRightEdge =
|
||||
e.pageX < rect.left + CORNER_THRESHOLD ||
|
||||
e.pageX > rect.right - CORNER_THRESHOLD;
|
||||
const isNearTopOrBottomEdge =
|
||||
e.pageY < rect.top + CORNER_THRESHOLD ||
|
||||
e.pageY > rect.bottom - CORNER_THRESHOLD;
|
||||
|
||||
if (isNearLeftOrRightEdge && isNearTopOrBottomEdge) {
|
||||
// cursor is near a corner, do not initiate dragging
|
||||
setIsGrowing(true);
|
||||
setStartX(e.pageX);
|
||||
setStartY(e.pageY);
|
||||
setBeforeGrowthWidth(cropBox.width);
|
||||
setBeforeGrowthHeight(cropBox.height);
|
||||
return;
|
||||
}
|
||||
|
||||
setIsDragging(true);
|
||||
setStartX(e.pageX - offsetX);
|
||||
setStartY(e.pageY - offsetY);
|
||||
};
|
||||
|
||||
const handleDrag = (e) => {
|
||||
if (!isDragging && !isGrowing) return;
|
||||
|
||||
// d- variables are the delta change between start and now
|
||||
const dx = e.pageX - startX;
|
||||
const dy = e.pageY - startY;
|
||||
|
||||
const { offsetX, offsetY, canvasBounds } = getCanvasBoundsOffsets();
|
||||
|
||||
if (isGrowing) {
|
||||
setCropBox((prev) => {
|
||||
const newWidth = Math.min(
|
||||
beforeGrowthWidth + dx,
|
||||
canvasBounds.width - prev.x + offsetX
|
||||
);
|
||||
const newHeight = Math.min(
|
||||
beforeGrowthHeight + dy,
|
||||
canvasBounds.height - prev.y + offsetY
|
||||
);
|
||||
|
||||
return {
|
||||
...prev,
|
||||
width: newWidth,
|
||||
height: newHeight,
|
||||
};
|
||||
});
|
||||
} else {
|
||||
setCropBox((prev) => {
|
||||
let newX = prev.x + dx;
|
||||
let newY = prev.y + dy;
|
||||
|
||||
// constrain the new position to the canvas boundaries, accounting for the offset
|
||||
newX = Math.max(
|
||||
offsetX,
|
||||
Math.min(newX, offsetX + canvasBounds.width - prev.width)
|
||||
);
|
||||
newY = Math.max(
|
||||
offsetY,
|
||||
Math.min(newY, offsetY + canvasBounds.height - prev.height)
|
||||
);
|
||||
|
||||
return {
|
||||
...prev,
|
||||
x: newX,
|
||||
y: newY,
|
||||
};
|
||||
});
|
||||
setStartX(e.pageX);
|
||||
setStartY(e.pageY);
|
||||
}
|
||||
};
|
||||
|
||||
const handleDragEnd = () => {
|
||||
setStartX(0);
|
||||
setStartY(0);
|
||||
|
||||
setIsGrowing(false);
|
||||
setIsDragging(false);
|
||||
};
|
||||
|
||||
const resetCropBox = () => {
|
||||
setCropBox((prev) => {
|
||||
const { offsetX, offsetY, canvasBounds } = getCanvasBoundsOffsets();
|
||||
|
||||
return {
|
||||
...prev,
|
||||
x: offsetX,
|
||||
y: offsetY,
|
||||
height: canvasBounds.height,
|
||||
width: canvasBounds.width,
|
||||
};
|
||||
});
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (!canvasRef.current) {
|
||||
return;
|
||||
|
@ -117,17 +267,23 @@ const ImageEditorOverlay = (props: IProps) => {
|
|||
try {
|
||||
applyFilters([canvasRef.current, originalSizeCanvasRef.current]);
|
||||
setColoursAdjusted(
|
||||
brightness !== filterDefaultValues.brightness ||
|
||||
contrast !== filterDefaultValues.contrast ||
|
||||
blur !== filterDefaultValues.blur ||
|
||||
saturation !== filterDefaultValues.saturation ||
|
||||
invert !== filterDefaultValues.invert
|
||||
brightness !== FILTER_DEFAULT_VALUES.brightness ||
|
||||
contrast !== FILTER_DEFAULT_VALUES.contrast ||
|
||||
blur !== FILTER_DEFAULT_VALUES.blur ||
|
||||
saturation !== FILTER_DEFAULT_VALUES.saturation ||
|
||||
invert !== FILTER_DEFAULT_VALUES.invert
|
||||
);
|
||||
} catch (e) {
|
||||
logError(e, 'Error applying filters');
|
||||
}
|
||||
}, [brightness, contrast, blur, saturation, invert, canvasRef, fileURL]);
|
||||
|
||||
useEffect(() => {
|
||||
if (currentTab !== 'crop') return;
|
||||
resetCropBox();
|
||||
setShowControlsDrawer(false);
|
||||
}, [currentTab]);
|
||||
|
||||
const applyFilters = async (canvases: HTMLCanvasElement[]) => {
|
||||
try {
|
||||
for (const canvas of canvases) {
|
||||
|
@ -203,6 +359,7 @@ const ImageEditorOverlay = (props: IProps) => {
|
|||
}
|
||||
|
||||
setCanvasLoading(true);
|
||||
|
||||
resetFilters();
|
||||
setCurrentRotationAngle(0);
|
||||
|
||||
|
@ -226,6 +383,7 @@ const ImageEditorOverlay = (props: IProps) => {
|
|||
parentRef.current.clientWidth / img.width,
|
||||
parentRef.current.clientHeight / img.height
|
||||
);
|
||||
setPreviewCanvasScale(scale);
|
||||
|
||||
const width = img.width * scale;
|
||||
const height = img.height * scale;
|
||||
|
@ -246,6 +404,13 @@ const ImageEditorOverlay = (props: IProps) => {
|
|||
setColoursAdjusted(false);
|
||||
|
||||
setCanvasLoading(false);
|
||||
|
||||
resetCropBox();
|
||||
setStartX(0);
|
||||
setStartY(0);
|
||||
setIsDragging(false);
|
||||
setIsGrowing(false);
|
||||
|
||||
resolve(true);
|
||||
} catch (e) {
|
||||
reject(e);
|
||||
|
@ -387,35 +552,97 @@ const ImageEditorOverlay = (props: IProps) => {
|
|||
boxSizing={'border-box'}
|
||||
display="flex"
|
||||
alignItems="center"
|
||||
justifyContent="center">
|
||||
justifyContent="center"
|
||||
position="relative"
|
||||
onMouseUp={handleDragEnd}
|
||||
onMouseMove={isDragging ? handleDrag : null}
|
||||
onMouseDown={handleDragStart}>
|
||||
<Box
|
||||
height="90%"
|
||||
width="100%"
|
||||
ref={parentRef}
|
||||
display="flex"
|
||||
alignItems="center"
|
||||
justifyContent="center">
|
||||
{(fileURL === null || canvasLoading) && (
|
||||
<CircularProgress />
|
||||
)}
|
||||
style={{
|
||||
position: 'relative',
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
}}>
|
||||
<Box
|
||||
height="90%"
|
||||
width="100%"
|
||||
ref={parentRef}
|
||||
display="flex"
|
||||
alignItems="center"
|
||||
justifyContent="center"
|
||||
position="relative">
|
||||
{(fileURL === null || canvasLoading) && (
|
||||
<CircularProgress />
|
||||
)}
|
||||
|
||||
<canvas
|
||||
ref={canvasRef}
|
||||
style={{
|
||||
objectFit: 'contain',
|
||||
display:
|
||||
fileURL === null || canvasLoading
|
||||
? 'none'
|
||||
: 'block',
|
||||
position: 'absolute',
|
||||
}}
|
||||
/>
|
||||
<canvas
|
||||
ref={originalSizeCanvasRef}
|
||||
style={{
|
||||
display: 'none',
|
||||
}}
|
||||
/>
|
||||
<canvas
|
||||
ref={canvasRef}
|
||||
style={{
|
||||
objectFit: 'contain',
|
||||
display:
|
||||
fileURL === null || canvasLoading
|
||||
? 'none'
|
||||
: 'block',
|
||||
position: 'absolute',
|
||||
}}
|
||||
/>
|
||||
<canvas
|
||||
ref={originalSizeCanvasRef}
|
||||
style={{
|
||||
display: 'none',
|
||||
}}
|
||||
/>
|
||||
|
||||
{currentTab === 'crop' && (
|
||||
<FreehandCropRegion
|
||||
cropBox={cropBox}
|
||||
ref={cropBoxRef}
|
||||
setIsDragging={setIsDragging}
|
||||
/>
|
||||
)}
|
||||
</Box>
|
||||
{currentTab === 'crop' && (
|
||||
<CenteredFlex marginTop="1rem">
|
||||
<EnteButton
|
||||
color="accent"
|
||||
startIcon={<CropIcon />}
|
||||
onClick={() => {
|
||||
if (
|
||||
!cropBoxRef.current ||
|
||||
!canvasRef.current
|
||||
)
|
||||
return;
|
||||
|
||||
const { x1, x2, y1, y2 } =
|
||||
getCropRegionArgs(
|
||||
cropBoxRef.current,
|
||||
canvasRef.current
|
||||
);
|
||||
setCanvasLoading(true);
|
||||
setTransformationPerformed(true);
|
||||
cropRegionOfCanvas(
|
||||
canvasRef.current,
|
||||
x1,
|
||||
y1,
|
||||
x2,
|
||||
y2
|
||||
);
|
||||
cropRegionOfCanvas(
|
||||
originalSizeCanvasRef.current,
|
||||
x1 / previewCanvasScale,
|
||||
y1 / previewCanvasScale,
|
||||
x2 / previewCanvasScale,
|
||||
y2 / previewCanvasScale
|
||||
);
|
||||
resetCropBox();
|
||||
setCanvasLoading(false);
|
||||
|
||||
setCurrentTab('transform');
|
||||
}}>
|
||||
{t('APPLY_CROP')}
|
||||
</EnteButton>
|
||||
</CenteredFlex>
|
||||
)}
|
||||
</Box>
|
||||
</Box>
|
||||
</Box>
|
||||
|
@ -441,6 +668,7 @@ const ImageEditorOverlay = (props: IProps) => {
|
|||
onChange={(_, value) => {
|
||||
setCurrentTab(value);
|
||||
}}>
|
||||
<Tab label={t('CROP')} value="crop" />
|
||||
<Tab label={t('TRANSFORM')} value="transform" />
|
||||
<Tab
|
||||
label={t('COLORS')}
|
||||
|
@ -463,18 +691,25 @@ const ImageEditorOverlay = (props: IProps) => {
|
|||
label={t('RESTORE_ORIGINAL')}
|
||||
/>
|
||||
</MenuItemGroup>
|
||||
{currentTab === 'transform' && (
|
||||
<ImageEditorOverlayContext.Provider
|
||||
value={{
|
||||
originalSizeCanvasRef,
|
||||
canvasRef,
|
||||
setCanvasLoading,
|
||||
canvasLoading,
|
||||
setTransformationPerformed,
|
||||
}}>
|
||||
<TransformMenu />
|
||||
</ImageEditorOverlayContext.Provider>
|
||||
)}
|
||||
<ImageEditorOverlayContext.Provider
|
||||
value={{
|
||||
originalSizeCanvasRef,
|
||||
canvasRef,
|
||||
setCanvasLoading,
|
||||
canvasLoading,
|
||||
setTransformationPerformed,
|
||||
setCurrentTab,
|
||||
}}>
|
||||
{currentTab === 'crop' && (
|
||||
<CropMenu
|
||||
previewScale={previewCanvasScale}
|
||||
cropBoxProps={cropBox}
|
||||
cropBoxRef={cropBoxRef}
|
||||
resetCropBox={resetCropBox}
|
||||
/>
|
||||
)}
|
||||
{currentTab === 'transform' && <TransformMenu />}
|
||||
</ImageEditorOverlayContext.Provider>
|
||||
{currentTab === 'colours' && (
|
||||
<ColoursMenu
|
||||
brightness={brightness}
|
||||
|
@ -495,14 +730,25 @@ const ImageEditorOverlay = (props: IProps) => {
|
|||
startIcon={<DownloadIcon />}
|
||||
onClick={downloadEditedPhoto}
|
||||
label={t('DOWNLOAD_EDITED')}
|
||||
disabled={
|
||||
!transformationPerformed && !coloursAdjusted
|
||||
}
|
||||
/>
|
||||
<MenuItemDivider />
|
||||
<EnteMenuItem
|
||||
startIcon={<CloudUploadIcon />}
|
||||
onClick={saveCopyToEnte}
|
||||
label={t('SAVE_A_COPY_TO_ENTE')}
|
||||
disabled={
|
||||
!transformationPerformed && !coloursAdjusted
|
||||
}
|
||||
/>
|
||||
</MenuItemGroup>
|
||||
{!transformationPerformed && !coloursAdjusted && (
|
||||
<MenuSectionTitle
|
||||
title={t('PHOTO_EDIT_REQUIRED_TO_SAVE')}
|
||||
/>
|
||||
)}
|
||||
</EnteDrawer>
|
||||
</Backdrop>
|
||||
</>
|
||||
|
|
10
apps/photos/src/constants/photoEditor.ts
Normal file
10
apps/photos/src/constants/photoEditor.ts
Normal file
|
@ -0,0 +1,10 @@
|
|||
export const FILTER_DEFAULT_VALUES = {
|
||||
brightness: 100,
|
||||
contrast: 100,
|
||||
blur: 0,
|
||||
saturation: 100,
|
||||
invert: false,
|
||||
};
|
||||
|
||||
// CORNER_THRESHOLD defines the threshold near the corners of the crop box in which dragging is assumed as not the intention
|
||||
export const CORNER_THRESHOLD = 20;
|
Loading…
Add table
Reference in a new issue