Media editor: saving an image with a pending crop applies that crop
Co-authored-by: Evan Hahn <69474926+EvanHahn-Signal@users.noreply.github.com>
This commit is contained in:
parent
2bbb5bf694
commit
ac5c49676b
|
@ -58,6 +58,13 @@ enum DrawTool {
|
||||||
Highlighter = 'Highlighter',
|
Highlighter = 'Highlighter',
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type PendingCropType = {
|
||||||
|
left: number;
|
||||||
|
top: number;
|
||||||
|
width: number;
|
||||||
|
height: number;
|
||||||
|
};
|
||||||
|
|
||||||
function isCmdOrCtrl(ev: KeyboardEvent): boolean {
|
function isCmdOrCtrl(ev: KeyboardEvent): boolean {
|
||||||
const { ctrlKey, metaKey } = ev;
|
const { ctrlKey, metaKey } = ev;
|
||||||
const commandKey = get(window, 'platform') === 'darwin' && metaKey;
|
const commandKey = get(window, 'platform') === 'darwin' && metaKey;
|
||||||
|
@ -346,57 +353,10 @@ export const MediaEditor = ({
|
||||||
|
|
||||||
// Refresh the background image according to imageState changes
|
// Refresh the background image according to imageState changes
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const backgroundImage = new fabric.Image(image, {
|
if (!fabricCanvas) {
|
||||||
canvas: fabricCanvas,
|
return;
|
||||||
height: imageState.height || image.height,
|
|
||||||
width: imageState.width || image.width,
|
|
||||||
});
|
|
||||||
|
|
||||||
let left: number;
|
|
||||||
let top: number;
|
|
||||||
switch (imageState.angle) {
|
|
||||||
case 0:
|
|
||||||
left = 0;
|
|
||||||
top = 0;
|
|
||||||
break;
|
|
||||||
case 90:
|
|
||||||
left = imageState.width;
|
|
||||||
top = 0;
|
|
||||||
break;
|
|
||||||
case 180:
|
|
||||||
left = imageState.width;
|
|
||||||
top = imageState.height;
|
|
||||||
break;
|
|
||||||
case 270:
|
|
||||||
left = 0;
|
|
||||||
top = imageState.height;
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
throw new Error('Unexpected angle');
|
|
||||||
}
|
}
|
||||||
|
drawFabricBackgroundImage({ fabricCanvas, image, imageState });
|
||||||
let { height, width } = imageState;
|
|
||||||
if (imageState.angle % 180) {
|
|
||||||
[width, height] = [height, width];
|
|
||||||
}
|
|
||||||
|
|
||||||
fabricCanvas?.setBackgroundImage(
|
|
||||||
backgroundImage,
|
|
||||||
fabricCanvas.requestRenderAll.bind(fabricCanvas),
|
|
||||||
{
|
|
||||||
angle: imageState.angle,
|
|
||||||
cropX: imageState.cropX,
|
|
||||||
cropY: imageState.cropY,
|
|
||||||
flipX: imageState.flipX,
|
|
||||||
flipY: imageState.flipY,
|
|
||||||
left,
|
|
||||||
top,
|
|
||||||
originX: 'left',
|
|
||||||
originY: 'top',
|
|
||||||
width,
|
|
||||||
height,
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}, [fabricCanvas, image, imageState]);
|
}, [fabricCanvas, image, imageState]);
|
||||||
|
|
||||||
const [canRedo, setCanRedo] = useState(false);
|
const [canRedo, setCanRedo] = useState(false);
|
||||||
|
@ -898,55 +858,13 @@ export const MediaEditor = ({
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const cropRect = fabricCanvas.getActiveObject();
|
const pendingCrop = getPendingCrop(fabricCanvas);
|
||||||
|
if (!pendingCrop) {
|
||||||
if (!(cropRect instanceof MediaEditorFabricCropRect)) {
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const { left, height, top, width } = cropRect.getBoundingRect(true);
|
setImageState(curr => getNewImageStateFromCrop(curr, pendingCrop));
|
||||||
|
moveFabricObjectsForCrop(fabricCanvas, pendingCrop);
|
||||||
setImageState(curr => {
|
|
||||||
let cropX: number;
|
|
||||||
let cropY: number;
|
|
||||||
switch (curr.angle) {
|
|
||||||
case 0:
|
|
||||||
cropX = curr.cropX + left;
|
|
||||||
cropY = curr.cropY + top;
|
|
||||||
break;
|
|
||||||
case 90:
|
|
||||||
cropX = curr.cropX + top;
|
|
||||||
cropY = curr.cropY + (curr.width - (left + width));
|
|
||||||
break;
|
|
||||||
case 180:
|
|
||||||
cropX = curr.cropX + (curr.width - (left + width));
|
|
||||||
cropY = curr.cropY + (curr.height - (top + height));
|
|
||||||
break;
|
|
||||||
case 270:
|
|
||||||
cropX = curr.cropX + (curr.height - (top + height));
|
|
||||||
cropY = curr.cropY + left;
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
throw new Error('Unexpected angle');
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
...curr,
|
|
||||||
cropX,
|
|
||||||
cropY,
|
|
||||||
height,
|
|
||||||
width,
|
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
||||||
fabricCanvas.getObjects().forEach(obj => {
|
|
||||||
const { x, y } = obj.getCenterPoint();
|
|
||||||
|
|
||||||
const translatedCenter = new fabric.Point(x - left, y - top);
|
|
||||||
obj.setPositionByOrigin(translatedCenter, 'center', 'center');
|
|
||||||
obj.setCoords();
|
|
||||||
});
|
|
||||||
|
|
||||||
setEditMode(undefined);
|
setEditMode(undefined);
|
||||||
}}
|
}}
|
||||||
type="button"
|
type="button"
|
||||||
|
@ -1142,25 +1060,39 @@ export const MediaEditor = ({
|
||||||
|
|
||||||
let data: Uint8Array;
|
let data: Uint8Array;
|
||||||
try {
|
try {
|
||||||
fabricCanvas.discardActiveObject();
|
const renderFabricCanvas = await cloneFabricCanvas(
|
||||||
fabricCanvas.remove(
|
fabricCanvas
|
||||||
...fabricCanvas
|
);
|
||||||
|
|
||||||
|
renderFabricCanvas.remove(
|
||||||
|
...renderFabricCanvas
|
||||||
.getObjects()
|
.getObjects()
|
||||||
.filter(obj => obj.excludeFromExport)
|
.filter(obj => obj.excludeFromExport)
|
||||||
);
|
);
|
||||||
|
|
||||||
fabricCanvas.setDimensions({
|
let finalImageState: ImageStateType;
|
||||||
width: imageState.width,
|
const pendingCrop = getPendingCrop(fabricCanvas);
|
||||||
height: imageState.height,
|
if (pendingCrop) {
|
||||||
});
|
finalImageState = getNewImageStateFromCrop(
|
||||||
fabricCanvas.setZoom(1);
|
imageState,
|
||||||
const renderedCanvas = fabricCanvas.toCanvasElement();
|
pendingCrop
|
||||||
|
);
|
||||||
|
moveFabricObjectsForCrop(renderFabricCanvas, pendingCrop);
|
||||||
|
drawFabricBackgroundImage({
|
||||||
|
fabricCanvas: renderFabricCanvas,
|
||||||
|
image,
|
||||||
|
imageState: finalImageState,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
finalImageState = imageState;
|
||||||
|
}
|
||||||
|
|
||||||
fabricCanvas.setDimensions({
|
renderFabricCanvas.setDimensions({
|
||||||
width: imageState.width * zoom,
|
width: finalImageState.width,
|
||||||
height: imageState.height * zoom,
|
height: finalImageState.height,
|
||||||
});
|
});
|
||||||
fabricCanvas.setZoom(zoom);
|
renderFabricCanvas.setZoom(1);
|
||||||
|
const renderedCanvas = renderFabricCanvas.toCanvasElement();
|
||||||
|
|
||||||
data = await canvasToBytes(renderedCanvas);
|
data = await canvasToBytes(renderedCanvas);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
|
@ -1183,3 +1115,129 @@ export const MediaEditor = ({
|
||||||
portal
|
portal
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
function getPendingCrop(
|
||||||
|
fabricCanvas: fabric.Canvas
|
||||||
|
): undefined | PendingCropType {
|
||||||
|
const activeObject = fabricCanvas.getActiveObject();
|
||||||
|
return activeObject instanceof MediaEditorFabricCropRect
|
||||||
|
? activeObject.getBoundingRect(true)
|
||||||
|
: undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getNewImageStateFromCrop(
|
||||||
|
state: Readonly<ImageStateType>,
|
||||||
|
{ left, height, top, width }: Readonly<PendingCropType>
|
||||||
|
): ImageStateType {
|
||||||
|
let cropX: number;
|
||||||
|
let cropY: number;
|
||||||
|
switch (state.angle) {
|
||||||
|
case 0:
|
||||||
|
cropX = state.cropX + left;
|
||||||
|
cropY = state.cropY + top;
|
||||||
|
break;
|
||||||
|
case 90:
|
||||||
|
cropX = state.cropX + top;
|
||||||
|
cropY = state.cropY + (state.width - (left + width));
|
||||||
|
break;
|
||||||
|
case 180:
|
||||||
|
cropX = state.cropX + (state.width - (left + width));
|
||||||
|
cropY = state.cropY + (state.height - (top + height));
|
||||||
|
break;
|
||||||
|
case 270:
|
||||||
|
cropX = state.cropX + (state.height - (top + height));
|
||||||
|
cropY = state.cropY + left;
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
throw new Error('Unexpected angle');
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
cropX,
|
||||||
|
cropY,
|
||||||
|
height,
|
||||||
|
width,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function cloneFabricCanvas(original: fabric.Canvas): Promise<fabric.Canvas> {
|
||||||
|
return new Promise(resolve => {
|
||||||
|
original.clone(resolve);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function moveFabricObjectsForCrop(
|
||||||
|
fabricCanvas: fabric.Canvas,
|
||||||
|
{ left, top }: Readonly<PendingCropType>
|
||||||
|
): void {
|
||||||
|
fabricCanvas.getObjects().forEach(obj => {
|
||||||
|
const { x, y } = obj.getCenterPoint();
|
||||||
|
|
||||||
|
const translatedCenter = new fabric.Point(x - left, y - top);
|
||||||
|
obj.setPositionByOrigin(translatedCenter, 'center', 'center');
|
||||||
|
obj.setCoords();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function drawFabricBackgroundImage({
|
||||||
|
fabricCanvas,
|
||||||
|
image,
|
||||||
|
imageState,
|
||||||
|
}: Readonly<{
|
||||||
|
fabricCanvas: fabric.Canvas;
|
||||||
|
image: HTMLImageElement;
|
||||||
|
imageState: Readonly<ImageStateType>;
|
||||||
|
}>): void {
|
||||||
|
const backgroundImage = new fabric.Image(image, {
|
||||||
|
canvas: fabricCanvas,
|
||||||
|
height: imageState.height || image.height,
|
||||||
|
width: imageState.width || image.width,
|
||||||
|
});
|
||||||
|
|
||||||
|
let left: number;
|
||||||
|
let top: number;
|
||||||
|
switch (imageState.angle) {
|
||||||
|
case 0:
|
||||||
|
left = 0;
|
||||||
|
top = 0;
|
||||||
|
break;
|
||||||
|
case 90:
|
||||||
|
left = imageState.width;
|
||||||
|
top = 0;
|
||||||
|
break;
|
||||||
|
case 180:
|
||||||
|
left = imageState.width;
|
||||||
|
top = imageState.height;
|
||||||
|
break;
|
||||||
|
case 270:
|
||||||
|
left = 0;
|
||||||
|
top = imageState.height;
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
throw new Error('Unexpected angle');
|
||||||
|
}
|
||||||
|
|
||||||
|
let { height, width } = imageState;
|
||||||
|
if (imageState.angle % 180) {
|
||||||
|
[width, height] = [height, width];
|
||||||
|
}
|
||||||
|
|
||||||
|
fabricCanvas.setBackgroundImage(
|
||||||
|
backgroundImage,
|
||||||
|
fabricCanvas.requestRenderAll.bind(fabricCanvas),
|
||||||
|
{
|
||||||
|
angle: imageState.angle,
|
||||||
|
cropX: imageState.cropX,
|
||||||
|
cropY: imageState.cropY,
|
||||||
|
flipX: imageState.flipX,
|
||||||
|
flipY: imageState.flipY,
|
||||||
|
left,
|
||||||
|
top,
|
||||||
|
originX: 'left',
|
||||||
|
originY: 'top',
|
||||||
|
width,
|
||||||
|
height,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
Loading…
Reference in New Issue