cursor.ts 4.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105
  1. import { CURSOR_TYPE, MIME_TYPES, THEME } from "./constants";
  2. import OpenColor from "open-color";
  3. import type { AppState, DataURL } from "./types";
  4. import { isHandToolActive, isEraserActive } from "./appState";
  5. const laserPointerCursorSVG_tag = `<svg viewBox="0 0 24 24" stroke-width="1" width="28" height="28" xmlns="http://www.w3.org/2000/svg">`;
  6. const laserPointerCursorBackgroundSVG = `<path d="M6.164 11.755a5.314 5.314 0 0 1-4.932-5.298 5.314 5.314 0 0 1 5.311-5.311 5.314 5.314 0 0 1 5.307 5.113l8.773 8.773a3.322 3.322 0 0 1 0 4.696l-.895.895a3.322 3.322 0 0 1-4.696 0l-8.868-8.868Z" style="fill:#fff"/>`;
  7. const laserPointerCursorIconSVG = `<path stroke="#1b1b1f" fill="#fff" d="m7.868 11.113 7.773 7.774a2.359 2.359 0 0 0 1.667.691 2.368 2.368 0 0 0 2.357-2.358c0-.625-.248-1.225-.69-1.667L11.201 7.78 9.558 9.469l-1.69 1.643v.001Zm10.273 3.606-3.333 3.333m-3.25-6.583 2 2m-7-7 3 3M3.664 3.625l1 1M2.529 6.922l1.407-.144m5.735-2.932-1.118.866M4.285 9.823l.758-1.194m1.863-6.207-.13 1.408"/>`;
  8. const laserPointerCursorDataURL_lightMode = `data:${
  9. MIME_TYPES.svg
  10. },${encodeURIComponent(
  11. `${laserPointerCursorSVG_tag}${laserPointerCursorIconSVG}</svg>`,
  12. )}`;
  13. const laserPointerCursorDataURL_darkMode = `data:${
  14. MIME_TYPES.svg
  15. },${encodeURIComponent(
  16. `${laserPointerCursorSVG_tag}${laserPointerCursorBackgroundSVG}${laserPointerCursorIconSVG}</svg>`,
  17. )}`;
  18. export const resetCursor = (interactiveCanvas: HTMLCanvasElement | null) => {
  19. if (interactiveCanvas) {
  20. interactiveCanvas.style.cursor = "";
  21. }
  22. };
  23. export const setCursor = (
  24. interactiveCanvas: HTMLCanvasElement | null,
  25. cursor: string,
  26. ) => {
  27. if (interactiveCanvas) {
  28. interactiveCanvas.style.cursor = cursor;
  29. }
  30. };
  31. let eraserCanvasCache: any;
  32. let previewDataURL: string;
  33. export const setEraserCursor = (
  34. interactiveCanvas: HTMLCanvasElement | null,
  35. theme: AppState["theme"],
  36. ) => {
  37. const cursorImageSizePx = 20;
  38. const drawCanvas = () => {
  39. const isDarkTheme = theme === THEME.DARK;
  40. eraserCanvasCache = document.createElement("canvas");
  41. eraserCanvasCache.theme = theme;
  42. eraserCanvasCache.height = cursorImageSizePx;
  43. eraserCanvasCache.width = cursorImageSizePx;
  44. const context = eraserCanvasCache.getContext("2d")!;
  45. context.lineWidth = 1;
  46. context.beginPath();
  47. context.arc(
  48. eraserCanvasCache.width / 2,
  49. eraserCanvasCache.height / 2,
  50. 5,
  51. 0,
  52. 2 * Math.PI,
  53. );
  54. context.fillStyle = isDarkTheme ? OpenColor.black : OpenColor.white;
  55. context.fill();
  56. context.strokeStyle = isDarkTheme ? OpenColor.white : OpenColor.black;
  57. context.stroke();
  58. previewDataURL = eraserCanvasCache.toDataURL(MIME_TYPES.svg) as DataURL;
  59. };
  60. if (!eraserCanvasCache || eraserCanvasCache.theme !== theme) {
  61. drawCanvas();
  62. }
  63. setCursor(
  64. interactiveCanvas,
  65. `url(${previewDataURL}) ${cursorImageSizePx / 2} ${
  66. cursorImageSizePx / 2
  67. }, auto`,
  68. );
  69. };
  70. export const setCursorForShape = (
  71. interactiveCanvas: HTMLCanvasElement | null,
  72. appState: Pick<AppState, "activeTool" | "theme">,
  73. ) => {
  74. if (!interactiveCanvas) {
  75. return;
  76. }
  77. if (appState.activeTool.type === "selection") {
  78. resetCursor(interactiveCanvas);
  79. } else if (isHandToolActive(appState)) {
  80. interactiveCanvas.style.cursor = CURSOR_TYPE.GRAB;
  81. } else if (isEraserActive(appState)) {
  82. setEraserCursor(interactiveCanvas, appState.theme);
  83. // do nothing if image tool is selected which suggests there's
  84. // a image-preview set as the cursor
  85. // Ignore custom type as well and let host decide
  86. } else if (appState.activeTool.type === "laser") {
  87. const url =
  88. appState.theme === THEME.LIGHT
  89. ? laserPointerCursorDataURL_lightMode
  90. : laserPointerCursorDataURL_darkMode;
  91. interactiveCanvas.style.cursor = `url(${url}), auto`;
  92. } else if (!["image", "custom"].includes(appState.activeTool.type)) {
  93. interactiveCanvas.style.cursor = CURSOR_TYPE.CROSSHAIR;
  94. } else if (appState.activeTool.type !== "image") {
  95. interactiveCanvas.style.cursor = CURSOR_TYPE.AUTO;
  96. }
  97. };