Bladeren bron

feat: dark theme export background

Arnošt Pleskot 2 jaren geleden
bovenliggende
commit
787f5d68cf

+ 19 - 13
src/actions/actionExport.tsx

@@ -25,7 +25,7 @@ import { nativeFileSystemSupported } from "../data/filesystem";
 import { Theme } from "../element/types";
 
 import "../components/ToolIcon.scss";
-import Select from "../components/Select";
+import Select, { convertToSelectItems } from "../components/Select";
 
 export const actionChangeProjectName = register({
   name: "changeProjectName",
@@ -119,21 +119,27 @@ export const actionChangeFancyBackgroundImageUrl = register({
   trackEvent: { category: "export", action: "toggleBackgroundImage" },
   perform: (_elements, appState, value) => {
     return {
-      appState: { ...appState, fancyBackgroundImageUrl: value },
+      appState: { ...appState, fancyBackgroundImageKey: value },
       commitToHistory: false,
     };
   },
-  PanelComponent: ({ updateData }) => (
-    <Select
-      items={FANCY_BACKGROUND_IMAGES}
-      ariaLabel={t("imageExportDialog.label.backgroundImage")}
-      placeholder={t("imageExportDialog.label.backgroundImage")}
-      value={DEFAULT_FANCY_BACKGROUND_IMAGE}
-      onChange={(value) => {
-        updateData(value);
-      }}
-    />
-  ),
+  PanelComponent: ({ updateData }) => {
+    const items = convertToSelectItems(
+      FANCY_BACKGROUND_IMAGES,
+      (item) => item.label,
+    );
+    return (
+      <Select
+        items={items}
+        ariaLabel={t("imageExportDialog.label.backgroundImage")}
+        placeholder={t("imageExportDialog.label.backgroundImage")}
+        value={DEFAULT_FANCY_BACKGROUND_IMAGE}
+        onChange={(value) => {
+          updateData(value);
+        }}
+      />
+    );
+  },
 });
 
 export const actionChangeExportEmbedScene = register({

+ 2 - 4
src/appState.ts

@@ -5,7 +5,6 @@ import {
   DEFAULT_FONT_FAMILY,
   DEFAULT_FONT_SIZE,
   DEFAULT_TEXT_ALIGN,
-  FANCY_BACKGROUND_IMAGES,
   EXPORT_SCALES,
   THEME,
 } from "./constants";
@@ -101,8 +100,7 @@ export const getDefaultAppState = (): Omit<
     pendingImageElementId: null,
     showHyperlinkPopup: false,
     selectedLinearElement: null,
-    fancyBackgroundImageUrl:
-      FANCY_BACKGROUND_IMAGES[DEFAULT_FANCY_BACKGROUND_IMAGE].path,
+    fancyBackgroundImageKey: DEFAULT_FANCY_BACKGROUND_IMAGE,
   };
 };
 
@@ -210,7 +208,7 @@ const APP_STATE_STORAGE_CONF = (<
   pendingImageElementId: { browser: false, export: false, server: false },
   showHyperlinkPopup: { browser: false, export: false, server: false },
   selectedLinearElement: { browser: true, export: false, server: false },
-  fancyBackgroundImageUrl: { browser: false, export: false, server: false },
+  fancyBackgroundImageKey: { browser: false, export: false, server: false },
 });
 
 const _clearAppStateForStorage = <

+ 12 - 5
src/components/ImageExportDialog.tsx

@@ -38,7 +38,7 @@ import { Tooltip } from "./Tooltip";
 import "./ImageExportDialog.scss";
 import { useAppProps } from "./App";
 import { FilledButton } from "./FilledButton";
-import Select from "./Select";
+import Select, { convertToSelectItems } from "./Select";
 
 const supportsContextFilters =
   "filter" in document.createElement("canvas").getContext("2d")!;
@@ -69,6 +69,11 @@ function isBackgroundImageKey(
   return key in FANCY_BACKGROUND_IMAGES;
 }
 
+const backgroundSelectItems = convertToSelectItems(
+  FANCY_BACKGROUND_IMAGES,
+  (item) => item.label,
+);
+
 const ImageExportModal = ({
   appState,
   elements,
@@ -138,7 +143,7 @@ const ImageExportModal = ({
   }, [
     appState,
     appState.exportBackground,
-    appState.fancyBackgroundImageUrl,
+    appState.fancyBackgroundImageKey,
     files,
     exportedElements,
   ]);
@@ -150,7 +155,9 @@ const ImageExportModal = ({
         <div
           className={clsx("ImageExportModal__preview__canvas", {
             "ImageExportModal__preview__canvas--img-bcg":
-              appState.exportBackground && appState.fancyBackgroundImageUrl,
+              appState.exportBackground &&
+              appState.fancyBackgroundImageKey &&
+              appState.fancyBackgroundImageKey !== "solid",
           })}
           ref={previewRef}
         >
@@ -199,7 +206,7 @@ const ImageExportModal = ({
         >
           {exportWithBackground && (
             <Select
-              items={FANCY_BACKGROUND_IMAGES}
+              items={backgroundSelectItems}
               ariaLabel={t("imageExportDialog.label.backgroundImage")}
               placeholder={t("imageExportDialog.label.backgroundImage")}
               value={exportBackgroundImage}
@@ -209,7 +216,7 @@ const ImageExportModal = ({
                   actionManager.executeAction(
                     actionChangeFancyBackgroundImageUrl,
                     "ui",
-                    FANCY_BACKGROUND_IMAGES[value].path,
+                    value,
                   );
                 }
               }}

+ 30 - 12
src/components/Select.tsx

@@ -4,23 +4,39 @@ import * as RadixSelect from "@radix-ui/react-select";
 import "./Select.scss";
 import { tablerChevronDownIcon, tablerChevronUpIcon } from "./icons";
 
-type SelectItems = Record<string, { path: string | null; label: string }>;
+type SelectItems<T extends string> = Record<T, string>;
 
-export type SelectProps = {
-  items: SelectItems;
-  value: keyof SelectItems;
-  onChange: (value: keyof SelectItems) => void;
+export type SelectProps<T extends string> = {
+  items: SelectItems<T>;
+  value: T;
+  onChange: (value: T) => void;
   placeholder?: string;
   ariaLabel?: string;
 };
 
-const Select = ({
+type ConverterFunction<T> = (
+  items: Record<string, T>,
+  getLabel: (item: T) => string,
+) => SelectItems<string>;
+
+export const convertToSelectItems: ConverterFunction<any> = (
+  items,
+  getLabel,
+) => {
+  const result: SelectItems<string> = {};
+  for (const key in items) {
+    result[key] = getLabel(items[key]);
+  }
+  return result;
+};
+
+const Select = <T extends string>({
   items,
   value,
   onChange,
   placeholder,
   ariaLabel,
-}: SelectProps) => (
+}: SelectProps<T>) => (
   <RadixSelect.Root value={value} onValueChange={onChange}>
     <RadixSelect.Trigger
       className="Select__trigger"
@@ -41,11 +57,13 @@ const Select = ({
       </RadixSelect.ScrollUpButton>
 
       <RadixSelect.Viewport className="Select__viewport">
-        {Object.entries(items).map(([itemValue, itemLabel]) => (
-          <SelectItem value={itemValue} key={itemValue}>
-            {itemLabel.label}
-          </SelectItem>
-        ))}
+        {(Object.entries(items) as [T, string][]).map(
+          ([itemValue, itemLabel]) => (
+            <SelectItem value={itemValue} key={itemValue}>
+              {itemLabel}
+            </SelectItem>
+          ),
+        )}
       </RadixSelect.Viewport>
 
       <RadixSelect.ScrollDownButton className="Select__scroll-button">

+ 31 - 7
src/constants.ts

@@ -188,6 +188,13 @@ export const ACTIVE_THRESHOLD = 3_000;
 
 export const THEME_FILTER = cssVariables.themeFilter;
 
+// using a stronger invert (100% vs our regular 93%) and saturate
+// as a temp hack to make images in dark theme look closer to original
+// color scheme (it's still not quite there and the colors look slightly
+// desatured, alas...)
+export const IMAGE_INVERT_FILTER =
+  "invert(100%) hue-rotate(180deg) saturate(1.25)";
+
 export const URL_QUERY_KEYS = {
   addLibrary: "addLibrary",
 } as const;
@@ -320,16 +327,33 @@ export const DEFAULT_SIDEBAR = {
 export const LIBRARY_DISABLED_TYPES = new Set(["embeddable", "image"] as const);
 
 export const FANCY_BACKGROUND_IMAGES = {
-  solid: { path: null, label: "solid color" },
-  bubbles: { path: "/backgrounds/bubbles.svg" as DataURL, label: "bubbles" },
+  solid: { light: null, dark: null, label: "solid color" },
+  bubbles: {
+    light: "/backgrounds/bubbles.svg" as DataURL,
+    dark: "/backgrounds/bubbles_dark.svg" as DataURL,
+    label: "bubbles",
+  },
   bubbles2: {
-    path: "/backgrounds/bubbles2.svg" as DataURL,
+    light: "/backgrounds/bubbles2.svg" as DataURL,
+    dark: "/backgrounds/bubbles2_dark.svg" as DataURL,
     label: "bubbles 2",
   },
-  bricks: { path: "/backgrounds/bricks.svg" as DataURL, label: "bricks" },
-  lines: { path: "/backgrounds/lines.svg" as DataURL, label: "lines" },
-  lines2: { path: "/backgrounds/lines2.svg" as DataURL, label: "lines 2" },
+  bricks: {
+    light: "/backgrounds/bricks.svg" as DataURL,
+    dark: "/backgrounds/bricks_dark.svg" as DataURL,
+    label: "bricks",
+  },
+  lines: {
+    light: "/backgrounds/lines.svg" as DataURL,
+    dark: "/backgrounds/lines_dark.svg" as DataURL,
+    label: "lines",
+  },
+  lines2: {
+    light: "/backgrounds/lines2.svg" as DataURL,
+    dark: "/backgrounds/lines2_dark.svg" as DataURL,
+    label: "lines 2",
+  },
 } as const;
 
 export const DEFAULT_FANCY_BACKGROUND_IMAGE: keyof typeof FANCY_BACKGROUND_IMAGES =
-  "solid" as const;
+  "bubbles" as const;

+ 1 - 1
src/data/index.ts

@@ -47,7 +47,7 @@ export const exportCanvas = async (
         exportPadding,
         exportScale: appState.exportScale,
         exportEmbedScene: appState.exportEmbedScene && type === "svg",
-        fancyBackgroundImageUrl: appState.fancyBackgroundImageUrl,
+        fancyBackgroundImageKey: appState.fancyBackgroundImageKey,
       },
       files,
     );

+ 2 - 2
src/element/image.ts

@@ -124,9 +124,9 @@ export const normalizeSVG = async (SVGString: string) => {
   }
 };
 
-export const loadSVGElement = (filePath: string) => {
+export const loadSVGElement = (dataURL: DataURL) => {
   return new Promise<SVGSVGElement>((resolve, reject) => {
-    fetch(filePath)
+    fetch(dataURL)
       .then((response) => response.text())
       .then((svgString) => {
         const parser = new DOMParser();

+ 1 - 6
src/renderer/renderElement.ts

@@ -34,6 +34,7 @@ import { getDefaultAppState } from "../appState";
 import {
   BOUND_TEXT_PADDING,
   FRAME_STYLE,
+  IMAGE_INVERT_FILTER,
   MAX_DECIMALS_FOR_SVG_EXPORT,
   MIME_TYPES,
   SVG_NS,
@@ -56,12 +57,6 @@ import { getContainingFrame } from "../frame";
 import { normalizeLink, toValidURL } from "../data/url";
 import { ShapeCache } from "../scene/ShapeCache";
 
-// using a stronger invert (100% vs our regular 93%) and saturate
-// as a temp hack to make images in dark theme look closer to original
-// color scheme (it's still not quite there and the colors look slightly
-// desatured, alas...)
-const IMAGE_INVERT_FILTER = "invert(100%) hue-rotate(180deg) saturate(1.25)";
-
 const defaultAppState = getDefaultAppState();
 
 const isPendingImageElement = (

+ 24 - 13
src/scene/export.ts

@@ -3,9 +3,10 @@ import { NonDeletedExcalidrawElement } from "../element/types";
 import { getCommonBounds, getElementAbsoluteCoords } from "../element/bounds";
 import { renderSceneToSvg, renderStaticScene } from "../renderer/renderScene";
 import { distance, isOnlyExportingSingleFrame } from "../utils";
-import { AppState, BinaryFiles, DataURL } from "../types";
+import { AppState, BinaryFiles } from "../types";
 import {
   DEFAULT_EXPORT_PADDING,
+  FANCY_BACKGROUND_IMAGES,
   FANCY_BG_BORDER_RADIUS,
   FANCY_BG_PADDING,
   SVG_NS,
@@ -51,7 +52,8 @@ export const exportToCanvas = async (
 ) => {
   const exportWithFancyBackground =
     exportBackground &&
-    !!appState.fancyBackgroundImageUrl &&
+    appState.fancyBackgroundImageKey &&
+    appState.fancyBackgroundImageKey !== "solid" &&
     elements.length > 0;
   const padding = !exportWithFancyBackground
     ? exportPadding
@@ -75,7 +77,7 @@ export const exportToCanvas = async (
 
   const renderConfig = {
     viewBackgroundColor:
-      exportBackground && !appState.fancyBackgroundImageUrl
+      exportBackground && !exportWithFancyBackground
         ? viewBackgroundColor
         : null,
     scrollX: -minX + (onlyExportingSingleFrame ? 0 : padding),
@@ -92,15 +94,19 @@ export const exportToCanvas = async (
     renderSelection: false,
     renderGrid: false,
     isExporting: true,
-    exportBackgroundImage: appState.fancyBackgroundImageUrl,
+    exportBackgroundImage: appState.fancyBackgroundImageKey,
   };
 
-  if (exportWithFancyBackground) {
+  if (
+    exportWithFancyBackground &&
+    appState.fancyBackgroundImageKey !== "solid"
+  ) {
     await applyFancyBackgroundOnCanvas({
       canvas,
-      fancyBackgroundImageUrl: appState.fancyBackgroundImageUrl!,
+      fancyBackgroundImageKey: appState.fancyBackgroundImageKey,
       backgroundColor: viewBackgroundColor,
       exportScale: appState.exportScale,
+      theme: renderConfig.theme,
     });
   }
 
@@ -139,7 +145,7 @@ export const exportToSvg = async (
     exportWithDarkMode?: boolean;
     exportEmbedScene?: boolean;
     renderFrame?: boolean;
-    fancyBackgroundImageUrl: DataURL | null;
+    fancyBackgroundImageKey?: keyof typeof FANCY_BACKGROUND_IMAGES;
   },
   files: BinaryFiles | null,
   opts?: {
@@ -157,8 +163,9 @@ export const exportToSvg = async (
 
   const exportWithFancyBackground =
     exportBackground &&
-    !!appState.fancyBackgroundImageUrl &&
-    elements.length > 0;
+    elements.length > 0 &&
+    appState.fancyBackgroundImageKey &&
+    appState.fancyBackgroundImageKey !== "solid";
 
   const padding = !exportWithFancyBackground
     ? exportPadding
@@ -191,7 +198,8 @@ export const exportToSvg = async (
     svgRoot.setAttribute("filter", THEME_FILTER);
   }
 
-  let assetPath = "https://excalidraw.com/";
+  // let assetPath = "https://excalidraw.com/";
+  let assetPath = "http://localhost:3000/";
   // Asset path needs to be determined only when using package
   if (import.meta.env.VITE_IS_EXCALIDRAW_NPM_PACKAGE) {
     assetPath =
@@ -258,14 +266,17 @@ export const exportToSvg = async (
 
   // render background rect
   if (appState.exportBackground && viewBackgroundColor) {
-    if (appState.fancyBackgroundImageUrl) {
+    if (
+      appState.fancyBackgroundImageKey &&
+      appState.fancyBackgroundImageKey !== "solid"
+    ) {
       await applyFancyBackgroundOnSvg({
         svgRoot,
-        fancyBackgroundImageUrl:
-          `${appState.fancyBackgroundImageUrl}` as DataURL,
+        fancyBackgroundImageKey: `${appState.fancyBackgroundImageKey}`,
         backgroundColor: viewBackgroundColor,
         dimensions: { w: width, h: height },
         exportScale,
+        theme: appState.exportWithDarkMode ? THEME.DARK : THEME.LIGHT,
       });
     } else {
       const rect = svgRoot.ownerDocument!.createElementNS(SVG_NS, "rect");

+ 44 - 10
src/scene/fancyBackground.ts

@@ -1,7 +1,15 @@
-import { FANCY_BG_BORDER_RADIUS, FANCY_BG_PADDING, SVG_NS } from "../constants";
+import {
+  FANCY_BACKGROUND_IMAGES,
+  FANCY_BG_BORDER_RADIUS,
+  FANCY_BG_PADDING,
+  IMAGE_INVERT_FILTER,
+  SVG_NS,
+  THEME,
+  THEME_FILTER,
+} from "../constants";
 import { loadHTMLImageElement, loadSVGElement } from "../element/image";
 import { roundRect } from "../renderer/roundRect";
-import { AppState, DataURL } from "../types";
+import { AppState } from "../types";
 
 type Dimensions = { w: number; h: number };
 
@@ -62,6 +70,7 @@ const addContentBackground = (
   normalizedDimensions: Dimensions,
   contentBackgroundColor: string,
   exportScale: AppState["exportScale"],
+  theme: AppState["theme"],
 ) => {
   const shadows = [
     {
@@ -113,6 +122,9 @@ const addContentBackground = (
     }
 
     if (index === shadows.length - 1) {
+      if (theme === THEME.DARK) {
+        context.filter = THEME_FILTER;
+      }
       context.fillStyle = contentBackgroundColor;
       context.fill();
     }
@@ -123,17 +135,25 @@ const addContentBackground = (
 
 export const applyFancyBackgroundOnCanvas = async ({
   canvas,
-  fancyBackgroundImageUrl,
+  fancyBackgroundImageKey,
   backgroundColor,
   exportScale,
+  theme,
 }: {
   canvas: HTMLCanvasElement;
-  fancyBackgroundImageUrl: DataURL;
+  fancyBackgroundImageKey: Exclude<
+    keyof typeof FANCY_BACKGROUND_IMAGES,
+    "solid"
+  >;
   backgroundColor: string;
   exportScale: AppState["exportScale"];
+  theme: AppState["theme"];
 }) => {
   const context = canvas.getContext("2d")!;
 
+  const fancyBackgroundImageUrl =
+    FANCY_BACKGROUND_IMAGES[fancyBackgroundImageKey][theme];
+
   const fancyBackgroundImage = await loadHTMLImageElement(
     fancyBackgroundImageUrl,
   );
@@ -142,31 +162,45 @@ export const applyFancyBackgroundOnCanvas = async ({
 
   addImageBackground(context, canvasDimensions, fancyBackgroundImage);
 
-  addContentBackground(context, canvasDimensions, backgroundColor, exportScale);
+  addContentBackground(
+    context,
+    canvasDimensions,
+    backgroundColor,
+    exportScale,
+    theme,
+  );
 };
 
 export const applyFancyBackgroundOnSvg = async ({
   svgRoot,
-  fancyBackgroundImageUrl,
+  fancyBackgroundImageKey,
   backgroundColor,
   dimensions,
   exportScale,
+  theme,
 }: {
   svgRoot: SVGSVGElement;
-  fancyBackgroundImageUrl: DataURL;
+  fancyBackgroundImageKey: Exclude<
+    keyof typeof FANCY_BACKGROUND_IMAGES,
+    "solid"
+  >;
   backgroundColor: string;
   dimensions: Dimensions;
   exportScale: AppState["exportScale"];
+  theme: AppState["theme"];
 }) => {
-  const fancyBackgroundImage = await loadSVGElement(
-    `${fancyBackgroundImageUrl}`,
-  );
+  const fancyBackgroundImageUrl =
+    FANCY_BACKGROUND_IMAGES[fancyBackgroundImageKey][theme];
+  const fancyBackgroundImage = await loadSVGElement(fancyBackgroundImageUrl);
 
   fancyBackgroundImage.setAttribute("x", "0");
   fancyBackgroundImage.setAttribute("y", "0");
   fancyBackgroundImage.setAttribute("width", `${dimensions.w}`);
   fancyBackgroundImage.setAttribute("height", `${dimensions.h}`);
   fancyBackgroundImage.setAttribute("preserveAspectRatio", "none");
+  if (theme === THEME.DARK) {
+    fancyBackgroundImage.setAttribute("filter", IMAGE_INVERT_FILTER);
+  }
 
   svgRoot.appendChild(fancyBackgroundImage);
 

+ 6 - 2
src/types.ts

@@ -32,7 +32,11 @@ import { isOverScrollBars } from "./scene";
 import { MaybeTransformHandleType } from "./element/transformHandles";
 import Library from "./data/library";
 import type { FileSystemHandle } from "./data/filesystem";
-import type { IMAGE_MIME_TYPES, MIME_TYPES } from "./constants";
+import type {
+  FANCY_BACKGROUND_IMAGES,
+  IMAGE_MIME_TYPES,
+  MIME_TYPES,
+} from "./constants";
 import { ContextMenuItems } from "./components/ContextMenu";
 import { Merge, ForwardRef, ValueOf } from "./utility-types";
 
@@ -287,7 +291,7 @@ export type AppState = {
   pendingImageElementId: ExcalidrawImageElement["id"] | null;
   showHyperlinkPopup: false | "info" | "editor";
   selectedLinearElement: LinearElementEditor | null;
-  fancyBackgroundImageUrl: DataURL | null;
+  fancyBackgroundImageKey: keyof typeof FANCY_BACKGROUND_IMAGES;
 };
 
 export type UIAppState = Omit<