Parcourir la source

feat: centered content bcg on svg export

Arnošt Pleskot il y a 2 ans
Parent
commit
9caa05825d
2 fichiers modifiés avec 179 ajouts et 52 suppressions
  1. 19 3
      src/scene/export.ts
  2. 160 49
      src/scene/fancyBackground.ts

+ 19 - 3
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 (
   </defs>
   `;
 
+  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,

+ 160 - 49
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");
 
-  // Solid color background
-  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}`,
+  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",
   );
-  rect.setAttribute(
-    "height",
-    `${dimensions.height - FANCY_BG_PADDING * 2 * exportScale}`,
+  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", 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,
+  });
+};