utils.ts 5.8 KB

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