render crop
This commit is contained in:
parent
f4bebaaa50
commit
71ed96eabb
@ -203,6 +203,8 @@ const getRelevantAppStateProps = (
|
|||||||
snapLines: appState.snapLines,
|
snapLines: appState.snapLines,
|
||||||
zenModeEnabled: appState.zenModeEnabled,
|
zenModeEnabled: appState.zenModeEnabled,
|
||||||
editingTextElement: appState.editingTextElement,
|
editingTextElement: appState.editingTextElement,
|
||||||
|
isCropping: appState.isCropping,
|
||||||
|
croppingElement: appState.croppingElement,
|
||||||
});
|
});
|
||||||
|
|
||||||
const areEqual = (
|
const areEqual = (
|
||||||
|
@ -107,6 +107,8 @@ const getRelevantAppStateProps = (
|
|||||||
frameToHighlight: appState.frameToHighlight,
|
frameToHighlight: appState.frameToHighlight,
|
||||||
editingGroupId: appState.editingGroupId,
|
editingGroupId: appState.editingGroupId,
|
||||||
currentHoveredFontFamily: appState.currentHoveredFontFamily,
|
currentHoveredFontFamily: appState.currentHoveredFontFamily,
|
||||||
|
isCropping: appState.isCropping,
|
||||||
|
croppingElement: appState.croppingElement,
|
||||||
});
|
});
|
||||||
|
|
||||||
const areEqual = (
|
const areEqual = (
|
||||||
|
@ -59,6 +59,7 @@ import type {
|
|||||||
ExcalidrawBindableElement,
|
ExcalidrawBindableElement,
|
||||||
ExcalidrawElement,
|
ExcalidrawElement,
|
||||||
ExcalidrawFrameLikeElement,
|
ExcalidrawFrameLikeElement,
|
||||||
|
ExcalidrawImageElement,
|
||||||
ExcalidrawLinearElement,
|
ExcalidrawLinearElement,
|
||||||
ExcalidrawTextElement,
|
ExcalidrawTextElement,
|
||||||
GroupId,
|
GroupId,
|
||||||
@ -591,6 +592,96 @@ const renderTransformHandles = (
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const renderCropHandles = (
|
||||||
|
context: CanvasRenderingContext2D,
|
||||||
|
renderConfig: InteractiveCanvasRenderConfig,
|
||||||
|
appState: InteractiveCanvasAppState,
|
||||||
|
croppingElement: ExcalidrawImageElement,
|
||||||
|
elementsMap: ElementsMap,
|
||||||
|
): void => {
|
||||||
|
const lineWidth = 3 / appState.zoom.value;
|
||||||
|
const length = 15 / appState.zoom.value;
|
||||||
|
|
||||||
|
const [x1, y1, x2, y2, cx, cy] = getElementAbsoluteCoords(
|
||||||
|
croppingElement,
|
||||||
|
elementsMap,
|
||||||
|
);
|
||||||
|
const halfWidth =
|
||||||
|
cx - x1 + (DEFAULT_TRANSFORM_HANDLE_SPACING * 2) / appState.zoom.value;
|
||||||
|
const halfHeight =
|
||||||
|
cy - y1 + (DEFAULT_TRANSFORM_HANDLE_SPACING * 2) / appState.zoom.value;
|
||||||
|
|
||||||
|
context.save();
|
||||||
|
context.fillStyle = renderConfig.selectionColor;
|
||||||
|
context.strokeStyle = renderConfig.selectionColor;
|
||||||
|
context.lineWidth = lineWidth;
|
||||||
|
|
||||||
|
const halfLineWidth = lineWidth / 2;
|
||||||
|
|
||||||
|
const handles: Array<
|
||||||
|
[
|
||||||
|
[number, number],
|
||||||
|
[number, number],
|
||||||
|
[number, number],
|
||||||
|
[number, number],
|
||||||
|
[number, number],
|
||||||
|
]
|
||||||
|
> = [
|
||||||
|
[
|
||||||
|
// x, y
|
||||||
|
[-halfWidth, -halfHeight],
|
||||||
|
// first start and t0
|
||||||
|
[0, halfLineWidth],
|
||||||
|
[length, halfLineWidth],
|
||||||
|
// second start and to
|
||||||
|
[halfLineWidth, 0],
|
||||||
|
[halfLineWidth, length - halfLineWidth],
|
||||||
|
],
|
||||||
|
[
|
||||||
|
[halfWidth - halfLineWidth, -halfHeight + halfLineWidth],
|
||||||
|
[halfLineWidth, 0],
|
||||||
|
[-length + halfLineWidth, 0],
|
||||||
|
[0, -halfLineWidth],
|
||||||
|
[0, length - lineWidth],
|
||||||
|
],
|
||||||
|
[
|
||||||
|
[-halfWidth, halfHeight],
|
||||||
|
[0, -halfLineWidth],
|
||||||
|
[length, -halfLineWidth],
|
||||||
|
[halfLineWidth, 0],
|
||||||
|
[halfLineWidth, -length + halfLineWidth],
|
||||||
|
],
|
||||||
|
[
|
||||||
|
[halfWidth - halfLineWidth, halfHeight - halfLineWidth],
|
||||||
|
[halfLineWidth, 0],
|
||||||
|
[-length + halfLineWidth, 0],
|
||||||
|
[0, halfLineWidth],
|
||||||
|
[0, -length + lineWidth],
|
||||||
|
],
|
||||||
|
];
|
||||||
|
|
||||||
|
handles.forEach((handle) => {
|
||||||
|
const [[x, y], [x1s, y1s], [x1t, y1t], [x2s, y2s], [x2t, y2t]] = handle;
|
||||||
|
|
||||||
|
context.save();
|
||||||
|
context.translate(cx, cy);
|
||||||
|
context.rotate(croppingElement.angle);
|
||||||
|
|
||||||
|
context.beginPath();
|
||||||
|
context.moveTo(x + x1s, y + y1s);
|
||||||
|
context.lineTo(x + x1t, y + y1t);
|
||||||
|
context.stroke();
|
||||||
|
|
||||||
|
context.beginPath();
|
||||||
|
context.moveTo(x + x2s, y + y2s);
|
||||||
|
context.lineTo(x + x2t, y + y2t);
|
||||||
|
context.stroke();
|
||||||
|
context.restore();
|
||||||
|
});
|
||||||
|
|
||||||
|
context.restore();
|
||||||
|
};
|
||||||
|
|
||||||
const renderTextBox = (
|
const renderTextBox = (
|
||||||
text: NonDeleted<ExcalidrawTextElement>,
|
text: NonDeleted<ExcalidrawTextElement>,
|
||||||
context: CanvasRenderingContext2D,
|
context: CanvasRenderingContext2D,
|
||||||
@ -898,7 +989,9 @@ const _renderInteractiveScene = ({
|
|||||||
!appState.viewModeEnabled &&
|
!appState.viewModeEnabled &&
|
||||||
showBoundingBox &&
|
showBoundingBox &&
|
||||||
// do not show transform handles when text is being edited
|
// do not show transform handles when text is being edited
|
||||||
!isTextElement(appState.editingTextElement)
|
!isTextElement(appState.editingTextElement) &&
|
||||||
|
// do not show transform handles when image is being cropped
|
||||||
|
!appState.croppingElement
|
||||||
) {
|
) {
|
||||||
renderTransformHandles(
|
renderTransformHandles(
|
||||||
context,
|
context,
|
||||||
@ -908,6 +1001,16 @@ const _renderInteractiveScene = ({
|
|||||||
selectedElements[0].angle,
|
selectedElements[0].angle,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (appState.isCropping && appState.croppingElement) {
|
||||||
|
renderCropHandles(
|
||||||
|
context,
|
||||||
|
renderConfig,
|
||||||
|
appState,
|
||||||
|
appState.croppingElement,
|
||||||
|
elementsMap,
|
||||||
|
);
|
||||||
|
}
|
||||||
} else if (selectedElements.length > 1 && !appState.isRotating) {
|
} else if (selectedElements.length > 1 && !appState.isRotating) {
|
||||||
const dashedLinePadding =
|
const dashedLinePadding =
|
||||||
(DEFAULT_TRANSFORM_HANDLE_SPACING * 2) / appState.zoom.value;
|
(DEFAULT_TRANSFORM_HANDLE_SPACING * 2) / appState.zoom.value;
|
||||||
|
@ -17,6 +17,7 @@ import {
|
|||||||
isArrowElement,
|
isArrowElement,
|
||||||
hasBoundTextElement,
|
hasBoundTextElement,
|
||||||
isMagicFrameElement,
|
isMagicFrameElement,
|
||||||
|
isImageElement,
|
||||||
} from "../element/typeChecks";
|
} from "../element/typeChecks";
|
||||||
import { getElementAbsoluteCoords } from "../element/bounds";
|
import { getElementAbsoluteCoords } from "../element/bounds";
|
||||||
import type { RoughCanvas } from "roughjs/bin/canvas";
|
import type { RoughCanvas } from "roughjs/bin/canvas";
|
||||||
@ -61,6 +62,7 @@ import { ShapeCache } from "../scene/ShapeCache";
|
|||||||
import { getVerticalOffset } from "../fonts";
|
import { getVerticalOffset } from "../fonts";
|
||||||
import { isRightAngleRads } from "../../math";
|
import { isRightAngleRads } from "../../math";
|
||||||
import { getCornerRadius } from "../shapes";
|
import { getCornerRadius } from "../shapes";
|
||||||
|
import { getUncroppedImageElement } from "../element/cropElement";
|
||||||
|
|
||||||
// using a stronger invert (100% vs our regular 93%) and saturate
|
// using a stronger invert (100% vs our regular 93%) and saturate
|
||||||
// as a temp hack to make images in dark theme look closer to original
|
// as a temp hack to make images in dark theme look closer to original
|
||||||
@ -434,8 +436,26 @@ const drawElementOnCanvas = (
|
|||||||
);
|
);
|
||||||
context.clip();
|
context.clip();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TODO: check why only croppingElement has the latest update
|
||||||
|
const { x, y, width, height } = element.crop
|
||||||
|
? element.crop
|
||||||
|
: element === appState.croppingElement &&
|
||||||
|
appState.croppingElement.crop
|
||||||
|
? appState.croppingElement.crop
|
||||||
|
: {
|
||||||
|
x: 0,
|
||||||
|
y: 0,
|
||||||
|
width: element.naturalWidth,
|
||||||
|
height: element.naturalHeight,
|
||||||
|
};
|
||||||
|
|
||||||
context.drawImage(
|
context.drawImage(
|
||||||
img,
|
img,
|
||||||
|
x,
|
||||||
|
y,
|
||||||
|
width,
|
||||||
|
height,
|
||||||
0 /* hardcoded for the selection box*/,
|
0 /* hardcoded for the selection box*/,
|
||||||
0,
|
0,
|
||||||
element.width,
|
element.width,
|
||||||
@ -921,14 +941,53 @@ export const renderElement = (
|
|||||||
context.imageSmoothingEnabled = false;
|
context.imageSmoothingEnabled = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
drawElementFromCanvas(
|
if (
|
||||||
elementWithCanvas,
|
element.id === appState.croppingElement?.id &&
|
||||||
context,
|
isImageElement(elementWithCanvas.element) &&
|
||||||
|
elementWithCanvas.element.crop !== null
|
||||||
|
) {
|
||||||
|
context.save();
|
||||||
|
context.globalAlpha = 0.1;
|
||||||
|
|
||||||
|
const uncroppedElementCanvas = generateElementCanvas(
|
||||||
|
getUncroppedImageElement(elementWithCanvas.element, elementsMap),
|
||||||
|
allElementsMap,
|
||||||
|
appState.zoom,
|
||||||
|
renderConfig,
|
||||||
|
appState,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (uncroppedElementCanvas) {
|
||||||
|
drawElementFromCanvas(
|
||||||
|
uncroppedElementCanvas,
|
||||||
|
context,
|
||||||
|
renderConfig,
|
||||||
|
appState,
|
||||||
|
allElementsMap,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
context.restore();
|
||||||
|
}
|
||||||
|
|
||||||
|
const _elementWithCanvas = generateElementCanvas(
|
||||||
|
elementWithCanvas.element,
|
||||||
|
allElementsMap,
|
||||||
|
appState.zoom,
|
||||||
renderConfig,
|
renderConfig,
|
||||||
appState,
|
appState,
|
||||||
allElementsMap,
|
|
||||||
);
|
);
|
||||||
|
|
||||||
|
if (_elementWithCanvas) {
|
||||||
|
drawElementFromCanvas(
|
||||||
|
_elementWithCanvas,
|
||||||
|
context,
|
||||||
|
renderConfig,
|
||||||
|
appState,
|
||||||
|
allElementsMap,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
// reset
|
// reset
|
||||||
context.imageSmoothingEnabled = currentImageSmoothingStatus;
|
context.imageSmoothingEnabled = currentImageSmoothingStatus;
|
||||||
}
|
}
|
||||||
|
@ -176,6 +176,9 @@ export type StaticCanvasAppState = Readonly<
|
|||||||
gridStep: AppState["gridStep"];
|
gridStep: AppState["gridStep"];
|
||||||
frameRendering: AppState["frameRendering"];
|
frameRendering: AppState["frameRendering"];
|
||||||
currentHoveredFontFamily: AppState["currentHoveredFontFamily"];
|
currentHoveredFontFamily: AppState["currentHoveredFontFamily"];
|
||||||
|
// Cropping
|
||||||
|
isCropping: AppState["isCropping"];
|
||||||
|
croppingElement: AppState["croppingElement"];
|
||||||
}
|
}
|
||||||
>;
|
>;
|
||||||
|
|
||||||
@ -198,6 +201,9 @@ export type InteractiveCanvasAppState = Readonly<
|
|||||||
snapLines: AppState["snapLines"];
|
snapLines: AppState["snapLines"];
|
||||||
zenModeEnabled: AppState["zenModeEnabled"];
|
zenModeEnabled: AppState["zenModeEnabled"];
|
||||||
editingTextElement: AppState["editingTextElement"];
|
editingTextElement: AppState["editingTextElement"];
|
||||||
|
// Cropping
|
||||||
|
isCropping: AppState["isCropping"];
|
||||||
|
croppingElement: AppState["croppingElement"];
|
||||||
}
|
}
|
||||||
>;
|
>;
|
||||||
|
|
||||||
@ -671,6 +677,12 @@ export type PointerDownState = Readonly<{
|
|||||||
// This is a center point of selected elements determined on the initial pointer down event (for rotation only)
|
// This is a center point of selected elements determined on the initial pointer down event (for rotation only)
|
||||||
center: { x: number; y: number };
|
center: { x: number; y: number };
|
||||||
};
|
};
|
||||||
|
crop: {
|
||||||
|
handleType: MaybeTransformHandleType;
|
||||||
|
isCropping: boolean;
|
||||||
|
offset: { x: number; y: number };
|
||||||
|
complete: boolean;
|
||||||
|
};
|
||||||
hit: {
|
hit: {
|
||||||
// The element the pointer is "hitting", is determined on the initial
|
// The element the pointer is "hitting", is determined on the initial
|
||||||
// pointer down event
|
// pointer down event
|
||||||
|
Loading…
x
Reference in New Issue
Block a user