feat: eye dropper (#6615)

This commit is contained in:
David Luzar 2023-06-02 17:06:11 +02:00 committed by GitHub
parent 644685a5a8
commit 079aa72475
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
31 changed files with 803 additions and 250 deletions

View file

@ -304,6 +304,7 @@ import { jotaiStore } from "../jotai";
import { activeConfirmDialogAtom } from "./ActiveConfirmDialog";
import { actionWrapTextInContainer } from "../actions/actionBoundText";
import BraveMeasureTextError from "./BraveMeasureTextError";
import { activeEyeDropperAtom } from "./EyeDropper";
const AppContext = React.createContext<AppClassProperties>(null!);
const AppPropsContext = React.createContext<AppProps>(null!);
@ -366,8 +367,6 @@ export const useExcalidrawActionManager = () =>
let didTapTwice: boolean = false;
let tappedTwiceTimer = 0;
let cursorX = 0;
let cursorY = 0;
let isHoldingSpace: boolean = false;
let isPanning: boolean = false;
let isDraggingScrollBar: boolean = false;
@ -425,7 +424,7 @@ class App extends React.Component<AppProps, AppState> {
hitLinkElement?: NonDeletedExcalidrawElement;
lastPointerDown: React.PointerEvent<HTMLCanvasElement> | null = null;
lastPointerUp: React.PointerEvent<HTMLElement> | PointerEvent | null = null;
lastScenePointer: { x: number; y: number } | null = null;
lastViewportPosition = { x: 0, y: 0 };
constructor(props: AppProps) {
super(props);
@ -634,6 +633,7 @@ class App extends React.Component<AppProps, AppState> {
</LayerUI>
<div className="excalidraw-textEditorContainer" />
<div className="excalidraw-contextMenuContainer" />
<div className="excalidraw-eye-dropper-container" />
{selectedElement.length === 1 &&
!this.state.contextMenu &&
this.state.showHyperlinkPopup && (
@ -724,6 +724,49 @@ class App extends React.Component<AppProps, AppState> {
}
};
private openEyeDropper = ({ type }: { type: "stroke" | "background" }) => {
jotaiStore.set(activeEyeDropperAtom, {
swapPreviewOnAlt: true,
previewType: type === "stroke" ? "strokeColor" : "backgroundColor",
onSelect: (color, event) => {
const shouldUpdateStrokeColor =
(type === "background" && event.altKey) ||
(type === "stroke" && !event.altKey);
const selectedElements = getSelectedElements(
this.scene.getElementsIncludingDeleted(),
this.state,
);
if (
!selectedElements.length ||
this.state.activeTool.type !== "selection"
) {
if (shouldUpdateStrokeColor) {
this.setState({
currentItemStrokeColor: color,
});
} else {
this.setState({
currentItemBackgroundColor: color,
});
}
} else {
this.updateScene({
elements: this.scene.getElementsIncludingDeleted().map((el) => {
if (this.state.selectedElementIds[el.id]) {
return newElementWith(el, {
[shouldUpdateStrokeColor ? "strokeColor" : "backgroundColor"]:
color,
});
}
return el;
}),
});
}
},
keepOpenOnAlt: false,
});
};
private syncActionResult = withBatchedUpdates(
(actionResult: ActionResult) => {
if (this.unmounted || actionResult === false) {
@ -1569,7 +1612,10 @@ class App extends React.Component<AppProps, AppState> {
return;
}
const elementUnderCursor = document.elementFromPoint(cursorX, cursorY);
const elementUnderCursor = document.elementFromPoint(
this.lastViewportPosition.x,
this.lastViewportPosition.y,
);
if (
event &&
(!(elementUnderCursor instanceof HTMLCanvasElement) ||
@ -1597,7 +1643,10 @@ class App extends React.Component<AppProps, AppState> {
// prefer spreadsheet data over image file (MS Office/Libre Office)
if (isSupportedImageFile(file) && !data.spreadsheet) {
const { x: sceneX, y: sceneY } = viewportCoordsToSceneCoords(
{ clientX: cursorX, clientY: cursorY },
{
clientX: this.lastViewportPosition.x,
clientY: this.lastViewportPosition.y,
},
this.state,
);
@ -1660,13 +1709,13 @@ class App extends React.Component<AppProps, AppState> {
typeof opts.position === "object"
? opts.position.clientX
: opts.position === "cursor"
? cursorX
? this.lastViewportPosition.x
: this.state.width / 2 + this.state.offsetLeft;
const clientY =
typeof opts.position === "object"
? opts.position.clientY
: opts.position === "cursor"
? cursorY
? this.lastViewportPosition.y
: this.state.height / 2 + this.state.offsetTop;
const { x, y } = viewportCoordsToSceneCoords(
@ -1750,7 +1799,10 @@ class App extends React.Component<AppProps, AppState> {
private addTextFromPaste(text: string, isPlainPaste = false) {
const { x, y } = viewportCoordsToSceneCoords(
{ clientX: cursorX, clientY: cursorY },
{
clientX: this.lastViewportPosition.x,
clientY: this.lastViewportPosition.y,
},
this.state,
);
@ -2083,8 +2135,8 @@ class App extends React.Component<AppProps, AppState> {
private updateCurrentCursorPosition = withBatchedUpdates(
(event: MouseEvent) => {
cursorX = event.clientX;
cursorY = event.clientY;
this.lastViewportPosition.x = event.clientX;
this.lastViewportPosition.y = event.clientY;
},
);
@ -2342,6 +2394,20 @@ class App extends React.Component<AppProps, AppState> {
) {
jotaiStore.set(activeConfirmDialogAtom, "clearCanvas");
}
// eye dropper
// -----------------------------------------------------------------------
const lowerCased = event.key.toLocaleLowerCase();
const isPickingStroke = lowerCased === KEYS.S && event.shiftKey;
const isPickingBackground =
event.key === KEYS.I || (lowerCased === KEYS.G && event.shiftKey);
if (isPickingStroke || isPickingBackground) {
this.openEyeDropper({
type: isPickingStroke ? "stroke" : "background",
});
}
// -----------------------------------------------------------------------
},
);
@ -2471,8 +2537,8 @@ class App extends React.Component<AppProps, AppState> {
this.setState((state) => ({
...getStateForZoom(
{
viewportX: cursorX,
viewportY: cursorY,
viewportX: this.lastViewportPosition.x,
viewportY: this.lastViewportPosition.y,
nextZoom: getNormalizedZoom(initialScale * event.scale),
},
state,
@ -6468,8 +6534,8 @@ class App extends React.Component<AppProps, AppState> {
this.translateCanvas((state) => ({
...getStateForZoom(
{
viewportX: cursorX,
viewportY: cursorY,
viewportX: this.lastViewportPosition.x,
viewportY: this.lastViewportPosition.y,
nextZoom: getNormalizedZoom(newZoom),
},
state,