From 6ec259a0e71174651bae95d4628138bf6fd68742 Mon Sep 17 00:00:00 2001 From: kj_sh604 Date: Sun, 15 Mar 2026 16:19:35 -0400 Subject: refactor: packages/ --- packages/excalidraw/cursor.ts | 105 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 105 insertions(+) create mode 100644 packages/excalidraw/cursor.ts (limited to 'packages/excalidraw/cursor.ts') diff --git a/packages/excalidraw/cursor.ts b/packages/excalidraw/cursor.ts new file mode 100644 index 0000000..759b7a4 --- /dev/null +++ b/packages/excalidraw/cursor.ts @@ -0,0 +1,105 @@ +import { CURSOR_TYPE, MIME_TYPES, THEME } from "./constants"; +import OpenColor from "open-color"; +import type { AppState, DataURL } from "./types"; +import { isHandToolActive, isEraserActive } from "./appState"; + +const laserPointerCursorSVG_tag = ``; +const laserPointerCursorBackgroundSVG = ``; +const laserPointerCursorIconSVG = ``; + +const laserPointerCursorDataURL_lightMode = `data:${ + MIME_TYPES.svg +},${encodeURIComponent( + `${laserPointerCursorSVG_tag}${laserPointerCursorIconSVG}`, +)}`; +const laserPointerCursorDataURL_darkMode = `data:${ + MIME_TYPES.svg +},${encodeURIComponent( + `${laserPointerCursorSVG_tag}${laserPointerCursorBackgroundSVG}${laserPointerCursorIconSVG}`, +)}`; + +export const resetCursor = (interactiveCanvas: HTMLCanvasElement | null) => { + if (interactiveCanvas) { + interactiveCanvas.style.cursor = ""; + } +}; + +export const setCursor = ( + interactiveCanvas: HTMLCanvasElement | null, + cursor: string, +) => { + if (interactiveCanvas) { + interactiveCanvas.style.cursor = cursor; + } +}; + +let eraserCanvasCache: any; +let previewDataURL: string; +export const setEraserCursor = ( + interactiveCanvas: 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 ? OpenColor.black : OpenColor.white; + context.fill(); + context.strokeStyle = isDarkTheme ? OpenColor.white : OpenColor.black; + context.stroke(); + previewDataURL = eraserCanvasCache.toDataURL(MIME_TYPES.svg) as DataURL; + }; + if (!eraserCanvasCache || eraserCanvasCache.theme !== theme) { + drawCanvas(); + } + + setCursor( + interactiveCanvas, + `url(${previewDataURL}) ${cursorImageSizePx / 2} ${ + cursorImageSizePx / 2 + }, auto`, + ); +}; + +export const setCursorForShape = ( + interactiveCanvas: HTMLCanvasElement | null, + appState: Pick, +) => { + if (!interactiveCanvas) { + return; + } + if (appState.activeTool.type === "selection") { + resetCursor(interactiveCanvas); + } else if (isHandToolActive(appState)) { + interactiveCanvas.style.cursor = CURSOR_TYPE.GRAB; + } else if (isEraserActive(appState)) { + setEraserCursor(interactiveCanvas, appState.theme); + // do nothing if image tool is selected which suggests there's + // a image-preview set as the cursor + // Ignore custom type as well and let host decide + } else if (appState.activeTool.type === "laser") { + const url = + appState.theme === THEME.LIGHT + ? laserPointerCursorDataURL_lightMode + : laserPointerCursorDataURL_darkMode; + interactiveCanvas.style.cursor = `url(${url}), auto`; + } else if (!["image", "custom"].includes(appState.activeTool.type)) { + interactiveCanvas.style.cursor = CURSOR_TYPE.CROSSHAIR; + } else if (appState.activeTool.type !== "image") { + interactiveCanvas.style.cursor = CURSOR_TYPE.AUTO; + } +}; -- cgit v1.2.3