render crop
This commit is contained in:
parent
f4bebaaa50
commit
71ed96eabb
@ -203,6 +203,8 @@ const getRelevantAppStateProps = (
|
||||
snapLines: appState.snapLines,
|
||||
zenModeEnabled: appState.zenModeEnabled,
|
||||
editingTextElement: appState.editingTextElement,
|
||||
isCropping: appState.isCropping,
|
||||
croppingElement: appState.croppingElement,
|
||||
});
|
||||
|
||||
const areEqual = (
|
||||
|
@ -107,6 +107,8 @@ const getRelevantAppStateProps = (
|
||||
frameToHighlight: appState.frameToHighlight,
|
||||
editingGroupId: appState.editingGroupId,
|
||||
currentHoveredFontFamily: appState.currentHoveredFontFamily,
|
||||
isCropping: appState.isCropping,
|
||||
croppingElement: appState.croppingElement,
|
||||
});
|
||||
|
||||
const areEqual = (
|
||||
|
@ -59,6 +59,7 @@ import type {
|
||||
ExcalidrawBindableElement,
|
||||
ExcalidrawElement,
|
||||
ExcalidrawFrameLikeElement,
|
||||
ExcalidrawImageElement,
|
||||
ExcalidrawLinearElement,
|
||||
ExcalidrawTextElement,
|
||||
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 = (
|
||||
text: NonDeleted<ExcalidrawTextElement>,
|
||||
context: CanvasRenderingContext2D,
|
||||
@ -898,7 +989,9 @@ const _renderInteractiveScene = ({
|
||||
!appState.viewModeEnabled &&
|
||||
showBoundingBox &&
|
||||
// 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(
|
||||
context,
|
||||
@ -908,6 +1001,16 @@ const _renderInteractiveScene = ({
|
||||
selectedElements[0].angle,
|
||||
);
|
||||
}
|
||||
|
||||
if (appState.isCropping && appState.croppingElement) {
|
||||
renderCropHandles(
|
||||
context,
|
||||
renderConfig,
|
||||
appState,
|
||||
appState.croppingElement,
|
||||
elementsMap,
|
||||
);
|
||||
}
|
||||
} else if (selectedElements.length > 1 && !appState.isRotating) {
|
||||
const dashedLinePadding =
|
||||
(DEFAULT_TRANSFORM_HANDLE_SPACING * 2) / appState.zoom.value;
|
||||
|
@ -17,6 +17,7 @@ import {
|
||||
isArrowElement,
|
||||
hasBoundTextElement,
|
||||
isMagicFrameElement,
|
||||
isImageElement,
|
||||
} from "../element/typeChecks";
|
||||
import { getElementAbsoluteCoords } from "../element/bounds";
|
||||
import type { RoughCanvas } from "roughjs/bin/canvas";
|
||||
@ -61,6 +62,7 @@ import { ShapeCache } from "../scene/ShapeCache";
|
||||
import { getVerticalOffset } from "../fonts";
|
||||
import { isRightAngleRads } from "../../math";
|
||||
import { getCornerRadius } from "../shapes";
|
||||
import { getUncroppedImageElement } from "../element/cropElement";
|
||||
|
||||
// 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
|
||||
@ -434,8 +436,26 @@ const drawElementOnCanvas = (
|
||||
);
|
||||
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(
|
||||
img,
|
||||
x,
|
||||
y,
|
||||
width,
|
||||
height,
|
||||
0 /* hardcoded for the selection box*/,
|
||||
0,
|
||||
element.width,
|
||||
@ -921,14 +941,53 @@ export const renderElement = (
|
||||
context.imageSmoothingEnabled = false;
|
||||
}
|
||||
|
||||
drawElementFromCanvas(
|
||||
elementWithCanvas,
|
||||
context,
|
||||
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(
|
||||
uncroppedElementCanvas,
|
||||
context,
|
||||
renderConfig,
|
||||
appState,
|
||||
allElementsMap,
|
||||
);
|
||||
}
|
||||
|
||||
context.restore();
|
||||
}
|
||||
|
||||
const _elementWithCanvas = generateElementCanvas(
|
||||
elementWithCanvas.element,
|
||||
allElementsMap,
|
||||
appState.zoom,
|
||||
renderConfig,
|
||||
appState,
|
||||
allElementsMap,
|
||||
);
|
||||
|
||||
if (_elementWithCanvas) {
|
||||
drawElementFromCanvas(
|
||||
_elementWithCanvas,
|
||||
context,
|
||||
renderConfig,
|
||||
appState,
|
||||
allElementsMap,
|
||||
);
|
||||
}
|
||||
|
||||
// reset
|
||||
context.imageSmoothingEnabled = currentImageSmoothingStatus;
|
||||
}
|
||||
|
@ -176,6 +176,9 @@ export type StaticCanvasAppState = Readonly<
|
||||
gridStep: AppState["gridStep"];
|
||||
frameRendering: AppState["frameRendering"];
|
||||
currentHoveredFontFamily: AppState["currentHoveredFontFamily"];
|
||||
// Cropping
|
||||
isCropping: AppState["isCropping"];
|
||||
croppingElement: AppState["croppingElement"];
|
||||
}
|
||||
>;
|
||||
|
||||
@ -198,6 +201,9 @@ export type InteractiveCanvasAppState = Readonly<
|
||||
snapLines: AppState["snapLines"];
|
||||
zenModeEnabled: AppState["zenModeEnabled"];
|
||||
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)
|
||||
center: { x: number; y: number };
|
||||
};
|
||||
crop: {
|
||||
handleType: MaybeTransformHandleType;
|
||||
isCropping: boolean;
|
||||
offset: { x: number; y: number };
|
||||
complete: boolean;
|
||||
};
|
||||
hit: {
|
||||
// The element the pointer is "hitting", is determined on the initial
|
||||
// pointer down event
|
||||
|
Loading…
x
Reference in New Issue
Block a user