|
@@ -7,154 +7,263 @@ import {
|
|
} from "../element/bounds";
|
|
} from "../element/bounds";
|
|
import { RoughCanvas } from "roughjs/bin/canvas";
|
|
import { RoughCanvas } from "roughjs/bin/canvas";
|
|
import { Drawable } from "roughjs/bin/core";
|
|
import { Drawable } from "roughjs/bin/core";
|
|
|
|
+import { RoughSVG } from "roughjs/bin/svg";
|
|
|
|
+import { RoughGenerator } from "roughjs/bin/generator";
|
|
|
|
+import { SVG_NS } from "../utils";
|
|
|
|
|
|
-export function renderElement(
|
|
|
|
|
|
+function generateElement(
|
|
element: ExcalidrawElement,
|
|
element: ExcalidrawElement,
|
|
- rc: RoughCanvas,
|
|
|
|
- context: CanvasRenderingContext2D,
|
|
|
|
|
|
+ generator: RoughGenerator,
|
|
) {
|
|
) {
|
|
- const generator = rc.generator;
|
|
|
|
- if (element.type === "selection") {
|
|
|
|
- const fillStyle = context.fillStyle;
|
|
|
|
- context.fillStyle = "rgba(0, 0, 255, 0.10)";
|
|
|
|
- context.fillRect(0, 0, element.width, element.height);
|
|
|
|
- context.fillStyle = fillStyle;
|
|
|
|
- } else if (element.type === "rectangle") {
|
|
|
|
- if (!element.shape) {
|
|
|
|
- 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,
|
|
|
|
- });
|
|
|
|
- }
|
|
|
|
-
|
|
|
|
- context.globalAlpha = element.opacity / 100;
|
|
|
|
- rc.draw(element.shape as Drawable);
|
|
|
|
- context.globalAlpha = 1;
|
|
|
|
- } else if (element.type === "diamond") {
|
|
|
|
- if (!element.shape) {
|
|
|
|
- const [
|
|
|
|
- topX,
|
|
|
|
- topY,
|
|
|
|
- rightX,
|
|
|
|
- rightY,
|
|
|
|
- bottomX,
|
|
|
|
- bottomY,
|
|
|
|
- leftX,
|
|
|
|
- leftY,
|
|
|
|
- ] = getDiamondPoints(element);
|
|
|
|
- element.shape = generator.polygon(
|
|
|
|
- [
|
|
|
|
- [topX, topY],
|
|
|
|
- [rightX, rightY],
|
|
|
|
- [bottomX, bottomY],
|
|
|
|
- [leftX, leftY],
|
|
|
|
- ],
|
|
|
|
- {
|
|
|
|
|
|
+ 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 [x1, y1, x2, y2, x3, y3, x4, y4] = getArrowPoints(element);
|
|
|
|
+ const options = {
|
|
stroke: element.strokeColor,
|
|
stroke: element.strokeColor,
|
|
- fill:
|
|
|
|
- element.backgroundColor === "transparent"
|
|
|
|
- ? undefined
|
|
|
|
- : element.backgroundColor,
|
|
|
|
- fillStyle: element.fillStyle,
|
|
|
|
strokeWidth: element.strokeWidth,
|
|
strokeWidth: element.strokeWidth,
|
|
roughness: element.roughness,
|
|
roughness: element.roughness,
|
|
seed: element.seed,
|
|
seed: element.seed,
|
|
- },
|
|
|
|
- );
|
|
|
|
- }
|
|
|
|
-
|
|
|
|
- context.globalAlpha = element.opacity / 100;
|
|
|
|
- rc.draw(element.shape as Drawable);
|
|
|
|
- context.globalAlpha = 1;
|
|
|
|
- } else if (element.type === "ellipse") {
|
|
|
|
- if (!element.shape) {
|
|
|
|
- element.shape = generator.ellipse(
|
|
|
|
- element.width / 2,
|
|
|
|
- element.height / 2,
|
|
|
|
- element.width,
|
|
|
|
- element.height,
|
|
|
|
- {
|
|
|
|
|
|
+ };
|
|
|
|
+ element.shape = [
|
|
|
|
+ // \
|
|
|
|
+ generator.line(x3, y3, x2, y2, options),
|
|
|
|
+ // -----
|
|
|
|
+ generator.line(x1, y1, x2, y2, options),
|
|
|
|
+ // /
|
|
|
|
+ generator.line(x4, y4, x2, y2, options),
|
|
|
|
+ ];
|
|
|
|
+ break;
|
|
|
|
+ }
|
|
|
|
+ case "line": {
|
|
|
|
+ const [x1, y1, x2, y2] = getLinePoints(element);
|
|
|
|
+ const options = {
|
|
stroke: element.strokeColor,
|
|
stroke: element.strokeColor,
|
|
- fill:
|
|
|
|
- element.backgroundColor === "transparent"
|
|
|
|
- ? undefined
|
|
|
|
- : element.backgroundColor,
|
|
|
|
- fillStyle: element.fillStyle,
|
|
|
|
strokeWidth: element.strokeWidth,
|
|
strokeWidth: element.strokeWidth,
|
|
roughness: element.roughness,
|
|
roughness: element.roughness,
|
|
seed: element.seed,
|
|
seed: element.seed,
|
|
- curveFitting: 1,
|
|
|
|
- },
|
|
|
|
- );
|
|
|
|
|
|
+ };
|
|
|
|
+ element.shape = generator.line(x1, y1, x2, y2, options);
|
|
|
|
+ break;
|
|
|
|
+ }
|
|
}
|
|
}
|
|
|
|
+ }
|
|
|
|
+}
|
|
|
|
|
|
- context.globalAlpha = element.opacity / 100;
|
|
|
|
- rc.draw(element.shape as Drawable);
|
|
|
|
- context.globalAlpha = 1;
|
|
|
|
- } else if (element.type === "arrow") {
|
|
|
|
- const [x1, y1, x2, y2, x3, y3, x4, y4] = getArrowPoints(element);
|
|
|
|
- const options = {
|
|
|
|
- stroke: element.strokeColor,
|
|
|
|
- strokeWidth: element.strokeWidth,
|
|
|
|
- roughness: element.roughness,
|
|
|
|
- seed: element.seed,
|
|
|
|
- };
|
|
|
|
-
|
|
|
|
- if (!element.shape) {
|
|
|
|
- element.shape = [
|
|
|
|
- // \
|
|
|
|
- generator.line(x3, y3, x2, y2, options),
|
|
|
|
- // -----
|
|
|
|
- generator.line(x1, y1, x2, y2, options),
|
|
|
|
- // /
|
|
|
|
- generator.line(x4, y4, x2, y2, options),
|
|
|
|
- ];
|
|
|
|
|
|
+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;
|
|
}
|
|
}
|
|
-
|
|
|
|
- context.globalAlpha = element.opacity / 100;
|
|
|
|
- (element.shape as Drawable[]).forEach(shape => rc.draw(shape));
|
|
|
|
- context.globalAlpha = 1;
|
|
|
|
- return;
|
|
|
|
- } else if (element.type === "line") {
|
|
|
|
- const [x1, y1, x2, y2] = getLinePoints(element);
|
|
|
|
- const options = {
|
|
|
|
- stroke: element.strokeColor,
|
|
|
|
- strokeWidth: element.strokeWidth,
|
|
|
|
- roughness: element.roughness,
|
|
|
|
- seed: element.seed,
|
|
|
|
- };
|
|
|
|
-
|
|
|
|
- if (!element.shape) {
|
|
|
|
- element.shape = generator.line(x1, y1, x2, y2, options);
|
|
|
|
|
|
+ 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;
|
|
|
|
+ break;
|
|
|
|
+ } else {
|
|
|
|
+ throw new Error("Unimplemented type " + element.type);
|
|
|
|
+ }
|
|
|
|
+ }
|
|
|
|
+ }
|
|
|
|
+}
|
|
|
|
|
|
- context.globalAlpha = element.opacity / 100;
|
|
|
|
- rc.draw(element.shape as Drawable);
|
|
|
|
- context.globalAlpha = 1;
|
|
|
|
- } else 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);
|
|
|
|
|
|
+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);
|
|
|
|
+ }
|
|
}
|
|
}
|
|
- context.fillStyle = fillStyle;
|
|
|
|
- context.font = font;
|
|
|
|
- context.globalAlpha = 1;
|
|
|
|
- } else {
|
|
|
|
- throw new Error("Unimplemented type " + element.type);
|
|
|
|
}
|
|
}
|
|
}
|
|
}
|