瀏覽代碼

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
Aakansha Doshi 3 年之前
父節點
當前提交
558227f744
共有 5 個文件被更改,包括 71 次插入20 次删除
  1. 3 1
      src/components/Actions.tsx
  2. 14 12
      src/components/App.tsx
  3. 1 0
      src/components/LayerUI.tsx
  4. 1 0
      src/components/MobileMenu.tsx
  5. 52 7
      src/utils.ts

+ 3 - 1
src/components/Actions.tsx

@@ -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 });
             }

+ 14 - 12
src/components/App.tsx

@@ -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",

+ 1 - 0
src/components/LayerUI.tsx

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

+ 1 - 0
src/components/MobileMenu.tsx

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

+ 52 - 7
src/utils.ts

@@ -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;
   }
 };