renderScene.ts 7.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276
  1. import { RoughCanvas } from "roughjs/bin/canvas";
  2. import { RoughSVG } from "roughjs/bin/svg";
  3. import { FlooredNumber, AppState } from "../types";
  4. import { ExcalidrawElement } from "../element/types";
  5. import { getElementAbsoluteCoords, handlerRectangles } from "../element";
  6. import { roundRect } from "./roundRect";
  7. import { SceneState } from "../scene/types";
  8. import {
  9. getScrollBars,
  10. SCROLLBAR_COLOR,
  11. SCROLLBAR_WIDTH,
  12. } from "../scene/scrollbars";
  13. import { getZoomTranslation } from "../scene/zoom";
  14. import { getSelectedElements } from "../scene/selection";
  15. import { renderElement, renderElementToSvg } from "./renderElement";
  16. export function renderScene(
  17. elements: readonly ExcalidrawElement[],
  18. appState: AppState,
  19. selectionElement: ExcalidrawElement | null,
  20. rc: RoughCanvas,
  21. canvas: HTMLCanvasElement,
  22. sceneState: SceneState,
  23. // extra options, currently passed by export helper
  24. {
  25. renderScrollbars = true,
  26. renderSelection = true,
  27. // Whether to employ render optimizations to improve performance.
  28. // Should not be turned on for export operations and similar, because it
  29. // doesn't guarantee pixel-perfect output.
  30. renderOptimizations = false,
  31. }: {
  32. renderScrollbars?: boolean;
  33. renderSelection?: boolean;
  34. renderOptimizations?: boolean;
  35. } = {},
  36. ) {
  37. if (!canvas) {
  38. return { atLeastOneVisibleElement: false };
  39. }
  40. const context = canvas.getContext("2d")!;
  41. // Get initial scale transform as reference for later usage
  42. const initialContextTransform = context.getTransform();
  43. // When doing calculations based on canvas width we should used normalized one
  44. const normalizedCanvasWidth =
  45. canvas.width / getContextTransformScaleX(initialContextTransform);
  46. const normalizedCanvasHeight =
  47. canvas.height / getContextTransformScaleY(initialContextTransform);
  48. const zoomTranslation = getZoomTranslation(canvas, sceneState.zoom);
  49. function applyZoom(context: CanvasRenderingContext2D): void {
  50. context.save();
  51. // Handle zoom scaling
  52. context.setTransform(
  53. getContextTransformScaleX(initialContextTransform) * sceneState.zoom,
  54. 0,
  55. 0,
  56. getContextTransformScaleY(initialContextTransform) * sceneState.zoom,
  57. getContextTransformTranslateX(context.getTransform()),
  58. getContextTransformTranslateY(context.getTransform()),
  59. );
  60. // Handle zoom translation
  61. context.setTransform(
  62. getContextTransformScaleX(context.getTransform()),
  63. 0,
  64. 0,
  65. getContextTransformScaleY(context.getTransform()),
  66. getContextTransformTranslateX(initialContextTransform) -
  67. zoomTranslation.x,
  68. getContextTransformTranslateY(initialContextTransform) -
  69. zoomTranslation.y,
  70. );
  71. }
  72. function resetZoom(context: CanvasRenderingContext2D): void {
  73. context.restore();
  74. }
  75. // Paint background
  76. context.save();
  77. if (typeof sceneState.viewBackgroundColor === "string") {
  78. const hasTransparence =
  79. sceneState.viewBackgroundColor === "transparent" ||
  80. sceneState.viewBackgroundColor.length === 5 ||
  81. sceneState.viewBackgroundColor.length === 9;
  82. if (hasTransparence) {
  83. context.clearRect(0, 0, normalizedCanvasWidth, normalizedCanvasHeight);
  84. }
  85. context.fillStyle = sceneState.viewBackgroundColor;
  86. context.fillRect(0, 0, normalizedCanvasWidth, normalizedCanvasHeight);
  87. } else {
  88. context.clearRect(0, 0, normalizedCanvasWidth, normalizedCanvasHeight);
  89. }
  90. context.restore();
  91. // Paint visible elements
  92. const visibleElements = elements.filter(element =>
  93. isVisibleElement(
  94. element,
  95. normalizedCanvasWidth,
  96. normalizedCanvasHeight,
  97. sceneState,
  98. ),
  99. );
  100. applyZoom(context);
  101. visibleElements.forEach(element => {
  102. renderElement(element, rc, context, renderOptimizations, sceneState);
  103. });
  104. resetZoom(context);
  105. // Pain selection element
  106. if (selectionElement) {
  107. applyZoom(context);
  108. renderElement(
  109. selectionElement,
  110. rc,
  111. context,
  112. renderOptimizations,
  113. sceneState,
  114. );
  115. resetZoom(context);
  116. }
  117. // Pain selected elements
  118. if (renderSelection) {
  119. const selectedElements = getSelectedElements(elements, appState);
  120. const dashledLinePadding = 4 / sceneState.zoom;
  121. applyZoom(context);
  122. context.translate(sceneState.scrollX, sceneState.scrollY);
  123. selectedElements.forEach(element => {
  124. const [
  125. elementX1,
  126. elementY1,
  127. elementX2,
  128. elementY2,
  129. ] = getElementAbsoluteCoords(element);
  130. const elementWidth = elementX2 - elementX1;
  131. const elementHeight = elementY2 - elementY1;
  132. const initialLineDash = context.getLineDash();
  133. context.setLineDash([8 / sceneState.zoom, 4 / sceneState.zoom]);
  134. context.strokeRect(
  135. elementX1 - dashledLinePadding,
  136. elementY1 - dashledLinePadding,
  137. elementWidth + dashledLinePadding * 2,
  138. elementHeight + dashledLinePadding * 2,
  139. );
  140. context.setLineDash(initialLineDash);
  141. });
  142. resetZoom(context);
  143. // Paint resize handlers
  144. if (selectedElements.length === 1 && selectedElements[0].type !== "text") {
  145. applyZoom(context);
  146. context.translate(sceneState.scrollX, sceneState.scrollY);
  147. const handlers = handlerRectangles(selectedElements[0], sceneState.zoom);
  148. Object.values(handlers)
  149. .filter(handler => handler !== undefined)
  150. .forEach(handler => {
  151. context.strokeRect(handler[0], handler[1], handler[2], handler[3]);
  152. });
  153. resetZoom(context);
  154. }
  155. }
  156. // Paint scrollbars
  157. if (renderScrollbars) {
  158. const scrollBars = getScrollBars(
  159. elements,
  160. normalizedCanvasWidth,
  161. normalizedCanvasHeight,
  162. sceneState,
  163. );
  164. context.save();
  165. context.fillStyle = SCROLLBAR_COLOR;
  166. context.strokeStyle = "rgba(255,255,255,0.8)";
  167. [scrollBars.horizontal, scrollBars.vertical].forEach(scrollBar => {
  168. if (scrollBar) {
  169. roundRect(
  170. context,
  171. scrollBar.x,
  172. scrollBar.y,
  173. scrollBar.width,
  174. scrollBar.height,
  175. SCROLLBAR_WIDTH / 2,
  176. );
  177. }
  178. });
  179. context.restore();
  180. return { atLeastOneVisibleElement: visibleElements.length > 0, scrollBars };
  181. }
  182. return { atLeastOneVisibleElement: visibleElements.length > 0 };
  183. }
  184. function isVisibleElement(
  185. element: ExcalidrawElement,
  186. viewportWidth: number,
  187. viewportHeight: number,
  188. {
  189. scrollX,
  190. scrollY,
  191. zoom,
  192. }: {
  193. scrollX: FlooredNumber;
  194. scrollY: FlooredNumber;
  195. zoom: number;
  196. },
  197. ) {
  198. const [x1, y1, x2, y2] = getElementAbsoluteCoords(element);
  199. // Apply zoom
  200. const viewportWidthWithZoom = viewportWidth / zoom;
  201. const viewportHeightWithZoom = viewportHeight / zoom;
  202. const viewportWidthDiff = viewportWidth - viewportWidthWithZoom;
  203. const viewportHeightDiff = viewportHeight - viewportHeightWithZoom;
  204. return (
  205. x2 + scrollX - viewportWidthDiff / 2 >= 0 &&
  206. x1 + scrollX - viewportWidthDiff / 2 <= viewportWidthWithZoom &&
  207. y2 + scrollY - viewportHeightDiff / 2 >= 0 &&
  208. y1 + scrollY - viewportHeightDiff / 2 <= viewportHeightWithZoom
  209. );
  210. }
  211. // This should be only called for exporting purposes
  212. export function renderSceneToSvg(
  213. elements: readonly ExcalidrawElement[],
  214. rsvg: RoughSVG,
  215. svgRoot: SVGElement,
  216. {
  217. offsetX = 0,
  218. offsetY = 0,
  219. }: {
  220. offsetX?: number;
  221. offsetY?: number;
  222. } = {},
  223. ) {
  224. if (!svgRoot) {
  225. return;
  226. }
  227. // render elements
  228. elements.forEach(element => {
  229. renderElementToSvg(
  230. element,
  231. rsvg,
  232. svgRoot,
  233. element.x + offsetX,
  234. element.y + offsetY,
  235. );
  236. });
  237. }
  238. function getContextTransformScaleX(transform: DOMMatrix): number {
  239. return transform.a;
  240. }
  241. function getContextTransformScaleY(transform: DOMMatrix): number {
  242. return transform.d;
  243. }
  244. function getContextTransformTranslateX(transform: DOMMatrix): number {
  245. return transform.e;
  246. }
  247. function getContextTransformTranslateY(transform: DOMMatrix): number {
  248. return transform.f;
  249. }