utils.ts 5.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209
  1. import { FlooredNumber } from "./types";
  2. import { getZoomOrigin } from "./scene";
  3. import { CURSOR_TYPE } from "./constants";
  4. export const SVG_NS = "http://www.w3.org/2000/svg";
  5. let mockDateTime: string | null = null;
  6. export const setDateTimeForTests = (dateTime: string) => {
  7. mockDateTime = dateTime;
  8. };
  9. export const getDateTime = () => {
  10. if (mockDateTime) {
  11. return mockDateTime;
  12. }
  13. const date = new Date();
  14. const year = date.getFullYear();
  15. const month = `${date.getMonth() + 1}`.padStart(2, "0");
  16. const day = `${date.getDate()}`.padStart(2, "0");
  17. const hr = `${date.getHours()}`.padStart(2, "0");
  18. const min = `${date.getMinutes()}`.padStart(2, "0");
  19. return `${year}-${month}-${day}-${hr}${min}`;
  20. };
  21. export const capitalizeString = (str: string) =>
  22. str.charAt(0).toUpperCase() + str.slice(1);
  23. export const isToolIcon = (
  24. target: Element | EventTarget | null,
  25. ): target is HTMLElement =>
  26. target instanceof HTMLElement && target.className.includes("ToolIcon");
  27. export const isInputLike = (
  28. target: Element | EventTarget | null,
  29. ): target is
  30. | HTMLInputElement
  31. | HTMLTextAreaElement
  32. | HTMLSelectElement
  33. | HTMLBRElement
  34. | HTMLDivElement =>
  35. (target instanceof HTMLElement && target.dataset.type === "wysiwyg") ||
  36. target instanceof HTMLBRElement || // newline in wysiwyg
  37. target instanceof HTMLInputElement ||
  38. target instanceof HTMLTextAreaElement ||
  39. target instanceof HTMLSelectElement;
  40. export const isWritableElement = (
  41. target: Element | EventTarget | null,
  42. ): target is
  43. | HTMLInputElement
  44. | HTMLTextAreaElement
  45. | HTMLBRElement
  46. | HTMLDivElement =>
  47. (target instanceof HTMLElement && target.dataset.type === "wysiwyg") ||
  48. target instanceof HTMLBRElement || // newline in wysiwyg
  49. target instanceof HTMLTextAreaElement ||
  50. (target instanceof HTMLInputElement &&
  51. (target.type === "text" || target.type === "number"));
  52. // https://github.com/grassator/canvas-text-editor/blob/master/lib/FontMetrics.js
  53. export const measureText = (text: string, font: string) => {
  54. const line = document.createElement("div");
  55. const body = document.body;
  56. line.style.position = "absolute";
  57. line.style.whiteSpace = "pre";
  58. line.style.font = font;
  59. body.appendChild(line);
  60. // Now we can measure width and height of the letter
  61. line.innerText = text;
  62. const width = line.offsetWidth;
  63. const height = line.offsetHeight;
  64. // Now creating 1px sized item that will be aligned to baseline
  65. // to calculate baseline shift
  66. const span = document.createElement("span");
  67. span.style.display = "inline-block";
  68. span.style.overflow = "hidden";
  69. span.style.width = "1px";
  70. span.style.height = "1px";
  71. line.appendChild(span);
  72. // Baseline is important for positioning text on canvas
  73. const baseline = span.offsetTop + span.offsetHeight;
  74. document.body.removeChild(line);
  75. return { width, height, baseline };
  76. };
  77. export const debounce = <T extends any[]>(
  78. fn: (...args: T) => void,
  79. timeout: number,
  80. ) => {
  81. let handle = 0;
  82. let lastArgs: T;
  83. const ret = (...args: T) => {
  84. lastArgs = args;
  85. clearTimeout(handle);
  86. handle = window.setTimeout(() => fn(...args), timeout);
  87. };
  88. ret.flush = () => {
  89. clearTimeout(handle);
  90. fn(...lastArgs);
  91. };
  92. return ret;
  93. };
  94. export const selectNode = (node: Element) => {
  95. const selection = window.getSelection();
  96. if (selection) {
  97. const range = document.createRange();
  98. range.selectNodeContents(node);
  99. selection.removeAllRanges();
  100. selection.addRange(range);
  101. }
  102. };
  103. export const removeSelection = () => {
  104. const selection = window.getSelection();
  105. if (selection) {
  106. selection.removeAllRanges();
  107. }
  108. };
  109. export const distance = (x: number, y: number) => Math.abs(x - y);
  110. export const resetCursor = () => {
  111. document.documentElement.style.cursor = "";
  112. };
  113. export const setCursorForShape = (shape: string) => {
  114. if (shape === "selection") {
  115. resetCursor();
  116. } else {
  117. document.documentElement.style.cursor = CURSOR_TYPE.CROSSHAIR;
  118. }
  119. };
  120. export const isFullScreen = () =>
  121. document.fullscreenElement?.nodeName === "HTML";
  122. export const allowFullScreen = () =>
  123. document.documentElement.requestFullscreen();
  124. export const exitFullScreen = () => document.exitFullscreen();
  125. export const getShortcutKey = (shortcut: string): string => {
  126. const isMac = /Mac|iPod|iPhone|iPad/.test(window.navigator.platform);
  127. if (isMac) {
  128. return `${shortcut
  129. .replace(/CtrlOrCmd/i, "Cmd")
  130. .replace(/Alt/i, "Option")
  131. .replace(/Del/i, "Delete")
  132. .replace(/Enter|Return/i, "Enter")}`;
  133. }
  134. return `${shortcut.replace(/CtrlOrCmd/i, "Ctrl")}`;
  135. };
  136. export const viewportCoordsToSceneCoords = (
  137. { clientX, clientY }: { clientX: number; clientY: number },
  138. {
  139. scrollX,
  140. scrollY,
  141. zoom,
  142. }: {
  143. scrollX: FlooredNumber;
  144. scrollY: FlooredNumber;
  145. zoom: number;
  146. },
  147. canvas: HTMLCanvasElement | null,
  148. scale: number,
  149. ) => {
  150. const zoomOrigin = getZoomOrigin(canvas, scale);
  151. const clientXWithZoom = zoomOrigin.x + (clientX - zoomOrigin.x) / zoom;
  152. const clientYWithZoom = zoomOrigin.y + (clientY - zoomOrigin.y) / zoom;
  153. const x = clientXWithZoom - scrollX;
  154. const y = clientYWithZoom - scrollY;
  155. return { x, y };
  156. };
  157. export const sceneCoordsToViewportCoords = (
  158. { sceneX, sceneY }: { sceneX: number; sceneY: number },
  159. {
  160. scrollX,
  161. scrollY,
  162. zoom,
  163. }: {
  164. scrollX: FlooredNumber;
  165. scrollY: FlooredNumber;
  166. zoom: number;
  167. },
  168. canvas: HTMLCanvasElement | null,
  169. scale: number,
  170. ) => {
  171. const zoomOrigin = getZoomOrigin(canvas, scale);
  172. const sceneXWithZoomAndScroll =
  173. zoomOrigin.x - (zoomOrigin.x - sceneX - scrollX) * zoom;
  174. const sceneYWithZoomAndScroll =
  175. zoomOrigin.y - (zoomOrigin.y - sceneY - scrollY) * zoom;
  176. const x = sceneXWithZoomAndScroll;
  177. const y = sceneYWithZoomAndScroll;
  178. return { x, y };
  179. };
  180. export const getGlobalCSSVariable = (name: string) =>
  181. getComputedStyle(document.documentElement).getPropertyValue(`--${name}`);