123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274 |
- import { ExcalidrawElement } from "../element/types";
- import { isTextElement } from "../element/typeChecks";
- import {
- getDiamondPoints,
- getArrowPoints,
- getLinePoints,
- } from "../element/bounds";
- import { RoughCanvas } from "roughjs/bin/canvas";
- import { Drawable } from "roughjs/bin/core";
- import { Point } from "roughjs/bin/geometry";
- import { RoughSVG } from "roughjs/bin/svg";
- import { RoughGenerator } from "roughjs/bin/generator";
- import { SVG_NS } from "../utils";
- function generateElement(
- element: ExcalidrawElement,
- generator: RoughGenerator,
- ) {
- if (!element.shape) {
- switch (element.type) {
- case "rectangle":
- element.shape = generator.rectangle(
- 0,
- 0,
- element.width,
- element.height,
- {
- stroke: element.strokeColor,
- fill:
- element.backgroundColor === "transparent"
- ? undefined
- : element.backgroundColor,
- fillStyle: element.fillStyle,
- strokeWidth: element.strokeWidth,
- roughness: element.roughness,
- seed: element.seed,
- },
- );
- break;
- case "diamond": {
- const [
- topX,
- topY,
- rightX,
- rightY,
- bottomX,
- bottomY,
- leftX,
- leftY,
- ] = getDiamondPoints(element);
- element.shape = generator.polygon(
- [
- [topX, topY],
- [rightX, rightY],
- [bottomX, bottomY],
- [leftX, leftY],
- ],
- {
- stroke: element.strokeColor,
- fill:
- element.backgroundColor === "transparent"
- ? undefined
- : element.backgroundColor,
- fillStyle: element.fillStyle,
- strokeWidth: element.strokeWidth,
- roughness: element.roughness,
- seed: element.seed,
- },
- );
- break;
- }
- case "ellipse":
- element.shape = generator.ellipse(
- element.width / 2,
- element.height / 2,
- element.width,
- element.height,
- {
- stroke: element.strokeColor,
- fill:
- element.backgroundColor === "transparent"
- ? undefined
- : element.backgroundColor,
- fillStyle: element.fillStyle,
- strokeWidth: element.strokeWidth,
- roughness: element.roughness,
- seed: element.seed,
- curveFitting: 1,
- },
- );
- break;
- case "arrow": {
- const [x2, y2, x3, y3, x4, y4] = getArrowPoints(element);
- const options = {
- stroke: element.strokeColor,
- strokeWidth: element.strokeWidth,
- roughness: element.roughness,
- seed: element.seed,
- };
- // points array can be empty in the beginning, so it is important to add
- // initial position to it
- const points: Point[] = element.points.length
- ? element.points
- : [[0, 0]];
- element.shape = [
- // \
- generator.line(x3, y3, x2, y2, options),
- // -----
- generator.curve(points, options),
- // /
- generator.line(x4, y4, x2, y2, options),
- ];
- break;
- }
- case "line": {
- const [x1, y1, x2, y2] = getLinePoints(element);
- const options = {
- stroke: element.strokeColor,
- strokeWidth: element.strokeWidth,
- roughness: element.roughness,
- seed: element.seed,
- };
- element.shape = generator.line(x1, y1, x2, y2, options);
- break;
- }
- }
- }
- }
- export function renderElement(
- element: ExcalidrawElement,
- rc: RoughCanvas,
- context: CanvasRenderingContext2D,
- ) {
- const generator = rc.generator;
- switch (element.type) {
- case "selection": {
- const fillStyle = context.fillStyle;
- context.fillStyle = "rgba(0, 0, 255, 0.10)";
- context.fillRect(0, 0, element.width, element.height);
- context.fillStyle = fillStyle;
- break;
- }
- case "rectangle":
- case "diamond":
- case "ellipse":
- case "line": {
- generateElement(element, generator);
- context.globalAlpha = element.opacity / 100;
- rc.draw(element.shape as Drawable);
- context.globalAlpha = 1;
- break;
- }
- case "arrow": {
- generateElement(element, generator);
- context.globalAlpha = element.opacity / 100;
- (element.shape as Drawable[]).forEach(shape => rc.draw(shape));
- context.globalAlpha = 1;
- break;
- }
- default: {
- if (isTextElement(element)) {
- context.globalAlpha = element.opacity / 100;
- const font = context.font;
- context.font = element.font;
- const fillStyle = context.fillStyle;
- context.fillStyle = element.strokeColor;
- // Canvas does not support multiline text by default
- const lines = element.text.replace(/\r\n?/g, "\n").split("\n");
- const lineHeight = element.height / lines.length;
- const offset = element.height - element.baseline;
- for (let i = 0; i < lines.length; i++) {
- context.fillText(lines[i], 0, (i + 1) * lineHeight - offset);
- }
- context.fillStyle = fillStyle;
- context.font = font;
- context.globalAlpha = 1;
- } else {
- throw new Error("Unimplemented type " + element.type);
- }
- }
- }
- }
- export function renderElementToSvg(
- element: ExcalidrawElement,
- rsvg: RoughSVG,
- svgRoot: SVGElement,
- offsetX?: number,
- offsetY?: number,
- ) {
- const generator = rsvg.generator;
- switch (element.type) {
- case "selection": {
- // Since this is used only during editing experience, which is canvas based,
- // this should not happen
- throw new Error("Selection rendering is not supported for SVG");
- }
- case "rectangle":
- case "diamond":
- case "ellipse":
- case "line": {
- generateElement(element, generator);
- const node = rsvg.draw(element.shape as Drawable);
- const opacity = element.opacity / 100;
- if (opacity !== 1) {
- node.setAttribute("stroke-opacity", `${opacity}`);
- node.setAttribute("fill-opacity", `${opacity}`);
- }
- node.setAttribute(
- "transform",
- `translate(${offsetX || 0} ${offsetY || 0})`,
- );
- svgRoot.appendChild(node);
- break;
- }
- case "arrow": {
- generateElement(element, generator);
- const group = svgRoot.ownerDocument!.createElementNS(SVG_NS, "g");
- const opacity = element.opacity / 100;
- (element.shape as Drawable[]).forEach(shape => {
- const node = rsvg.draw(shape);
- if (opacity !== 1) {
- node.setAttribute("stroke-opacity", `${opacity}`);
- node.setAttribute("fill-opacity", `${opacity}`);
- }
- node.setAttribute(
- "transform",
- `translate(${offsetX || 0} ${offsetY || 0})`,
- );
- group.appendChild(node);
- });
- svgRoot.appendChild(group);
- break;
- }
- default: {
- if (isTextElement(element)) {
- const opacity = element.opacity / 100;
- const node = svgRoot.ownerDocument!.createElementNS(SVG_NS, "g");
- if (opacity !== 1) {
- node.setAttribute("stroke-opacity", `${opacity}`);
- node.setAttribute("fill-opacity", `${opacity}`);
- }
- node.setAttribute(
- "transform",
- `translate(${offsetX || 0} ${offsetY || 0})`,
- );
- const lines = element.text.replace(/\r\n?/g, "\n").split("\n");
- const lineHeight = element.height / lines.length;
- const offset = element.height - element.baseline;
- const fontSplit = element.font.split(" ").filter(d => !!d.trim());
- let fontFamily = fontSplit[0];
- let fontSize = "20px";
- if (fontSplit.length > 1) {
- fontFamily = fontSplit[1];
- fontSize = fontSplit[0];
- }
- for (let i = 0; i < lines.length; i++) {
- const text = svgRoot.ownerDocument!.createElementNS(SVG_NS, "text");
- text.textContent = lines[i];
- text.setAttribute("x", "0");
- text.setAttribute("y", `${(i + 1) * lineHeight - offset}`);
- text.setAttribute("font-family", fontFamily);
- text.setAttribute("font-size", fontSize);
- text.setAttribute("fill", element.strokeColor);
- node.appendChild(text);
- }
- svgRoot.appendChild(node);
- } else {
- throw new Error("Unimplemented type " + element.type);
- }
- }
- }
- }
|