render crop

pull/8613/head
Ryan Di 5 months ago
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,13 +941,52 @@ export const renderElement = (
context.imageSmoothingEnabled = false; context.imageSmoothingEnabled = false;
} }
if (
element.id === appState.croppingElement?.id &&
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( drawElementFromCanvas(
elementWithCanvas, uncroppedElementCanvas,
context,
renderConfig,
appState,
allElementsMap,
);
}
context.restore();
}
const _elementWithCanvas = generateElementCanvas(
elementWithCanvas.element,
allElementsMap,
appState.zoom,
renderConfig,
appState,
);
if (_elementWithCanvas) {
drawElementFromCanvas(
_elementWithCanvas,
context, context,
renderConfig, renderConfig,
appState, appState,
allElementsMap, 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…
Cancel
Save