utils.ts 6.0 KB

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