renderElement.ts 8.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274
  1. import { ExcalidrawElement } from "../element/types";
  2. import { isTextElement } from "../element/typeChecks";
  3. import {
  4. getDiamondPoints,
  5. getArrowPoints,
  6. getLinePoints,
  7. } from "../element/bounds";
  8. import { RoughCanvas } from "roughjs/bin/canvas";
  9. import { Drawable } from "roughjs/bin/core";
  10. import { Point } from "roughjs/bin/geometry";
  11. import { RoughSVG } from "roughjs/bin/svg";
  12. import { RoughGenerator } from "roughjs/bin/generator";
  13. import { SVG_NS } from "../utils";
  14. function generateElement(
  15. element: ExcalidrawElement,
  16. generator: RoughGenerator,
  17. ) {
  18. if (!element.shape) {
  19. switch (element.type) {
  20. case "rectangle":
  21. element.shape = generator.rectangle(
  22. 0,
  23. 0,
  24. element.width,
  25. element.height,
  26. {
  27. stroke: element.strokeColor,
  28. fill:
  29. element.backgroundColor === "transparent"
  30. ? undefined
  31. : element.backgroundColor,
  32. fillStyle: element.fillStyle,
  33. strokeWidth: element.strokeWidth,
  34. roughness: element.roughness,
  35. seed: element.seed,
  36. },
  37. );
  38. break;
  39. case "diamond": {
  40. const [
  41. topX,
  42. topY,
  43. rightX,
  44. rightY,
  45. bottomX,
  46. bottomY,
  47. leftX,
  48. leftY,
  49. ] = getDiamondPoints(element);
  50. element.shape = generator.polygon(
  51. [
  52. [topX, topY],
  53. [rightX, rightY],
  54. [bottomX, bottomY],
  55. [leftX, leftY],
  56. ],
  57. {
  58. stroke: element.strokeColor,
  59. fill:
  60. element.backgroundColor === "transparent"
  61. ? undefined
  62. : element.backgroundColor,
  63. fillStyle: element.fillStyle,
  64. strokeWidth: element.strokeWidth,
  65. roughness: element.roughness,
  66. seed: element.seed,
  67. },
  68. );
  69. break;
  70. }
  71. case "ellipse":
  72. element.shape = generator.ellipse(
  73. element.width / 2,
  74. element.height / 2,
  75. element.width,
  76. element.height,
  77. {
  78. stroke: element.strokeColor,
  79. fill:
  80. element.backgroundColor === "transparent"
  81. ? undefined
  82. : element.backgroundColor,
  83. fillStyle: element.fillStyle,
  84. strokeWidth: element.strokeWidth,
  85. roughness: element.roughness,
  86. seed: element.seed,
  87. curveFitting: 1,
  88. },
  89. );
  90. break;
  91. case "arrow": {
  92. const [x2, y2, x3, y3, x4, y4] = getArrowPoints(element);
  93. const options = {
  94. stroke: element.strokeColor,
  95. strokeWidth: element.strokeWidth,
  96. roughness: element.roughness,
  97. seed: element.seed,
  98. };
  99. // points array can be empty in the beginning, so it is important to add
  100. // initial position to it
  101. const points: Point[] = element.points.length
  102. ? element.points
  103. : [[0, 0]];
  104. element.shape = [
  105. // \
  106. generator.line(x3, y3, x2, y2, options),
  107. // -----
  108. generator.curve(points, options),
  109. // /
  110. generator.line(x4, y4, x2, y2, options),
  111. ];
  112. break;
  113. }
  114. case "line": {
  115. const [x1, y1, x2, y2] = getLinePoints(element);
  116. const options = {
  117. stroke: element.strokeColor,
  118. strokeWidth: element.strokeWidth,
  119. roughness: element.roughness,
  120. seed: element.seed,
  121. };
  122. element.shape = generator.line(x1, y1, x2, y2, options);
  123. break;
  124. }
  125. }
  126. }
  127. }
  128. export function renderElement(
  129. element: ExcalidrawElement,
  130. rc: RoughCanvas,
  131. context: CanvasRenderingContext2D,
  132. ) {
  133. const generator = rc.generator;
  134. switch (element.type) {
  135. case "selection": {
  136. const fillStyle = context.fillStyle;
  137. context.fillStyle = "rgba(0, 0, 255, 0.10)";
  138. context.fillRect(0, 0, element.width, element.height);
  139. context.fillStyle = fillStyle;
  140. break;
  141. }
  142. case "rectangle":
  143. case "diamond":
  144. case "ellipse":
  145. case "line": {
  146. generateElement(element, generator);
  147. context.globalAlpha = element.opacity / 100;
  148. rc.draw(element.shape as Drawable);
  149. context.globalAlpha = 1;
  150. break;
  151. }
  152. case "arrow": {
  153. generateElement(element, generator);
  154. context.globalAlpha = element.opacity / 100;
  155. (element.shape as Drawable[]).forEach(shape => rc.draw(shape));
  156. context.globalAlpha = 1;
  157. break;
  158. }
  159. default: {
  160. if (isTextElement(element)) {
  161. context.globalAlpha = element.opacity / 100;
  162. const font = context.font;
  163. context.font = element.font;
  164. const fillStyle = context.fillStyle;
  165. context.fillStyle = element.strokeColor;
  166. // Canvas does not support multiline text by default
  167. const lines = element.text.replace(/\r\n?/g, "\n").split("\n");
  168. const lineHeight = element.height / lines.length;
  169. const offset = element.height - element.baseline;
  170. for (let i = 0; i < lines.length; i++) {
  171. context.fillText(lines[i], 0, (i + 1) * lineHeight - offset);
  172. }
  173. context.fillStyle = fillStyle;
  174. context.font = font;
  175. context.globalAlpha = 1;
  176. } else {
  177. throw new Error("Unimplemented type " + element.type);
  178. }
  179. }
  180. }
  181. }
  182. export function renderElementToSvg(
  183. element: ExcalidrawElement,
  184. rsvg: RoughSVG,
  185. svgRoot: SVGElement,
  186. offsetX?: number,
  187. offsetY?: number,
  188. ) {
  189. const generator = rsvg.generator;
  190. switch (element.type) {
  191. case "selection": {
  192. // Since this is used only during editing experience, which is canvas based,
  193. // this should not happen
  194. throw new Error("Selection rendering is not supported for SVG");
  195. }
  196. case "rectangle":
  197. case "diamond":
  198. case "ellipse":
  199. case "line": {
  200. generateElement(element, generator);
  201. const node = rsvg.draw(element.shape as Drawable);
  202. const opacity = element.opacity / 100;
  203. if (opacity !== 1) {
  204. node.setAttribute("stroke-opacity", `${opacity}`);
  205. node.setAttribute("fill-opacity", `${opacity}`);
  206. }
  207. node.setAttribute(
  208. "transform",
  209. `translate(${offsetX || 0} ${offsetY || 0})`,
  210. );
  211. svgRoot.appendChild(node);
  212. break;
  213. }
  214. case "arrow": {
  215. generateElement(element, generator);
  216. const group = svgRoot.ownerDocument!.createElementNS(SVG_NS, "g");
  217. const opacity = element.opacity / 100;
  218. (element.shape as Drawable[]).forEach(shape => {
  219. const node = rsvg.draw(shape);
  220. if (opacity !== 1) {
  221. node.setAttribute("stroke-opacity", `${opacity}`);
  222. node.setAttribute("fill-opacity", `${opacity}`);
  223. }
  224. node.setAttribute(
  225. "transform",
  226. `translate(${offsetX || 0} ${offsetY || 0})`,
  227. );
  228. group.appendChild(node);
  229. });
  230. svgRoot.appendChild(group);
  231. break;
  232. }
  233. default: {
  234. if (isTextElement(element)) {
  235. const opacity = element.opacity / 100;
  236. const node = svgRoot.ownerDocument!.createElementNS(SVG_NS, "g");
  237. if (opacity !== 1) {
  238. node.setAttribute("stroke-opacity", `${opacity}`);
  239. node.setAttribute("fill-opacity", `${opacity}`);
  240. }
  241. node.setAttribute(
  242. "transform",
  243. `translate(${offsetX || 0} ${offsetY || 0})`,
  244. );
  245. const lines = element.text.replace(/\r\n?/g, "\n").split("\n");
  246. const lineHeight = element.height / lines.length;
  247. const offset = element.height - element.baseline;
  248. const fontSplit = element.font.split(" ").filter(d => !!d.trim());
  249. let fontFamily = fontSplit[0];
  250. let fontSize = "20px";
  251. if (fontSplit.length > 1) {
  252. fontFamily = fontSplit[1];
  253. fontSize = fontSplit[0];
  254. }
  255. for (let i = 0; i < lines.length; i++) {
  256. const text = svgRoot.ownerDocument!.createElementNS(SVG_NS, "text");
  257. text.textContent = lines[i];
  258. text.setAttribute("x", "0");
  259. text.setAttribute("y", `${(i + 1) * lineHeight - offset}`);
  260. text.setAttribute("font-family", fontFamily);
  261. text.setAttribute("font-size", fontSize);
  262. text.setAttribute("fill", element.strokeColor);
  263. node.appendChild(text);
  264. }
  265. svgRoot.appendChild(node);
  266. } else {
  267. throw new Error("Unimplemented type " + element.type);
  268. }
  269. }
  270. }
  271. }