瀏覽代碼

feat: add logo into export

Arnošt Pleskot 2 年之前
父節點
當前提交
780af522a2
共有 6 個文件被更改,包括 170 次插入31 次删除
  1. 8 3
      src/components/ImageExportDialog.tsx
  2. 1 0
      src/constants.ts
  3. 46 23
      src/scene/export.ts
  4. 84 4
      src/scene/fancyBackground.ts
  5. 1 0
      src/types.ts
  6. 30 1
      src/utils.ts

+ 8 - 3
src/components/ImageExportDialog.tsx

@@ -39,7 +39,7 @@ import { useAppProps } from "./App";
 import { FilledButton } from "./FilledButton";
 import Select, { convertToSelectItems } from "./Select";
 import { getCommonBounds } from "../element";
-import { defaultExportScale, distance } from "../utils";
+import { convertToExportPadding, defaultExportScale, distance } from "../utils";
 import { getFancyBackgroundPadding } from "../scene/fancyBackground";
 
 const supportsContextFilters =
@@ -137,12 +137,17 @@ const ImageExportModal = ({
       const maxWidth = previewNode.offsetWidth;
       const maxHeight = previewNode.offsetHeight;
 
+      const padding = getFancyBackgroundPadding(
+        convertToExportPadding(DEFAULT_EXPORT_PADDING),
+        true,
+      );
+
       const scale =
         Math.floor(
           (getScaleToFit(
             {
-              width: distance(minX, maxX) + getFancyBackgroundPadding() * 2,
-              height: distance(minY, maxY) + getFancyBackgroundPadding() * 2,
+              width: distance(minX, maxX) + padding[1] + padding[3],
+              height: distance(minY, maxY) + padding[0] + padding[2],
             },
             { width: maxWidth, height: maxHeight },
           ) +

+ 1 - 0
src/constants.ts

@@ -234,6 +234,7 @@ export const MAX_DECIMALS_FOR_SVG_EXPORT = 2;
 export const EXPORT_SCALES = [1, 2, 3];
 export const DEFAULT_EXPORT_PADDING = 10; // px
 export const FANCY_BG_PADDING = 24; // px
+export const FANCY_BG_LOGO_PADDING = 20; // px
 export const FANCY_BG_BORDER_RADIUS = 12; // px
 
 export const DEFAULT_MAX_IMAGE_WIDTH_OR_HEIGHT = 1440;

+ 46 - 23
src/scene/export.ts

@@ -3,11 +3,12 @@ import { NonDeletedExcalidrawElement } from "../element/types";
 import { getCommonBounds, getElementAbsoluteCoords } from "../element/bounds";
 import { renderSceneToSvg, renderStaticScene } from "../renderer/renderScene";
 import {
+  convertToExportPadding,
   distance,
   expandToAspectRatio,
   isOnlyExportingSingleFrame,
 } from "../utils";
-import { AppState, BinaryFiles, Dimensions } from "../types";
+import { AppState, BinaryFiles, Dimensions, ExportPadding } from "../types";
 import {
   DEFAULT_EXPORT_PADDING,
   FANCY_BACKGROUND_IMAGES,
@@ -37,10 +38,12 @@ export const exportToCanvas = async (
   {
     exportBackground,
     exportPadding = DEFAULT_EXPORT_PADDING,
+    exportLogo = true,
     viewBackgroundColor,
   }: {
     exportBackground: boolean;
-    exportPadding?: number;
+    exportPadding?: number | ExportPadding;
+    exportLogo?: boolean;
     viewBackgroundColor: string;
   },
   createCanvas: (
@@ -58,9 +61,15 @@ export const exportToCanvas = async (
     appState.fancyBackgroundImageKey &&
     appState.fancyBackgroundImageKey !== "solid" &&
     elements.length > 0;
+
   const padding = !exportWithFancyBackground
-    ? exportPadding
-    : getFancyBackgroundPadding(exportPadding);
+    ? convertToExportPadding(exportPadding)
+    : getFancyBackgroundPadding(
+        convertToExportPadding(exportPadding),
+        exportLogo,
+      );
+
+  console.log(padding, exportPadding);
 
   const [minX, minY, width, height] = !exportWithFancyBackground
     ? getCanvasSize(elements, padding)
@@ -83,7 +92,7 @@ export const exportToCanvas = async (
   const onlyExportingSingleFrame = isOnlyExportingSingleFrame(elements);
 
   let scrollXAdjustment = 0;
-  let scrollYAdjustment = 0;
+  const scrollYAdjustment = 0;
 
   if (
     exportWithFancyBackground &&
@@ -102,10 +111,11 @@ export const exportToCanvas = async (
       exportScale: scale,
       theme: appState.exportWithDarkMode ? THEME.DARK : THEME.LIGHT,
       contentSize,
+      includeLogo: exportLogo,
     });
 
-    scrollXAdjustment = (width - contentSize.width - padding * 2) / 2;
-    scrollYAdjustment = (height - contentSize.height - padding * 2) / 2;
+    scrollXAdjustment =
+      (width - contentSize.width - (padding[1] + padding[3])) / 2;
   }
 
   renderStaticScene({
@@ -121,9 +131,9 @@ export const exportToCanvas = async (
           ? viewBackgroundColor
           : null,
       scrollX:
-        -minX + (onlyExportingSingleFrame ? 0 : padding + scrollXAdjustment),
+        -minX + (onlyExportingSingleFrame ? 0 : padding[3] + scrollXAdjustment),
       scrollY:
-        -minY + (onlyExportingSingleFrame ? 0 : padding + scrollYAdjustment),
+        -minY + (onlyExportingSingleFrame ? 0 : padding[0] + scrollYAdjustment),
       zoom: defaultAppState.zoom,
       shouldCacheIgnoreZoom: false,
       theme: appState.exportWithDarkMode ? THEME.DARK : THEME.LIGHT,
@@ -172,8 +182,8 @@ export const exportToSvg = async (
     appState.fancyBackgroundImageKey !== "solid";
 
   const padding = !exportWithFancyBackground
-    ? exportPadding
-    : getFancyBackgroundPadding(exportPadding) * exportScale;
+    ? convertToExportPadding(exportPadding)
+    : getFancyBackgroundPadding(convertToExportPadding(exportPadding), true);
 
   let metadata = "";
   if (exportEmbedScene) {
@@ -228,8 +238,10 @@ export const exportToSvg = async (
 
   const onlyExportingSingleFrame = isOnlyExportingSingleFrame(elements);
 
-  const offsetX = -minX + (onlyExportingSingleFrame ? 0 : padding);
-  const offsetY = -minY + (onlyExportingSingleFrame ? 0 : padding);
+  const offsetX = -minX + (onlyExportingSingleFrame ? 0 : padding[3]);
+  const offsetY = -minY + (onlyExportingSingleFrame ? 0 : padding[0]);
+
+  console.log(offsetX, offsetY);
 
   const exportingFrame =
     isExportingWholeCanvas || !onlyExportingSingleFrame
@@ -293,10 +305,13 @@ export const exportToSvg = async (
         exportScale,
         theme: appState.exportWithDarkMode ? THEME.DARK : THEME.LIGHT,
         contentSize,
+        includeLogo: true,
       });
 
-      offsetXAdjustment = (width - contentSize.width - padding * 2) / 2;
-      offsetYAdjustment = (height - contentSize.height - padding * 2) / 2;
+      offsetXAdjustment =
+        (width - contentSize.width - (padding[1] + padding[3])) / 2;
+      offsetYAdjustment =
+        (height - contentSize.height - (padding[0] + padding[2])) / 2;
     } else {
       const rect = svgRoot.ownerDocument!.createElementNS(SVG_NS, "rect");
       rect.setAttribute("x", "0");
@@ -323,7 +338,7 @@ export const exportToSvg = async (
 // calculate smallest area to fit the contents in
 const getCanvasSize = (
   elements: readonly NonDeletedExcalidrawElement[],
-  exportPadding: number,
+  exportPadding: ExportPadding,
   opts?: { aspectRatio: Dimensions },
 ): [number, number, number, number] => {
   // we should decide if we are exporting the whole canvas
@@ -351,11 +366,18 @@ const getCanvasSize = (
     );
   }
 
-  const padding = onlyExportingSingleFrame ? 0 : exportPadding * 2;
-
   const [minX, minY, maxX, maxY] = getCommonBounds(elements);
-  const width = distance(minX, maxX) + padding;
-  const height = distance(minY, maxY) + padding;
+
+  let width = 0;
+  let height = 0;
+
+  if (onlyExportingSingleFrame) {
+    width = distance(minX, maxX);
+    height = distance(minY, maxY);
+  } else {
+    width = distance(minX, maxX) + exportPadding[1] + exportPadding[3];
+    height = distance(minY, maxY) + exportPadding[0] + exportPadding[2];
+  }
 
   if (opts?.aspectRatio) {
     const expandedDimensions = expandToAspectRatio(
@@ -374,9 +396,10 @@ export const getExportSize = (
   exportPadding: number,
   scale: number,
 ): [number, number] => {
-  const [, , width, height] = getCanvasSize(elements, exportPadding).map(
-    (dimension) => Math.trunc(dimension * scale),
-  );
+  const [, , width, height] = getCanvasSize(
+    elements,
+    convertToExportPadding(exportPadding),
+  ).map((dimension) => Math.trunc(dimension * scale));
 
   return [width, height];
 };

+ 84 - 4
src/scene/fancyBackground.ts

@@ -1,7 +1,10 @@
 import {
   DEFAULT_EXPORT_PADDING,
+  EXPORT_LOGO_URL,
+  EXPORT_LOGO_URL_DARK,
   FANCY_BACKGROUND_IMAGES,
   FANCY_BG_BORDER_RADIUS,
+  FANCY_BG_LOGO_PADDING,
   FANCY_BG_PADDING,
   IMAGE_INVERT_FILTER,
   SVG_NS,
@@ -11,11 +14,24 @@ import {
 import { loadHTMLImageElement, loadSVGElement } from "../element/image";
 import { getScaleToFill } from "../packages/utils";
 import { roundRect } from "../renderer/roundRect";
-import { AppState, DataURL, Dimensions } from "../types";
+import { AppState, DataURL, Dimensions, ExportPadding } from "../types";
 
 export const getFancyBackgroundPadding = (
-  exportPadding = DEFAULT_EXPORT_PADDING,
-) => FANCY_BG_PADDING + FANCY_BG_BORDER_RADIUS + exportPadding;
+  exportPadding: ExportPadding = [
+    DEFAULT_EXPORT_PADDING,
+    DEFAULT_EXPORT_PADDING,
+    DEFAULT_EXPORT_PADDING,
+    DEFAULT_EXPORT_PADDING,
+  ],
+  includeLogo = false,
+): ExportPadding =>
+  exportPadding.map(
+    (padding, index) =>
+      FANCY_BG_PADDING +
+      FANCY_BG_BORDER_RADIUS +
+      padding +
+      (index === 2 && includeLogo ? 20 : 0),
+  ) as [number, number, number, number];
 
 const addImageBackground = (
   context: CanvasRenderingContext2D,
@@ -65,6 +81,7 @@ const getContentBackgound = (
   contentSize: Dimensions,
   normalizedDimensions: Dimensions,
   exportScale: number,
+  includeLogo: boolean,
 ): { x: number; y: number; width: number; height: number } => {
   const x =
     (normalizedDimensions.width - contentSize.width * exportScale) / 2 -
@@ -80,7 +97,8 @@ const getContentBackgound = (
     exportScale;
 
   const height =
-    (contentSize.height +
+    (contentSize.height -
+      (includeLogo ? FANCY_BG_LOGO_PADDING : 0) +
       (DEFAULT_EXPORT_PADDING + FANCY_BG_BORDER_RADIUS) * 2) *
     exportScale;
 
@@ -94,6 +112,7 @@ const addContentBackground = (
   exportScale: AppState["exportScale"],
   theme: AppState["theme"],
   contentSize: Dimensions,
+  includeLogo: boolean,
 ) => {
   const shadows = [
     {
@@ -129,6 +148,7 @@ const addContentBackground = (
       contentSize,
       normalizedDimensions,
       exportScale,
+      includeLogo,
     );
 
     if (context.roundRect) {
@@ -162,6 +182,26 @@ const addContentBackground = (
   });
 };
 
+const addLogo = (
+  context: CanvasRenderingContext2D,
+  canvasDimensions: Dimensions,
+  logoImage: HTMLImageElement,
+  exportScale: number,
+) => {
+  context.save();
+  context.beginPath();
+  context.drawImage(
+    logoImage,
+    ((canvasDimensions.width - logoImage.width) / 2) * exportScale, // center horizontally
+    (canvasDimensions.height - logoImage.height - 12) * exportScale, // 12px from bottom
+    logoImage.width * exportScale,
+    logoImage.height * exportScale,
+  );
+
+  context.closePath();
+  context.restore();
+};
+
 export const applyFancyBackgroundOnCanvas = async ({
   canvas,
   fancyBackgroundImageKey,
@@ -169,6 +209,7 @@ export const applyFancyBackgroundOnCanvas = async ({
   exportScale,
   theme,
   contentSize,
+  includeLogo,
 }: {
   canvas: HTMLCanvasElement;
   fancyBackgroundImageKey: Exclude<
@@ -179,6 +220,7 @@ export const applyFancyBackgroundOnCanvas = async ({
   exportScale: AppState["exportScale"];
   theme: AppState["theme"];
   contentSize: Dimensions;
+  includeLogo: boolean;
 }) => {
   const context = canvas.getContext("2d")!;
 
@@ -208,7 +250,15 @@ export const applyFancyBackgroundOnCanvas = async ({
     exportScale,
     theme,
     contentSize,
+    includeLogo,
   );
+
+  if (includeLogo) {
+    const logoImage = await loadHTMLImageElement(
+      theme === THEME.DARK ? EXPORT_LOGO_URL_DARK : EXPORT_LOGO_URL,
+    );
+    addLogo(context, canvasDimensions, logoImage, exportScale);
+  }
 };
 
 const addImageBackgroundToSvg = async ({
@@ -241,12 +291,14 @@ const addContentBackgroundToSvg = ({
   contentSize,
   backgroundColor,
   dimensions,
+  includeLogo,
 }: {
   svgRoot: SVGSVGElement;
   exportScale: number;
   contentSize: Dimensions;
   backgroundColor: string;
   dimensions: Dimensions;
+  includeLogo: boolean;
 }) => {
   // Create the shadow filter
   const filter = svgRoot.ownerDocument!.createElementNS(SVG_NS, "filter");
@@ -305,6 +357,7 @@ const addContentBackgroundToSvg = ({
     contentSize,
     dimensions,
     exportScale,
+    includeLogo,
   );
   const rect = svgRoot.ownerDocument!.createElementNS(SVG_NS, "rect");
   rect.setAttribute("x", x.toString());
@@ -318,6 +371,23 @@ const addContentBackgroundToSvg = ({
   svgRoot.appendChild(rect);
 };
 
+const addLogoToSvg = (
+  svgRoot: SVGSVGElement,
+  canvasDimensions: Dimensions,
+  logoImage: SVGSVGElement,
+  exportScale: number,
+) => {
+  const logoWidth = parseFloat(logoImage.getAttribute("width") || "0");
+  const logoHeight = parseFloat(logoImage.getAttribute("height") || "0");
+
+  const x = (canvasDimensions.width - logoWidth) / 2; // center horizontally
+  const y = canvasDimensions.height - logoHeight - 12; // 12px from bottom
+
+  logoImage.setAttribute("x", `${x}`);
+  logoImage.setAttribute("y", `${y * exportScale}`);
+  svgRoot.appendChild(logoImage);
+};
+
 export const applyFancyBackgroundOnSvg = async ({
   svgRoot,
   fancyBackgroundImageKey,
@@ -326,6 +396,7 @@ export const applyFancyBackgroundOnSvg = async ({
   exportScale,
   theme,
   contentSize,
+  includeLogo,
 }: {
   svgRoot: SVGSVGElement;
   fancyBackgroundImageKey: Exclude<
@@ -337,6 +408,7 @@ export const applyFancyBackgroundOnSvg = async ({
   exportScale: AppState["exportScale"];
   theme: AppState["theme"];
   contentSize: Dimensions;
+  includeLogo: boolean;
 }) => {
   // Image background
   const fancyBackgroundImageUrl =
@@ -355,5 +427,13 @@ export const applyFancyBackgroundOnSvg = async ({
     contentSize,
     backgroundColor,
     dimensions,
+    includeLogo,
   });
+
+  if (includeLogo) {
+    const logoImage = await loadSVGElement(
+      theme === THEME.DARK ? EXPORT_LOGO_URL_DARK : EXPORT_LOGO_URL,
+    );
+    addLogoToSvg(svgRoot, dimensions, logoImage, exportScale);
+  }
 };

+ 1 - 0
src/types.ts

@@ -659,3 +659,4 @@ export type FrameNameBoundsCache = {
 };
 
 export type Dimensions = { width: number; height: number };
+export type ExportPadding = [number, number, number, number];

+ 30 - 1
src/utils.ts

@@ -16,7 +16,14 @@ import {
   FontString,
   NonDeletedExcalidrawElement,
 } from "./element/types";
-import { AppState, DataURL, Dimensions, LastActiveTool, Zoom } from "./types";
+import {
+  AppState,
+  DataURL,
+  Dimensions,
+  ExportPadding,
+  LastActiveTool,
+  Zoom,
+} from "./types";
 import { unstable_batchedUpdates } from "react-dom";
 import { SHAPES } from "./shapes";
 import { isEraserActive, isHandToolActive } from "./appState";
@@ -1052,3 +1059,25 @@ export const expandToAspectRatio = (
     height: newHeight,
   };
 };
+
+const isExportPadding = (value: any): value is ExportPadding => {
+  return (
+    Array.isArray(value) &&
+    value.length === 4 &&
+    value.every((item) => typeof item === "number")
+  );
+};
+
+export const convertToExportPadding = (
+  padding: number | ExportPadding,
+): ExportPadding => {
+  if (typeof padding === "number") {
+    return [padding, padding, padding, padding];
+  }
+
+  if (isExportPadding(padding)) {
+    return padding;
+  }
+
+  throw new Error("Invalid padding value");
+};