diff --git a/src/scene/export.ts b/src/scene/export.ts index d824babce..04591b48c 100644 --- a/src/scene/export.ts +++ b/src/scene/export.ts @@ -189,7 +189,11 @@ export const exportToSvg = async ( console.error(error); } } - const [minX, minY, width, height] = getCanvasSize(elements, padding); + const [minX, minY, width, height] = !exportWithFancyBackground + ? getCanvasSize(elements, padding) + : getCanvasSize(elements, padding, { + aspectRatio: { width: 16, height: 9 }, + }); // initialize SVG root const svgRoot = document.createElementNS(SVG_NS, "svg"); @@ -267,12 +271,20 @@ export const exportToSvg = async ( `; + let offsetXAdjustment = 0; + let offsetYAdjustment = 0; + // render background rect if (appState.exportBackground && viewBackgroundColor) { if ( appState.fancyBackgroundImageKey && appState.fancyBackgroundImageKey !== "solid" ) { + const commonBounds = getCommonBounds(elements); + const contentSize: Dimensions = { + width: distance(commonBounds[0], commonBounds[2]), + height: distance(commonBounds[1], commonBounds[3]), + }; await applyFancyBackgroundOnSvg({ svgRoot, fancyBackgroundImageKey: `${appState.fancyBackgroundImageKey}`, @@ -280,7 +292,11 @@ export const exportToSvg = async ( dimensions: { width, height }, exportScale, theme: appState.exportWithDarkMode ? THEME.DARK : THEME.LIGHT, + contentSize, }); + + offsetXAdjustment = (width - contentSize.width - padding * 2) / 2; + offsetYAdjustment = (height - contentSize.height - padding * 2) / 2; } else { const rect = svgRoot.ownerDocument!.createElementNS(SVG_NS, "rect"); rect.setAttribute("x", "0"); @@ -294,8 +310,8 @@ export const exportToSvg = async ( const rsvg = rough.svg(svgRoot); renderSceneToSvg(elements, rsvg, svgRoot, files || {}, { - offsetX, - offsetY, + offsetX: offsetX + offsetXAdjustment, + offsetY: offsetY + offsetYAdjustment, exportWithDarkMode: appState.exportWithDarkMode, exportingFrameId: exportingFrame?.id || null, renderEmbeddables: opts?.renderEmbeddables, diff --git a/src/scene/fancyBackground.ts b/src/scene/fancyBackground.ts index 9722ad291..24de96916 100644 --- a/src/scene/fancyBackground.ts +++ b/src/scene/fancyBackground.ts @@ -11,7 +11,7 @@ import { import { loadHTMLImageElement, loadSVGElement } from "../element/image"; import { getScaleToFill } from "../packages/utils"; import { roundRect } from "../renderer/roundRect"; -import { AppState, Dimensions } from "../types"; +import { AppState, DataURL, Dimensions } from "../types"; export const getFancyBackgroundPadding = ( exportPadding = DEFAULT_EXPORT_PADDING, @@ -61,6 +61,32 @@ const addImageBackground = ( context.restore(); }; +const getContentBackgound = ( + contentSize: Dimensions, + normalizedDimensions: Dimensions, + exportScale: number, +): { x: number; y: number; width: number; height: number } => { + const x = + (normalizedDimensions.width - contentSize.width * exportScale) / 2 - + FANCY_BG_PADDING * exportScale; + + const y = + (normalizedDimensions.height - contentSize.height * exportScale) / 2 - + FANCY_BG_PADDING * exportScale; + + const width = + (contentSize.width + + (DEFAULT_EXPORT_PADDING + FANCY_BG_BORDER_RADIUS) * 2) * + exportScale; + + const height = + (contentSize.height + + (DEFAULT_EXPORT_PADDING + FANCY_BG_BORDER_RADIUS) * 2) * + exportScale; + + return { x, y, width, height }; +}; + const addContentBackground = ( context: CanvasRenderingContext2D, normalizedDimensions: Dimensions, @@ -72,20 +98,20 @@ const addContentBackground = ( const shadows = [ { offsetX: 0, - offsetY: 0.7698959708213806, - blur: 1.4945039749145508, + offsetY: 0, + blur: 2, alpha: 0.02, }, { offsetX: 0, - offsetY: 1.1299999952316284, - blur: 4.1321120262146, + offsetY: 1, + blur: 4, alpha: 0.04, }, { offsetX: 0, - offsetY: 4.130000114440918, - blur: 9.94853401184082, + offsetY: 4, + blur: 10, alpha: 0.05, }, { offsetX: 0, offsetY: 13, blur: 33, alpha: 0.07 }, @@ -99,24 +125,18 @@ const addContentBackground = ( context.shadowOffsetX = shadow.offsetX * exportScale; context.shadowOffsetY = shadow.offsetY * exportScale; - const x = - (normalizedDimensions.width - contentSize.width * exportScale) / 2 - - FANCY_BG_PADDING * exportScale; - const y = - (normalizedDimensions.height - contentSize.height * exportScale) / 2 - - FANCY_BG_PADDING * exportScale; + const { x, y, width, height } = getContentBackgound( + contentSize, + normalizedDimensions, + exportScale, + ); - // fixme: position is no scaled to the center if (context.roundRect) { context.roundRect( x, y, - (contentSize.width + - (DEFAULT_EXPORT_PADDING + FANCY_BG_BORDER_RADIUS) * 2) * - exportScale, - (contentSize.height + - (DEFAULT_EXPORT_PADDING + FANCY_BG_BORDER_RADIUS) * 2) * - exportScale, + width, + height, FANCY_BG_BORDER_RADIUS * exportScale, ); } else { @@ -124,12 +144,8 @@ const addContentBackground = ( context, x, y, - (contentSize.width + - (DEFAULT_EXPORT_PADDING + FANCY_BG_BORDER_RADIUS) * 2) * - exportScale, - (contentSize.height * exportScale + - (DEFAULT_EXPORT_PADDING + FANCY_BG_BORDER_RADIUS) * 2) * - exportScale, + width, + height, FANCY_BG_BORDER_RADIUS * exportScale, ); } @@ -195,27 +211,17 @@ export const applyFancyBackgroundOnCanvas = async ({ ); }; -export const applyFancyBackgroundOnSvg = async ({ +const addImageBackgroundToSvg = async ({ svgRoot, - fancyBackgroundImageKey, - backgroundColor, + fancyBackgroundImageUrl, dimensions, - exportScale, theme, }: { svgRoot: SVGSVGElement; - fancyBackgroundImageKey: Exclude< - keyof typeof FANCY_BACKGROUND_IMAGES, - "solid" - >; - backgroundColor: string; + fancyBackgroundImageUrl: DataURL; dimensions: Dimensions; - exportScale: AppState["exportScale"]; theme: AppState["theme"]; }) => { - // Image background - const fancyBackgroundImageUrl = - FANCY_BACKGROUND_IMAGES[fancyBackgroundImageKey][theme]; const fancyBackgroundImage = await loadSVGElement(fancyBackgroundImageUrl); fancyBackgroundImage.setAttribute("x", "0"); @@ -228,21 +234,126 @@ export const applyFancyBackgroundOnSvg = async ({ } svgRoot.appendChild(fancyBackgroundImage); +}; +const addContentBackgroundToSvg = ({ + svgRoot, + exportScale, + contentSize, + backgroundColor, + dimensions, +}: { + svgRoot: SVGSVGElement; + exportScale: number; + contentSize: Dimensions; + backgroundColor: string; + dimensions: Dimensions; +}) => { + // Create the shadow filter + const filter = svgRoot.ownerDocument!.createElementNS(SVG_NS, "filter"); + filter.setAttribute("id", "shadow"); + + const feGaussianBlur = svgRoot.ownerDocument!.createElementNS( + SVG_NS, + "feGaussianBlur", + ); + feGaussianBlur.setAttribute("in", "SourceAlpha"); + feGaussianBlur.setAttribute("stdDeviation", "3"); + + const feOffset = svgRoot.ownerDocument!.createElementNS(SVG_NS, "feOffset"); + feOffset.setAttribute("dx", "4"); + feOffset.setAttribute("dy", "4"); + feOffset.setAttribute("result", "offsetblur"); + + const feFlood = svgRoot.ownerDocument!.createElementNS(SVG_NS, "feFlood"); + feFlood.setAttribute("flood-color", "black"); + feFlood.setAttribute("flood-opacity", "0.04"); + feFlood.setAttribute("result", "color"); + + const feComposite = svgRoot.ownerDocument!.createElementNS( + SVG_NS, + "feComposite", + ); + feComposite.setAttribute("in", "color"); + feComposite.setAttribute("in2", "offsetblur"); + feComposite.setAttribute("operator", "in"); + feComposite.setAttribute("result", "shadow"); + + const feMerge = svgRoot.ownerDocument!.createElementNS(SVG_NS, "feMerge"); + const feMergeNodeIn = svgRoot.ownerDocument!.createElementNS( + SVG_NS, + "feMergeNode", + ); + feMergeNodeIn.setAttribute("in", "shadow"); + const feMergeNodeGraphic = svgRoot.ownerDocument!.createElementNS( + SVG_NS, + "feMergeNode", + ); + feMergeNodeGraphic.setAttribute("in", "SourceGraphic"); + feMerge.appendChild(feMergeNodeIn); + feMerge.appendChild(feMergeNodeGraphic); + + filter.appendChild(feGaussianBlur); + filter.appendChild(feOffset); + filter.appendChild(feFlood); + filter.appendChild(feComposite); + filter.appendChild(feMerge); + + svgRoot.appendChild(filter); // Solid color background + const { x, y, width, height } = getContentBackgound( + contentSize, + dimensions, + exportScale, + ); const rect = svgRoot.ownerDocument!.createElementNS(SVG_NS, "rect"); - rect.setAttribute("x", (FANCY_BG_PADDING * exportScale).toString()); - rect.setAttribute("y", (FANCY_BG_PADDING * exportScale).toString()); - rect.setAttribute( - "width", - `${dimensions.width - FANCY_BG_PADDING * 2 * exportScale}`, - ); - rect.setAttribute( - "height", - `${dimensions.height - FANCY_BG_PADDING * 2 * exportScale}`, - ); + rect.setAttribute("x", x.toString()); + rect.setAttribute("y", y.toString()); + rect.setAttribute("width", width.toString()); + rect.setAttribute("height", height.toString()); rect.setAttribute("rx", (FANCY_BG_BORDER_RADIUS * exportScale).toString()); rect.setAttribute("ry", (FANCY_BG_BORDER_RADIUS * exportScale).toString()); rect.setAttribute("fill", backgroundColor); + rect.setAttribute("filter", "url(#shadow)"); svgRoot.appendChild(rect); }; + +export const applyFancyBackgroundOnSvg = async ({ + svgRoot, + fancyBackgroundImageKey, + backgroundColor, + dimensions, + exportScale, + theme, + contentSize, +}: { + svgRoot: SVGSVGElement; + fancyBackgroundImageKey: Exclude< + keyof typeof FANCY_BACKGROUND_IMAGES, + "solid" + >; + backgroundColor: string; + dimensions: Dimensions; + exportScale: AppState["exportScale"]; + theme: AppState["theme"]; + contentSize: Dimensions; +}) => { + // Image background + const fancyBackgroundImageUrl = + FANCY_BACKGROUND_IMAGES[fancyBackgroundImageKey][theme]; + + await addImageBackgroundToSvg({ + svgRoot, + fancyBackgroundImageUrl, + dimensions, + theme, + }); + + addContentBackgroundToSvg({ + svgRoot, + exportScale, + contentSize, + backgroundColor, + dimensions, + }); +};