feat: update eraser cursor (#4922)

* feat: update eraser cursor

* fix dark theme

* check before adding active class

* use custom cursor instead of DOM manipulation

* cache canvas and redraw only when theme changes

* use oc colors

* remove

* cache preview data url

* increase linwidth

* update coords for cursor

* add white 2px outline

* improvements

* use 1px line width 6px radius for outer

* improve
This commit is contained in:
Aakansha Doshi 2022-03-15 20:56:39 +05:30 committed by GitHub
parent 6d45430344
commit 558227f744
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 71 additions and 20 deletions

View File

@ -185,11 +185,13 @@ export const ShapesSwitcher = ({
elementType,
setAppState,
onImageAction,
appState,
}: {
canvas: HTMLCanvasElement | null;
elementType: AppState["elementType"];
setAppState: React.Component<any, AppState>["setState"];
onImageAction: (data: { pointerType: PointerType | null }) => void;
appState: AppState;
}) => (
<>
{SHAPES.map(({ value, icon, key }, index) => {
@ -217,7 +219,7 @@ export const ShapesSwitcher = ({
multiElement: null,
selectedElementIds: {},
});
setCursorForShape(canvas, value);
setCursorForShape(canvas, { ...appState, elementType: value });
if (value === "image") {
onImageAction({ pointerType });
}

View File

@ -214,6 +214,7 @@ import {
withBatchedUpdates,
wrapEvent,
withBatchedUpdatesThrottled,
setEraserCursor,
} from "../utils";
import ContextMenu, { ContextMenuOption } from "./ContextMenu";
import LayerUI from "./LayerUI";
@ -1051,6 +1052,9 @@ class App extends React.Component<AppProps, AppState> {
) {
this.setState({ elementType: "selection" });
}
if (prevState.theme !== this.state.theme) {
setEraserCursor(this.canvas, this.state.theme);
}
// Hide hyperlink popup if shown when element type is not selection
if (
prevState.elementType === "selection" &&
@ -1873,7 +1877,7 @@ class App extends React.Component<AppProps, AppState> {
} else if (this.state.elementType === "selection") {
resetCursor(this.canvas);
} else {
setCursorForShape(this.canvas, this.state.elementType);
setCursorForShape(this.canvas, this.state);
this.setState({
selectedElementIds: {},
selectedGroupIds: {},
@ -1899,7 +1903,7 @@ class App extends React.Component<AppProps, AppState> {
private selectShapeTool(elementType: AppState["elementType"]) {
if (!isHoldingSpace) {
setCursorForShape(this.canvas, elementType);
setCursorForShape(this.canvas, this.state);
}
if (isToolIcon(document.activeElement)) {
this.focusContainer();
@ -2043,7 +2047,7 @@ class App extends React.Component<AppProps, AppState> {
editingElement: null,
});
if (this.state.elementLocked) {
setCursorForShape(this.canvas, this.state.elementType);
setCursorForShape(this.canvas, this.state);
}
this.focusContainer();
@ -2525,7 +2529,7 @@ class App extends React.Component<AppProps, AppState> {
if (isOverScrollBar) {
resetCursor(this.canvas);
} else {
setCursorForShape(this.canvas, this.state.elementType);
setCursorForShape(this.canvas, this.state);
}
}
@ -2575,7 +2579,7 @@ class App extends React.Component<AppProps, AppState> {
const { points, lastCommittedPoint } = multiElement;
const lastPoint = points[points.length - 1];
setCursorForShape(this.canvas, this.state.elementType);
setCursorForShape(this.canvas, this.state);
if (lastPoint === lastCommittedPoint) {
// if we haven't yet created a temp point and we're beyond commit-zone
@ -2689,7 +2693,9 @@ class App extends React.Component<AppProps, AppState> {
scenePointer,
hitElement,
);
if (isEraserActive(this.state)) {
return;
}
if (
this.hitLinkElement &&
!this.state.selectedElementIds[this.hitLinkElement.id]
@ -2706,8 +2712,6 @@ class App extends React.Component<AppProps, AppState> {
!this.state.showHyperlinkPopup
) {
this.setState({ showHyperlinkPopup: "info" });
} else if (isEraserActive(this.state)) {
setCursor(this.canvas, CURSOR_TYPE.AUTO);
} else if (this.state.elementType === "text") {
setCursor(
this.canvas,
@ -2998,8 +3002,6 @@ class App extends React.Component<AppProps, AppState> {
hitElement,
);
}
if (isEraserActive(this.state)) {
}
if (
this.hitLinkElement &&
!this.state.selectedElementIds[this.hitLinkElement.id]
@ -3128,7 +3130,7 @@ class App extends React.Component<AppProps, AppState> {
if (this.state.viewModeEnabled) {
setCursor(this.canvas, CURSOR_TYPE.GRAB);
} else {
setCursorForShape(this.canvas, this.state.elementType);
setCursorForShape(this.canvas, this.state);
}
}
this.setState({
@ -3253,7 +3255,7 @@ class App extends React.Component<AppProps, AppState> {
const onPointerUp = withBatchedUpdates(() => {
isDraggingScrollBar = false;
setCursorForShape(this.canvas, this.state.elementType);
setCursorForShape(this.canvas, this.state);
lastPointerUp = null;
this.setState({
cursorButton: "up",

View File

@ -343,6 +343,7 @@ const LayerUI = ({
{heading}
<Stack.Row gap={1}>
<ShapesSwitcher
appState={appState}
canvas={canvas}
elementType={appState.elementType}
setAppState={setAppState}

View File

@ -72,6 +72,7 @@ export const MobileMenu = ({
{heading}
<Stack.Row gap={1}>
<ShapesSwitcher
appState={appState}
canvas={canvas}
elementType={appState.elementType}
setAppState={setAppState}

View File

@ -1,13 +1,17 @@
import oc from "open-color";
import colors from "./colors";
import {
CURSOR_TYPE,
DEFAULT_VERSION,
EVENT,
FONT_FAMILY,
MIME_TYPES,
THEME,
WINDOWS_EMOJI_FALLBACK_FONT,
} from "./constants";
import { FontFamilyValues, FontString } from "./element/types";
import { Zoom } from "./types";
import { AppState, DataURL, Zoom } from "./types";
import { unstable_batchedUpdates } from "react-dom";
import { isDarwin } from "./keys";
@ -215,21 +219,62 @@ export const setCursor = (canvas: HTMLCanvasElement | null, cursor: string) => {
}
};
let eraserCanvasCache: any;
let previewDataURL: string;
export const setEraserCursor = (
canvas: HTMLCanvasElement | null,
theme: AppState["theme"],
) => {
const cursorImageSizePx = 20;
const drawCanvas = () => {
const isDarkTheme = theme === THEME.DARK;
eraserCanvasCache = document.createElement("canvas");
eraserCanvasCache.theme = theme;
eraserCanvasCache.height = cursorImageSizePx;
eraserCanvasCache.width = cursorImageSizePx;
const context = eraserCanvasCache.getContext("2d")!;
context.lineWidth = 1;
context.beginPath();
context.arc(
eraserCanvasCache.width / 2,
eraserCanvasCache.height / 2,
5,
0,
2 * Math.PI,
);
context.fillStyle = isDarkTheme ? oc.black : oc.white;
context.fill();
context.strokeStyle = isDarkTheme ? oc.white : oc.black;
context.stroke();
previewDataURL = eraserCanvasCache.toDataURL(MIME_TYPES.svg) as DataURL;
};
if (!eraserCanvasCache || eraserCanvasCache.theme !== theme) {
drawCanvas();
}
setCursor(
canvas,
`url(${previewDataURL}) ${cursorImageSizePx / 2} ${
cursorImageSizePx / 2
}, auto`,
);
};
export const setCursorForShape = (
canvas: HTMLCanvasElement | null,
shape: string,
appState: AppState,
) => {
if (!canvas) {
return;
}
if (shape === "selection") {
if (appState.elementType === "selection") {
resetCursor(canvas);
} else if (shape === "eraser") {
resetCursor(canvas);
} else if (appState.elementType === "eraser") {
setEraserCursor(canvas, appState.theme);
// do nothing if image tool is selected which suggests there's
// a image-preview set as the cursor
} else if (shape !== "image") {
} else if (appState.elementType !== "image") {
canvas.style.cursor = CURSOR_TYPE.CROSSHAIR;
}
};