From 97b11b0f53c97f9e2b6866e0995105088149f4ec Mon Sep 17 00:00:00 2001 From: Preet <833927+pshihn@users.noreply.github.com> Date: Tue, 28 Jan 2020 12:25:13 -0800 Subject: [PATCH] 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. --- public/locales/de/translation.json | 1 + public/locales/en/translation.json | 1 + public/locales/es/translation.json | 1 + public/locales/fr/translation.json | 1 + public/locales/pt/translation.json | 1 + src/components/ExportDialog.tsx | 18 +- src/components/icons.tsx | 9 + src/index-node.ts | 4 +- src/index.tsx | 10 + src/renderer/renderElement.ts | 385 ++++++++++++++++++---------- src/renderer/renderScene.ts | 31 ++- src/scene/data.ts | 16 +- src/scene/export.ts | 112 ++++++++ src/scene/getExportCanvasPreview.ts | 55 ---- src/scene/types.ts | 2 +- src/utils.ts | 2 + 16 files changed, 447 insertions(+), 202 deletions(-) create mode 100644 src/scene/export.ts delete mode 100644 src/scene/getExportCanvasPreview.ts diff --git a/public/locales/de/translation.json b/public/locales/de/translation.json index 363ec8de..f81e53a4 100644 --- a/public/locales/de/translation.json +++ b/public/locales/de/translation.json @@ -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" diff --git a/public/locales/en/translation.json b/public/locales/en/translation.json index 79ccc572..790a9d13 100644 --- a/public/locales/en/translation.json +++ b/public/locales/en/translation.json @@ -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", diff --git a/public/locales/es/translation.json b/public/locales/es/translation.json index 248eb3a8..c0838680 100644 --- a/public/locales/es/translation.json +++ b/public/locales/es/translation.json @@ -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", diff --git a/public/locales/fr/translation.json b/public/locales/fr/translation.json index 602bf4e9..9712d2cf 100644 --- a/public/locales/fr/translation.json +++ b/public/locales/fr/translation.json @@ -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", diff --git a/public/locales/pt/translation.json b/public/locales/pt/translation.json index 34efb86c..c630b504 100644 --- a/public/locales/pt/translation.json +++ b/public/locales/pt/translation.json @@ -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", diff --git a/src/components/ExportDialog.tsx b/src/components/ExportDialog.tsx index 1f11d195..72258d45 100644 --- a/src/components/ExportDialog.tsx +++ b/src/components/ExportDialog.tsx @@ -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} /> + onExportToSvg(exportedElements, scale)} + /> {probablySupportsClipboard && ( ); + +export const svgFile = ( + +); diff --git a/src/index-node.ts b/src/index-node.ts index 071bd07f..48fc47f7 100644 --- a/src/index-node.ts +++ b/src/index-node.ts @@ -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, diff --git a/src/index.tsx b/src/index.tsx index c37ed15b..f022f945 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -564,6 +564,16 @@ export class App extends React.Component { 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, { diff --git a/src/renderer/renderElement.ts b/src/renderer/renderElement.ts index 2a29bed7..cbe33110 100644 --- a/src/renderer/renderElement.ts +++ b/src/renderer/renderElement.ts @@ -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); } } diff --git a/src/renderer/renderScene.ts b/src/renderer/renderScene.ts index d38e64d8..59d0a36d 100644 --- a/src/renderer/renderScene.ts +++ b/src/renderer/renderScene.ts @@ -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, + ); + }); +} diff --git a/src/scene/data.ts b/src/scene/data.ts index 9c2aadfb..fe16c12e 100644 --- a/src/scene/data.ts +++ b/src/scene/data.ts @@ -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, diff --git a/src/scene/export.ts b/src/scene/export.ts new file mode 100644 index 00000000..a5f06d4f --- /dev/null +++ b/src/scene/export.ts @@ -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 = ` + + + + `; + + // 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; +} diff --git a/src/scene/getExportCanvasPreview.ts b/src/scene/getExportCanvasPreview.ts deleted file mode 100644 index b279a1a8..00000000 --- a/src/scene/getExportCanvasPreview.ts +++ /dev/null @@ -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; -} diff --git a/src/scene/types.ts b/src/scene/types.ts index 9f676542..04b28b60 100644 --- a/src/scene/types.ts +++ b/src/scene/types.ts @@ -16,4 +16,4 @@ export interface Scene { elements: ExcalidrawTextElement[]; } -export type ExportType = "png" | "clipboard" | "backend"; +export type ExportType = "png" | "clipboard" | "backend" | "svg"; diff --git a/src/utils.ts b/src/utils.ts index 4c2e2061..ff7ea588 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -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();