SVG export (#598)

* first draft of export to SVG. WIP

* enabled text rendeing - which is not quite right atm

* placeholder svg icon

* size the canvas based on the bounding box of elements

* Do not add opacity attributes if default

* render background rect

* Ensure arrows are in the same SVG group

* parse font-size from font

* export web fonts

* use fixed locations for fonts

* Rename export functions

* renamed export file

* oops broke the icon.
This commit is contained in:
Preet 2020-01-28 12:25:13 -08:00 committed by GitHub
parent 321e4022b0
commit 97b11b0f53
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
16 changed files with 447 additions and 202 deletions

View File

@ -12,6 +12,7 @@
"copyToClipboard": "In die Zwischenablage kopieren",
"export": "Export",
"exportToPng": "Als PNG exportieren",
"exportToSvg": "Als SVG exportieren",
"getShareableLink": "Teilbaren Link erhalten",
"load": "Laden",
"save": "Speichern"

View File

@ -45,6 +45,7 @@
"clearReset": "Clear the canvas & reset background color",
"export": "Export",
"exportToPng": "Export to PNG",
"exportToSvg": "Export to SVG",
"copyToClipboard": "Copy to clipboard",
"save": "Save",
"load": "Load",

View File

@ -45,6 +45,7 @@
"clearReset": "Limpiar lienzo y reiniciar el color de fondo",
"export": "Exportar",
"exportToPng": "Exportar a PNG",
"exportToSvg": "Exportar a SVG",
"copyToClipboard": "Copiar al portapapeles",
"save": "Guardar",
"load": "Cargar",

View File

@ -41,6 +41,7 @@
"clearReset": "Effacer le canvas & réinitialiser la couleur d'arrière-plan",
"export": "Exporter",
"exportToPng": "Exporter en PNG",
"exportToSvg": "Exporter en SVG",
"copyToClipboard": "Copier dans le presse-papier",
"save": "Sauvegarder",
"load": "Ouvrir",

View File

@ -41,6 +41,7 @@
"clearReset": "Limpar o canvas e redefinir a cor de fundo",
"export": "Exportar",
"exportToPng": "Exportar em PNG",
"exportToSvg": "Exportar em SVG",
"copyToClipboard": "Copiar para o clipboard",
"save": "Guardar",
"load": "Carregar",

View File

@ -4,11 +4,11 @@ import React, { useState, useEffect, useRef } from "react";
import { Modal } from "./Modal";
import { ToolButton } from "./ToolButton";
import { clipboard, exportFile, downloadFile, link } from "./icons";
import { clipboard, exportFile, downloadFile, svgFile, link } from "./icons";
import { Island } from "./Island";
import { ExcalidrawElement } from "../element/types";
import { AppState } from "../types";
import { getExportCanvasPreview } from "../scene/getExportCanvasPreview";
import { exportToCanvas } from "../scene/export";
import { ActionsManagerInterface, UpdaterFn } from "../actions/types";
import Stack from "./Stack";
@ -36,6 +36,7 @@ function ExportModal({
actionManager,
syncActionResult,
onExportToPng,
onExportToSvg,
onExportToClipboard,
onExportToBackend,
onCloseRequest,
@ -46,6 +47,7 @@ function ExportModal({
actionManager: ActionsManagerInterface;
syncActionResult: UpdaterFn;
onExportToPng: ExportCB;
onExportToSvg: ExportCB;
onExportToClipboard: ExportCB;
onExportToBackend: ExportCB;
onCloseRequest: () => void;
@ -70,7 +72,7 @@ function ExportModal({
useEffect(() => {
const previewNode = previewRef.current;
const canvas = getExportCanvasPreview(exportedElements, {
const canvas = exportToCanvas(exportedElements, {
exportBackground,
viewBackgroundColor,
exportPadding,
@ -136,6 +138,13 @@ function ExportModal({
onClick={() => onExportToPng(exportedElements, scale)}
ref={pngButton}
/>
<ToolButton
type="button"
icon={svgFile}
title={t("buttons.exportToSvg")}
aria-label={t("buttons.exportToSvg")}
onClick={() => onExportToSvg(exportedElements, scale)}
/>
{probablySupportsClipboard && (
<ToolButton
type="button"
@ -213,6 +222,7 @@ export function ExportDialog({
actionManager,
syncActionResult,
onExportToPng,
onExportToSvg,
onExportToClipboard,
onExportToBackend,
}: {
@ -222,6 +232,7 @@ export function ExportDialog({
actionManager: ActionsManagerInterface;
syncActionResult: UpdaterFn;
onExportToPng: ExportCB;
onExportToSvg: ExportCB;
onExportToClipboard: ExportCB;
onExportToBackend: ExportCB;
}) {
@ -257,6 +268,7 @@ export function ExportDialog({
actionManager={actionManager}
syncActionResult={syncActionResult}
onExportToPng={onExportToPng}
onExportToSvg={onExportToSvg}
onExportToClipboard={onExportToClipboard}
onExportToBackend={onExportToBackend}
onCloseRequest={handleClose}

View File

@ -85,3 +85,12 @@ export const downloadFile = (
/>
</svg>
);
export const svgFile = (
<svg aria-hidden="true" focusable="false" role="img" viewBox="0 0 50 50">
<path
fill="currentColor"
d="M 19 8 L 19 20 L 31 20 L 31 8 Z M 3 11 C 1.34375 11 0 12.34375 0 14 C 0 15.65625 1.34375 17 3 17 C 4.300781 17 5.398438 16.160156 5.8125 15 L 17 15 C 12.121094 17.609375 8.785156 22.492188 8.125 28 L 10.15625 28 C 10.804688 23.21875 13.734375 18.980469 18 16.71875 L 18 13 L 5.8125 13 C 5.398438 11.839844 4.300781 11 3 11 Z M 47 11 C 45.699219 11 44.601563 11.839844 44.1875 13 L 32 13 L 32 16.71875 C 36.269531 18.976563 39.195313 23.203125 39.84375 28 L 41.875 28 C 41.21875 22.476563 37.882813 17.609375 33 15 L 44.1875 15 C 44.601563 16.160156 45.699219 17 47 17 C 48.65625 17 50 15.65625 50 14 C 50 12.34375 48.65625 11 47 11 Z M 3 29 L 3 41 L 15 41 L 15 29 Z M 35 29 L 35 41 L 47 41 L 47 29 Z"
></path>
</svg>
);

View File

@ -1,4 +1,4 @@
import { getExportCanvasPreview } from "../src/scene/getExportCanvasPreview";
import { exportToCanvas } from "./scene/export";
const { registerFont, createCanvas } = require("canvas");
@ -58,7 +58,7 @@ const elements = [
registerFont("./public/FG_Virgil.ttf", { family: "Virgil" });
registerFont("./public/Cascadia.ttf", { family: "Cascadia" });
const canvas = getExportCanvasPreview(
const canvas = exportToCanvas(
elements as any,
{
exportBackground: true,

View File

@ -564,6 +564,16 @@ export class App extends React.Component<any, AppState> {
scale,
});
}}
onExportToSvg={(exportedElements, scale) => {
if (this.canvas) {
exportCanvas("svg", exportedElements, this.canvas, {
exportBackground: this.state.exportBackground,
name: this.state.name,
viewBackgroundColor: this.state.viewBackgroundColor,
scale,
});
}
}}
onExportToClipboard={(exportedElements, scale) => {
if (this.canvas)
exportCanvas("clipboard", exportedElements, this.canvas, {

View File

@ -7,6 +7,119 @@ import {
} from "../element/bounds";
import { RoughCanvas } from "roughjs/bin/canvas";
import { Drawable } from "roughjs/bin/core";
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 [x1, y1, x2, y2, x3, y3, x4, y4] = getArrowPoints(element);
const options = {
stroke: element.strokeColor,
strokeWidth: element.strokeWidth,
roughness: element.roughness,
seed: element.seed,
};
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,
strokeWidth: element.strokeWidth,
roughness: element.roughness,
seed: element.seed,
};
element.shape = generator.line(x1, y1, x2, y2, options);
break;
}
}
}
}
export function renderElement(
element: ExcalidrawElement,
@ -14,147 +127,143 @@ export function renderElement(
context: CanvasRenderingContext2D,
) {
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,
});
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;
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],
],
{
stroke: element.strokeColor,
fill:
element.backgroundColor === "transparent"
? undefined
: element.backgroundColor,
fillStyle: element.fillStyle,
strokeWidth: element.strokeWidth,
roughness: element.roughness,
seed: element.seed,
},
);
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;
}
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,
{
stroke: element.strokeColor,
fill:
element.backgroundColor === "transparent"
? undefined
: element.backgroundColor,
fillStyle: element.fillStyle,
strokeWidth: element.strokeWidth,
roughness: element.roughness,
seed: element.seed,
curveFitting: 1,
},
);
case "arrow": {
generateElement(element, generator);
context.globalAlpha = element.opacity / 100;
(element.shape as Drawable[]).forEach(shape => rc.draw(shape));
context.globalAlpha = 1;
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),
];
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);
}
}
}
}
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.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);
}
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);
}
context.fillStyle = fillStyle;
context.font = font;
context.globalAlpha = 1;
} else {
throw new Error("Unimplemented type " + element.type);
}
}

View File

@ -1,4 +1,5 @@
import { RoughCanvas } from "roughjs/bin/canvas";
import { RoughSVG } from "roughjs/bin/svg";
import { ExcalidrawElement } from "../element/types";
import { getElementAbsoluteCoords, handlerRectangles } from "../element";
@ -11,7 +12,7 @@ import {
SCROLLBAR_WIDTH,
} from "../scene/scrollbars";
import { renderElement } from "./renderElement";
import { renderElement, renderElementToSvg } from "./renderElement";
export function renderScene(
elements: readonly ExcalidrawElement[],
@ -154,3 +155,31 @@ function isVisibleElement(
y2 += scrollY;
return x2 >= 0 && x1 <= canvasWidth && y2 >= 0 && y1 <= canvasHeight;
}
// This should be only called for exporting purposes
export function renderSceneToSvg(
elements: readonly ExcalidrawElement[],
rsvg: RoughSVG,
svgRoot: SVGElement,
{
offsetX = 0,
offsetY = 0,
}: {
offsetX?: number;
offsetY?: number;
} = {},
) {
if (!svgRoot) {
return;
}
// render elements
elements.forEach(element => {
renderElementToSvg(
element,
rsvg,
svgRoot,
element.x + offsetX,
element.y + offsetY,
);
});
}

View File

@ -4,7 +4,7 @@ import { getDefaultAppState } from "../appState";
import { AppState } from "../types";
import { ExportType } from "./types";
import { getExportCanvasPreview } from "./getExportCanvasPreview";
import { exportToCanvas, exportToSvg } from "./export";
import nanoid from "nanoid";
import { fileOpen, fileSave } from "browser-nativefs";
import { getCommonBounds } from "../element";
@ -194,7 +194,19 @@ export async function exportCanvas(
return window.alert(i18n.t("alerts.cannotExportEmptyCanvas"));
// calculate smallest area to fit the contents in
const tempCanvas = getExportCanvasPreview(elements, {
if (type === "svg") {
const tempSvg = exportToSvg(elements, {
exportBackground,
viewBackgroundColor,
exportPadding,
});
await fileSave(new Blob([tempSvg.outerHTML], { type: "image/svg+xml" }), {
fileName: `${name}.svg`,
});
return;
}
const tempCanvas = exportToCanvas(elements, {
exportBackground,
viewBackgroundColor,
exportPadding,

112
src/scene/export.ts Normal file
View File

@ -0,0 +1,112 @@
import rough from "roughjs/bin/rough";
import { ExcalidrawElement } from "../element/types";
import { getCommonBounds } from "../element/bounds";
import { renderScene, renderSceneToSvg } from "../renderer/renderScene";
import { distance, SVG_NS } from "../utils";
export function exportToCanvas(
elements: readonly ExcalidrawElement[],
{
exportBackground,
exportPadding = 10,
viewBackgroundColor,
scale = 1,
}: {
exportBackground: boolean;
exportPadding?: number;
scale?: number;
viewBackgroundColor: string;
},
createCanvas: (width: number, height: number) => any = function(
width,
height,
) {
const tempCanvas = document.createElement("canvas");
tempCanvas.width = width * scale;
tempCanvas.height = height * scale;
return tempCanvas;
},
) {
// calculate smallest area to fit the contents in
const [minX, minY, maxX, maxY] = getCommonBounds(elements);
const width = distance(minX, maxX) + exportPadding * 2;
const height = distance(minY, maxY) + exportPadding * 2;
const tempCanvas: any = createCanvas(width, height);
tempCanvas.getContext("2d")?.scale(scale, scale);
renderScene(
elements,
rough.canvas(tempCanvas),
tempCanvas,
{
viewBackgroundColor: exportBackground ? viewBackgroundColor : null,
scrollX: 0,
scrollY: 0,
},
{
offsetX: -minX + exportPadding,
offsetY: -minY + exportPadding,
renderScrollbars: false,
renderSelection: false,
},
);
return tempCanvas;
}
export function exportToSvg(
elements: readonly ExcalidrawElement[],
{
exportBackground,
exportPadding = 10,
viewBackgroundColor,
}: {
exportBackground: boolean;
exportPadding?: number;
viewBackgroundColor: string;
},
): SVGSVGElement {
// calculate canvas dimensions
const [minX, minY, maxX, maxY] = getCommonBounds(elements);
const width = distance(minX, maxX) + exportPadding * 2;
const height = distance(minY, maxY) + exportPadding * 2;
// initialze SVG root
const svgRoot = document.createElementNS(SVG_NS, "svg");
svgRoot.setAttribute("version", "1.1");
svgRoot.setAttribute("xmlns", SVG_NS);
svgRoot.setAttribute("viewBox", `0 0 ${width} ${height}`);
svgRoot.innerHTML = `
<defs>
<style>
@font-face {
font-family: "Virgil";
src: url("https://excalidraw.com/FG_Virgil.ttf");
}
@font-face {
font-family: "Cascadia";
src: url("https://excalidraw.com/Cascadia.ttf");
}
</style>
</defs>
`;
// render backgroiund rect
if (exportBackground && viewBackgroundColor) {
const rect = svgRoot.ownerDocument!.createElementNS(SVG_NS, "rect");
rect.setAttribute("x", "0");
rect.setAttribute("y", "0");
rect.setAttribute("width", `${width}`);
rect.setAttribute("height", `${height}`);
rect.setAttribute("fill", viewBackgroundColor);
svgRoot.appendChild(rect);
}
const rsvg = rough.svg(svgRoot);
renderSceneToSvg(elements, rsvg, svgRoot, {
offsetX: -minX + exportPadding,
offsetY: -minY + exportPadding,
});
return svgRoot;
}

View File

@ -1,55 +0,0 @@
import rough from "roughjs/bin/rough";
import { ExcalidrawElement } from "../element/types";
import { getCommonBounds } from "../element/bounds";
import { renderScene } from "../renderer/renderScene";
import { distance } from "../utils";
export function getExportCanvasPreview(
elements: readonly ExcalidrawElement[],
{
exportBackground,
exportPadding = 10,
viewBackgroundColor,
scale = 1,
}: {
exportBackground: boolean;
exportPadding?: number;
scale?: number;
viewBackgroundColor: string;
},
createCanvas: (width: number, height: number) => any = function(
width,
height,
) {
const tempCanvas = document.createElement("canvas");
tempCanvas.width = width * scale;
tempCanvas.height = height * scale;
return tempCanvas;
},
) {
// calculate smallest area to fit the contents in
const [minX, minY, maxX, maxY] = getCommonBounds(elements);
const width = distance(minX, maxX) + exportPadding * 2;
const height = distance(minY, maxY) + exportPadding * 2;
const tempCanvas: any = createCanvas(width, height);
tempCanvas.getContext("2d")?.scale(scale, scale);
renderScene(
elements,
rough.canvas(tempCanvas),
tempCanvas,
{
viewBackgroundColor: exportBackground ? viewBackgroundColor : null,
scrollX: 0,
scrollY: 0,
},
{
offsetX: -minX + exportPadding,
offsetY: -minY + exportPadding,
renderScrollbars: false,
renderSelection: false,
},
);
return tempCanvas;
}

View File

@ -16,4 +16,4 @@ export interface Scene {
elements: ExcalidrawTextElement[];
}
export type ExportType = "png" | "clipboard" | "backend";
export type ExportType = "png" | "clipboard" | "backend" | "svg";

View File

@ -1,3 +1,5 @@
export const SVG_NS = "http://www.w3.org/2000/svg";
export function getDateTime() {
const date = new Date();
const year = date.getFullYear();