dwelle 10 bulan lalu
induk
melakukan
0458834681

+ 85 - 0
packages/excalidraw/colors.ts

@@ -1,5 +1,90 @@
 import oc from "open-color";
 import oc from "open-color";
 import type { Merge } from "./utility-types";
 import type { Merge } from "./utility-types";
+import { clamp } from "../math/utils";
+import tinycolor from "tinycolor2";
+import { degreesToRadians } from "../math/angle";
+import type { Degrees } from "../math/types";
+
+function cssHueRotate(
+  red: number,
+  green: number,
+  blue: number,
+  degrees: Degrees,
+): { r: number; g: number; b: number } {
+  // normalize
+  const r = red / 255;
+  const g = green / 255;
+  const b = blue / 255;
+
+  // Convert degrees to radians
+  const a = degreesToRadians(degrees);
+
+  const c = Math.cos(a);
+  const s = Math.sin(a);
+
+  // rotation matrix
+  const matrix = [
+    0.213 + c * 0.787 - s * 0.213,
+    0.715 - c * 0.715 - s * 0.715,
+    0.072 - c * 0.072 + s * 0.928,
+    0.213 - c * 0.213 + s * 0.143,
+    0.715 + c * 0.285 + s * 0.14,
+    0.072 - c * 0.072 - s * 0.283,
+    0.213 - c * 0.213 - s * 0.787,
+    0.715 - c * 0.715 + s * 0.715,
+    0.072 + c * 0.928 + s * 0.072,
+  ];
+
+  // transform
+  const newR = r * matrix[0] + g * matrix[1] + b * matrix[2];
+  const newG = r * matrix[3] + g * matrix[4] + b * matrix[5];
+  const newB = r * matrix[6] + g * matrix[7] + b * matrix[8];
+
+  // clamp the values to [0, 1] range and convert back to [0, 255]
+  return {
+    r: Math.round(Math.max(0, Math.min(1, newR)) * 255),
+    g: Math.round(Math.max(0, Math.min(1, newG)) * 255),
+    b: Math.round(Math.max(0, Math.min(1, newB)) * 255),
+  };
+}
+
+const cssInvert = (
+  r: number,
+  g: number,
+  b: number,
+  percent: number,
+): { r: number; g: number; b: number } => {
+  const p = clamp(percent, 0, 100) / 100;
+
+  // Function to invert a single color component
+  const invertComponent = (color: number): number => {
+    // Apply the invert formula
+    const inverted = color * (1 - p) + (255 - color) * p;
+    // Round to the nearest integer and clamp to [0, 255]
+    return Math.round(clamp(inverted, 0, 255));
+  };
+
+  // Calculate the inverted RGB components
+  const invertedR = invertComponent(r);
+  const invertedG = invertComponent(g);
+  const invertedB = invertComponent(b);
+
+  return { r: invertedR, g: invertedG, b: invertedB };
+};
+
+export const applyDarkModeFilter = (color: string) => {
+  let tc = tinycolor(color);
+
+  const _alpha = tc._a;
+
+  // order of operations matters
+  // (corresponds to "filter: invert(invertPercent) hue-rotate(hueDegrees)" in css)
+  tc = tinycolor(cssInvert(tc._r, tc._g, tc._b, 93));
+  tc = tinycolor(cssHueRotate(tc._r, tc._g, tc._b, 180 as Degrees));
+  tc.setAlpha(_alpha);
+
+  return tc.toHex8String();
+};
 
 
 // FIXME can't put to utils.ts rn because of circular dependency
 // FIXME can't put to utils.ts rn because of circular dependency
 const pick = <R extends Record<string, any>, K extends readonly (keyof R)[]>(
 const pick = <R extends Record<string, any>, K extends readonly (keyof R)[]>(

+ 7 - 0
packages/excalidraw/components/App.tsx

@@ -1700,6 +1700,7 @@ class App extends React.Component<AppProps, AppState> {
                             elementsPendingErasure: this.elementsPendingErasure,
                             elementsPendingErasure: this.elementsPendingErasure,
                             pendingFlowchartNodes:
                             pendingFlowchartNodes:
                               this.flowChartCreator.pendingNodes,
                               this.flowChartCreator.pendingNodes,
+                            theme: this.state.theme,
                           }}
                           }}
                         />
                         />
                         {this.state.newElement && (
                         {this.state.newElement && (
@@ -1720,6 +1721,7 @@ class App extends React.Component<AppProps, AppState> {
                               elementsPendingErasure:
                               elementsPendingErasure:
                                 this.elementsPendingErasure,
                                 this.elementsPendingErasure,
                               pendingFlowchartNodes: null,
                               pendingFlowchartNodes: null,
+                              theme: this.state.theme,
                             }}
                             }}
                           />
                           />
                         )}
                         )}
@@ -2695,6 +2697,11 @@ class App extends React.Component<AppProps, AppState> {
         activeTool: updateActiveTool(this.state, { type: "selection" }),
         activeTool: updateActiveTool(this.state, { type: "selection" }),
       });
       });
     }
     }
+    if (prevState.theme !== this.state.theme) {
+      this.scene
+        .getElementsIncludingDeleted()
+        .forEach((element) => ShapeCache.delete(element));
+    }
     if (
     if (
       this.state.activeTool.type === "eraser" &&
       this.state.activeTool.type === "eraser" &&
       prevState.theme !== this.state.theme
       prevState.theme !== this.state.theme

+ 1 - 1
packages/excalidraw/css/styles.scss

@@ -124,7 +124,7 @@ body.excalidraw-cursor-resize * {
     // recommends surface color of #121212, 93% yields #111111 for #FFF
     // recommends surface color of #121212, 93% yields #111111 for #FFF
 
 
     canvas {
     canvas {
-      filter: var(--theme-filter);
+      // filter: var(--theme-filter);
     }
     }
   }
   }
 
 

+ 5 - 1
packages/excalidraw/css/variables.module.scss

@@ -189,7 +189,11 @@
   }
   }
 }
 }
 
 
-$theme-filter: "invert(93%) hue-rotate(180deg)";
+$theme-filter: "invert(93%) hue-rotate(180deg)"; // prod
+// $theme-filter: "invert(93%)"; // prod
+// $theme-filter: "hue-rotate(180deg)"; // prod
+// $theme-filter: "hue-rotate(180deg) invert(93%)";
+
 $right-sidebar-width: "302px";
 $right-sidebar-width: "302px";
 
 
 :export {
 :export {

+ 7 - 1
packages/excalidraw/element/textWysiwyg.tsx

@@ -11,7 +11,7 @@ import {
   isBoundToContainer,
   isBoundToContainer,
   isTextElement,
   isTextElement,
 } from "./typeChecks";
 } from "./typeChecks";
-import { CLASSES, isSafari, POINTER_BUTTON } from "../constants";
+import { CLASSES, isSafari, POINTER_BUTTON, THEME } from "../constants";
 import type {
 import type {
   ExcalidrawElement,
   ExcalidrawElement,
   ExcalidrawLinearElement,
   ExcalidrawLinearElement,
@@ -50,6 +50,7 @@ import {
   originalContainerCache,
   originalContainerCache,
   updateOriginalContainerCache,
   updateOriginalContainerCache,
 } from "./containerCache";
 } from "./containerCache";
+import { applyDarkModeFilter } from "../colors";
 
 
 const getTransform = (
 const getTransform = (
   width: number,
   width: number,
@@ -273,10 +274,15 @@ export const textWysiwyg = ({
         textAlign,
         textAlign,
         verticalAlign,
         verticalAlign,
         color: updatedTextElement.strokeColor,
         color: updatedTextElement.strokeColor,
+        // color:
+        //   appState.theme === THEME.DARK
+        //     ? applyDarkModeFilter(updatedTextElement.strokeColor)
+        //     : updatedTextElement.strokeColor,
         opacity: updatedTextElement.opacity / 100,
         opacity: updatedTextElement.opacity / 100,
         filter: "var(--theme-filter)",
         filter: "var(--theme-filter)",
         maxHeight: `${editorMaxHeight}px`,
         maxHeight: `${editorMaxHeight}px`,
       });
       });
+      // console.log("...", updatedTextElement.strokeColor);
       editable.scrollTop = 0;
       editable.scrollTop = 0;
       // For some reason updating font attribute doesn't set font family
       // For some reason updating font attribute doesn't set font family
       // hence updating font family explicitly for test environment
       // hence updating font family explicitly for test environment

+ 10 - 0
packages/excalidraw/global.d.ts

@@ -104,3 +104,13 @@ declare namespace jest {
     toBeNonNaNNumber(): void;
     toBeNonNaNNumber(): void;
   }
   }
 }
 }
+
+declare namespace tinycolor {
+  interface Instance {
+    _r: number;
+    _g: number;
+    _b: number;
+    _a: number;
+    _ok: boolean;
+  }
+}

+ 2 - 0
packages/excalidraw/package.json

@@ -63,6 +63,7 @@
     "@radix-ui/react-popover": "1.0.3",
     "@radix-ui/react-popover": "1.0.3",
     "@radix-ui/react-tabs": "1.0.2",
     "@radix-ui/react-tabs": "1.0.2",
     "@tldraw/vec": "1.7.1",
     "@tldraw/vec": "1.7.1",
+    "@types/tinycolor2": "1.4.6",
     "browser-fs-access": "0.29.1",
     "browser-fs-access": "0.29.1",
     "canvas-roundrect-polyfill": "0.0.1",
     "canvas-roundrect-polyfill": "0.0.1",
     "clsx": "1.1.1",
     "clsx": "1.1.1",
@@ -84,6 +85,7 @@
     "pwacompat": "2.0.17",
     "pwacompat": "2.0.17",
     "roughjs": "4.6.4",
     "roughjs": "4.6.4",
     "sass": "1.51.0",
     "sass": "1.51.0",
+    "tinycolor2": "1.6.0",
     "tunnel-rat": "0.1.2"
     "tunnel-rat": "0.1.2"
   },
   },
   "devDependencies": {
   "devDependencies": {

+ 6 - 2
packages/excalidraw/renderer/helpers.ts

@@ -3,6 +3,7 @@ import type { StaticCanvasAppState, AppState } from "../types";
 import type { StaticCanvasRenderConfig } from "../scene/types";
 import type { StaticCanvasRenderConfig } from "../scene/types";
 
 
 import { THEME, THEME_FILTER } from "../constants";
 import { THEME, THEME_FILTER } from "../constants";
+import { applyDarkModeFilter } from "../colors";
 
 
 export const fillCircle = (
 export const fillCircle = (
   context: CanvasRenderingContext2D,
   context: CanvasRenderingContext2D,
@@ -50,7 +51,7 @@ export const bootstrapCanvas = ({
   context.scale(scale, scale);
   context.scale(scale, scale);
 
 
   if (isExporting && theme === THEME.DARK) {
   if (isExporting && theme === THEME.DARK) {
-    context.filter = THEME_FILTER;
+    // context.filter = THEME_FILTER;
   }
   }
 
 
   // Paint background
   // Paint background
@@ -64,7 +65,10 @@ export const bootstrapCanvas = ({
       context.clearRect(0, 0, normalizedWidth, normalizedHeight);
       context.clearRect(0, 0, normalizedWidth, normalizedHeight);
     }
     }
     context.save();
     context.save();
-    context.fillStyle = viewBackgroundColor;
+    context.fillStyle =
+      theme === THEME.DARK
+        ? applyDarkModeFilter(viewBackgroundColor)
+        : viewBackgroundColor;
     context.fillRect(0, 0, normalizedWidth, normalizedHeight);
     context.fillRect(0, 0, normalizedWidth, normalizedHeight);
     context.restore();
     context.restore();
   } else {
   } else {

+ 20 - 7
packages/excalidraw/renderer/renderElement.ts

@@ -61,6 +61,7 @@ import { ShapeCache } from "../scene/ShapeCache";
 import { getVerticalOffset } from "../fonts";
 import { getVerticalOffset } from "../fonts";
 import { isRightAngleRads } from "../../math";
 import { isRightAngleRads } from "../../math";
 import { getCornerRadius } from "../shapes";
 import { getCornerRadius } from "../shapes";
+import { applyDarkModeFilter } from "../colors";
 
 
 // using a stronger invert (100% vs our regular 93%) and saturate
 // 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
 // as a temp hack to make images in dark theme look closer to original
@@ -247,9 +248,9 @@ const generateElementCanvas = (
   const rc = rough.canvas(canvas);
   const rc = rough.canvas(canvas);
 
 
   // in dark theme, revert the image color filter
   // in dark theme, revert the image color filter
-  if (shouldResetImageFilter(element, renderConfig, appState)) {
-    context.filter = IMAGE_INVERT_FILTER;
-  }
+  // if (shouldResetImageFilter(element, renderConfig, appState)) {
+  //   context.filter = IMAGE_INVERT_FILTER;
+  // }
 
 
   drawElementOnCanvas(element, rc, context, renderConfig, appState);
   drawElementOnCanvas(element, rc, context, renderConfig, appState);
 
 
@@ -403,7 +404,10 @@ const drawElementOnCanvas = (
     case "freedraw": {
     case "freedraw": {
       // Draw directly to canvas
       // Draw directly to canvas
       context.save();
       context.save();
-      context.fillStyle = element.strokeColor;
+      context.fillStyle =
+        appState.theme === THEME.DARK
+          ? applyDarkModeFilter(element.strokeColor)
+          : element.strokeColor;
 
 
       const path = getFreeDrawPath2D(element) as Path2D;
       const path = getFreeDrawPath2D(element) as Path2D;
       const fillShape = ShapeCache.get(element);
       const fillShape = ShapeCache.get(element);
@@ -412,7 +416,10 @@ const drawElementOnCanvas = (
         rc.draw(fillShape);
         rc.draw(fillShape);
       }
       }
 
 
-      context.fillStyle = element.strokeColor;
+      context.fillStyle =
+        appState.theme === THEME.DARK
+          ? applyDarkModeFilter(element.strokeColor)
+          : element.strokeColor;
       context.fill(path);
       context.fill(path);
 
 
       context.restore();
       context.restore();
@@ -458,7 +465,10 @@ const drawElementOnCanvas = (
         context.canvas.setAttribute("dir", rtl ? "rtl" : "ltr");
         context.canvas.setAttribute("dir", rtl ? "rtl" : "ltr");
         context.save();
         context.save();
         context.font = getFontString(element);
         context.font = getFontString(element);
-        context.fillStyle = element.strokeColor;
+        context.fillStyle =
+          appState.theme === THEME.DARK
+            ? applyDarkModeFilter(element.strokeColor)
+            : element.strokeColor;
         context.textAlign = element.textAlign as CanvasTextAlign;
         context.textAlign = element.textAlign as CanvasTextAlign;
 
 
         // Canvas does not support multiline text by default
         // Canvas does not support multiline text by default
@@ -699,7 +709,10 @@ export const renderElement = (
         context.fillStyle = "rgba(0, 0, 200, 0.04)";
         context.fillStyle = "rgba(0, 0, 200, 0.04)";
 
 
         context.lineWidth = FRAME_STYLE.strokeWidth / appState.zoom.value;
         context.lineWidth = FRAME_STYLE.strokeWidth / appState.zoom.value;
-        context.strokeStyle = FRAME_STYLE.strokeColor;
+        context.strokeStyle =
+          appState.theme === THEME.DARK
+            ? applyDarkModeFilter(element.strokeColor)
+            : FRAME_STYLE.strokeColor;
 
 
         // TODO change later to only affect AI frames
         // TODO change later to only affect AI frames
         if (isMagicFrameElement(element)) {
         if (isMagicFrameElement(element)) {

+ 21 - 4
packages/excalidraw/renderer/staticSvgScene.ts

@@ -5,6 +5,7 @@ import {
   MAX_DECIMALS_FOR_SVG_EXPORT,
   MAX_DECIMALS_FOR_SVG_EXPORT,
   MIME_TYPES,
   MIME_TYPES,
   SVG_NS,
   SVG_NS,
+  THEME,
 } from "../constants";
 } from "../constants";
 import { normalizeLink, toValidURL } from "../data/url";
 import { normalizeLink, toValidURL } from "../data/url";
 import { getElementAbsoluteCoords } from "../element";
 import { getElementAbsoluteCoords } from "../element";
@@ -37,6 +38,7 @@ import { getFontFamilyString, isRTL, isTestEnv } from "../utils";
 import { getFreeDrawSvgPath, IMAGE_INVERT_FILTER } from "./renderElement";
 import { getFreeDrawSvgPath, IMAGE_INVERT_FILTER } from "./renderElement";
 import { getVerticalOffset } from "../fonts";
 import { getVerticalOffset } from "../fonts";
 import { getCornerRadius, isPathALoop } from "../shapes";
 import { getCornerRadius, isPathALoop } from "../shapes";
+import { applyDarkModeFilter } from "../colors";
 
 
 const roughSVGDrawWithPrecision = (
 const roughSVGDrawWithPrecision = (
   rsvg: RoughSVG,
   rsvg: RoughSVG,
@@ -139,7 +141,7 @@ const renderElementToSvg = (
     case "rectangle":
     case "rectangle":
     case "diamond":
     case "diamond":
     case "ellipse": {
     case "ellipse": {
-      const shape = ShapeCache.generateElementShape(element, null);
+      const shape = ShapeCache.generateElementShape(element, renderConfig);
       const node = roughSVGDrawWithPrecision(
       const node = roughSVGDrawWithPrecision(
         rsvg,
         rsvg,
         shape,
         shape,
@@ -389,7 +391,12 @@ const renderElementToSvg = (
       );
       );
       node.setAttribute("stroke", "none");
       node.setAttribute("stroke", "none");
       const path = svgRoot.ownerDocument!.createElementNS(SVG_NS, "path");
       const path = svgRoot.ownerDocument!.createElementNS(SVG_NS, "path");
-      path.setAttribute("fill", element.strokeColor);
+      path.setAttribute(
+        "fill",
+        renderConfig.theme === THEME.DARK
+          ? applyDarkModeFilter(element.strokeColor)
+          : element.strokeColor,
+      );
       path.setAttribute("d", getFreeDrawSvgPath(element));
       path.setAttribute("d", getFreeDrawSvgPath(element));
       node.appendChild(path);
       node.appendChild(path);
 
 
@@ -526,7 +533,12 @@ const renderElementToSvg = (
         rect.setAttribute("ry", FRAME_STYLE.radius.toString());
         rect.setAttribute("ry", FRAME_STYLE.radius.toString());
 
 
         rect.setAttribute("fill", "none");
         rect.setAttribute("fill", "none");
-        rect.setAttribute("stroke", FRAME_STYLE.strokeColor);
+        rect.setAttribute(
+          "stroke",
+          renderConfig.theme === THEME.DARK
+            ? applyDarkModeFilter(FRAME_STYLE.strokeColor)
+            : FRAME_STYLE.strokeColor,
+        );
         rect.setAttribute("stroke-width", FRAME_STYLE.strokeWidth.toString());
         rect.setAttribute("stroke-width", FRAME_STYLE.strokeWidth.toString());
 
 
         addToRoot(rect, element);
         addToRoot(rect, element);
@@ -577,7 +589,12 @@ const renderElementToSvg = (
           text.setAttribute("y", `${i * lineHeightPx + verticalOffset}`);
           text.setAttribute("y", `${i * lineHeightPx + verticalOffset}`);
           text.setAttribute("font-family", getFontFamilyString(element));
           text.setAttribute("font-family", getFontFamilyString(element));
           text.setAttribute("font-size", `${element.fontSize}px`);
           text.setAttribute("font-size", `${element.fontSize}px`);
-          text.setAttribute("fill", element.strokeColor);
+          text.setAttribute(
+            "fill",
+            renderConfig.theme === THEME.DARK
+              ? applyDarkModeFilter(element.strokeColor)
+              : element.strokeColor,
+          );
           text.setAttribute("text-anchor", textAnchor);
           text.setAttribute("text-anchor", textAnchor);
           text.setAttribute("style", "white-space: pre;");
           text.setAttribute("style", "white-space: pre;");
           text.setAttribute("direction", direction);
           text.setAttribute("direction", direction);

+ 34 - 10
packages/excalidraw/scene/Shape.ts

@@ -13,7 +13,7 @@ import type {
 import { generateFreeDrawShape } from "../renderer/renderElement";
 import { generateFreeDrawShape } from "../renderer/renderElement";
 import { isTransparent, assertNever } from "../utils";
 import { isTransparent, assertNever } from "../utils";
 import { simplify } from "points-on-curve";
 import { simplify } from "points-on-curve";
-import { ROUGHNESS } from "../constants";
+import { ROUGHNESS, THEME } from "../constants";
 import {
 import {
   isElbowArrow,
   isElbowArrow,
   isEmbeddableElement,
   isEmbeddableElement,
@@ -22,7 +22,7 @@ import {
   isLinearElement,
   isLinearElement,
 } from "../element/typeChecks";
 } from "../element/typeChecks";
 import { canChangeRoundness } from "./comparisons";
 import { canChangeRoundness } from "./comparisons";
-import type { EmbedsValidationStatus } from "../types";
+import type { AppState, EmbedsValidationStatus } from "../types";
 import {
 import {
   point,
   point,
   pointDistance,
   pointDistance,
@@ -30,6 +30,7 @@ import {
   type LocalPoint,
   type LocalPoint,
 } from "../../math";
 } from "../../math";
 import { getCornerRadius, isPathALoop } from "../shapes";
 import { getCornerRadius, isPathALoop } from "../shapes";
+import { applyDarkModeFilter } from "../colors";
 
 
 const getDashArrayDashed = (strokeWidth: number) => [8, 8 + strokeWidth];
 const getDashArrayDashed = (strokeWidth: number) => [8, 8 + strokeWidth];
 
 
@@ -61,6 +62,7 @@ function adjustRoughness(element: ExcalidrawElement): number {
 export const generateRoughOptions = (
 export const generateRoughOptions = (
   element: ExcalidrawElement,
   element: ExcalidrawElement,
   continuousPath = false,
   continuousPath = false,
+  isDarkMode: boolean = false,
 ): Options => {
 ): Options => {
   const options: Options = {
   const options: Options = {
     seed: element.seed,
     seed: element.seed,
@@ -85,7 +87,9 @@ export const generateRoughOptions = (
     fillWeight: element.strokeWidth / 2,
     fillWeight: element.strokeWidth / 2,
     hachureGap: element.strokeWidth * 4,
     hachureGap: element.strokeWidth * 4,
     roughness: adjustRoughness(element),
     roughness: adjustRoughness(element),
-    stroke: element.strokeColor,
+    stroke: isDarkMode
+      ? applyDarkModeFilter(element.strokeColor)
+      : element.strokeColor,
     preserveVertices:
     preserveVertices:
       continuousPath || element.roughness < ROUGHNESS.cartoonist,
       continuousPath || element.roughness < ROUGHNESS.cartoonist,
   };
   };
@@ -99,6 +103,8 @@ export const generateRoughOptions = (
       options.fillStyle = element.fillStyle;
       options.fillStyle = element.fillStyle;
       options.fill = isTransparent(element.backgroundColor)
       options.fill = isTransparent(element.backgroundColor)
         ? undefined
         ? undefined
+        : isDarkMode
+        ? applyDarkModeFilter(element.backgroundColor)
         : element.backgroundColor;
         : element.backgroundColor;
       if (element.type === "ellipse") {
       if (element.type === "ellipse") {
         options.curveFitting = 1;
         options.curveFitting = 1;
@@ -112,6 +118,8 @@ export const generateRoughOptions = (
         options.fill =
         options.fill =
           element.backgroundColor === "transparent"
           element.backgroundColor === "transparent"
             ? undefined
             ? undefined
+            : isDarkMode
+            ? applyDarkModeFilter(element.backgroundColor)
             : element.backgroundColor;
             : element.backgroundColor;
       }
       }
       return options;
       return options;
@@ -165,6 +173,7 @@ const getArrowheadShapes = (
   generator: RoughGenerator,
   generator: RoughGenerator,
   options: Options,
   options: Options,
   canvasBackgroundColor: string,
   canvasBackgroundColor: string,
+  isDarkMode: boolean,
 ) => {
 ) => {
   const arrowheadPoints = getArrowheadPoints(
   const arrowheadPoints = getArrowheadPoints(
     element,
     element,
@@ -192,10 +201,14 @@ const getArrowheadShapes = (
           fill:
           fill:
             arrowhead === "circle_outline"
             arrowhead === "circle_outline"
               ? canvasBackgroundColor
               ? canvasBackgroundColor
+              : isDarkMode
+              ? applyDarkModeFilter(element.strokeColor)
               : element.strokeColor,
               : element.strokeColor,
 
 
           fillStyle: "solid",
           fillStyle: "solid",
-          stroke: element.strokeColor,
+          stroke: isDarkMode
+            ? applyDarkModeFilter(element.strokeColor)
+            : element.strokeColor,
           roughness: Math.min(0.5, options.roughness || 0),
           roughness: Math.min(0.5, options.roughness || 0),
         }),
         }),
       ];
       ];
@@ -220,6 +233,8 @@ const getArrowheadShapes = (
             fill:
             fill:
               arrowhead === "triangle_outline"
               arrowhead === "triangle_outline"
                 ? canvasBackgroundColor
                 ? canvasBackgroundColor
+                : isDarkMode
+                ? applyDarkModeFilter(element.strokeColor)
                 : element.strokeColor,
                 : element.strokeColor,
             fillStyle: "solid",
             fillStyle: "solid",
             roughness: Math.min(1, options.roughness || 0),
             roughness: Math.min(1, options.roughness || 0),
@@ -248,6 +263,8 @@ const getArrowheadShapes = (
             fill:
             fill:
               arrowhead === "diamond_outline"
               arrowhead === "diamond_outline"
                 ? canvasBackgroundColor
                 ? canvasBackgroundColor
+                : isDarkMode
+                ? applyDarkModeFilter(element.strokeColor)
                 : element.strokeColor,
                 : element.strokeColor,
             fillStyle: "solid",
             fillStyle: "solid",
             roughness: Math.min(1, options.roughness || 0),
             roughness: Math.min(1, options.roughness || 0),
@@ -291,12 +308,15 @@ export const _generateElementShape = (
     isExporting,
     isExporting,
     canvasBackgroundColor,
     canvasBackgroundColor,
     embedsValidationStatus,
     embedsValidationStatus,
+    theme,
   }: {
   }: {
     isExporting: boolean;
     isExporting: boolean;
     canvasBackgroundColor: string;
     canvasBackgroundColor: string;
     embedsValidationStatus: EmbedsValidationStatus | null;
     embedsValidationStatus: EmbedsValidationStatus | null;
+    theme: AppState["theme"];
   },
   },
 ): Drawable | Drawable[] | null => {
 ): Drawable | Drawable[] | null => {
+  const isDarkMode = theme === THEME.DARK;
   switch (element.type) {
   switch (element.type) {
     case "rectangle":
     case "rectangle":
     case "iframe":
     case "iframe":
@@ -322,6 +342,7 @@ export const _generateElementShape = (
               embedsValidationStatus,
               embedsValidationStatus,
             ),
             ),
             true,
             true,
+            isDarkMode,
           ),
           ),
         );
         );
       } else {
       } else {
@@ -337,6 +358,7 @@ export const _generateElementShape = (
               embedsValidationStatus,
               embedsValidationStatus,
             ),
             ),
             false,
             false,
+            isDarkMode,
           ),
           ),
         );
         );
       }
       }
@@ -374,7 +396,7 @@ export const _generateElementShape = (
             C ${topX} ${topY}, ${topX} ${topY}, ${topX + verticalRadius} ${
             C ${topX} ${topY}, ${topX} ${topY}, ${topX + verticalRadius} ${
             topY + horizontalRadius
             topY + horizontalRadius
           }`,
           }`,
-          generateRoughOptions(element, true),
+          generateRoughOptions(element, true, isDarkMode),
         );
         );
       } else {
       } else {
         shape = generator.polygon(
         shape = generator.polygon(
@@ -384,7 +406,7 @@ export const _generateElementShape = (
             [bottomX, bottomY],
             [bottomX, bottomY],
             [leftX, leftY],
             [leftX, leftY],
           ],
           ],
-          generateRoughOptions(element),
+          generateRoughOptions(element, undefined, isDarkMode),
         );
         );
       }
       }
       return shape;
       return shape;
@@ -395,14 +417,14 @@ export const _generateElementShape = (
         element.height / 2,
         element.height / 2,
         element.width,
         element.width,
         element.height,
         element.height,
-        generateRoughOptions(element),
+        generateRoughOptions(element, undefined, isDarkMode),
       );
       );
       return shape;
       return shape;
     }
     }
     case "line":
     case "line":
     case "arrow": {
     case "arrow": {
       let shape: ElementShapes[typeof element.type];
       let shape: ElementShapes[typeof element.type];
-      const options = generateRoughOptions(element);
+      const options = generateRoughOptions(element, undefined, isDarkMode);
 
 
       // points array can be empty in the beginning, so it is important to add
       // points array can be empty in the beginning, so it is important to add
       // initial position to it
       // initial position to it
@@ -414,7 +436,7 @@ export const _generateElementShape = (
         shape = [
         shape = [
           generator.path(
           generator.path(
             generateElbowArrowShape(points, 16),
             generateElbowArrowShape(points, 16),
-            generateRoughOptions(element, true),
+            generateRoughOptions(element, true, isDarkMode),
           ),
           ),
         ];
         ];
       } else if (!element.roundness) {
       } else if (!element.roundness) {
@@ -446,6 +468,7 @@ export const _generateElementShape = (
             generator,
             generator,
             options,
             options,
             canvasBackgroundColor,
             canvasBackgroundColor,
+            isDarkMode,
           );
           );
           shape.push(...shapes);
           shape.push(...shapes);
         }
         }
@@ -463,6 +486,7 @@ export const _generateElementShape = (
             generator,
             generator,
             options,
             options,
             canvasBackgroundColor,
             canvasBackgroundColor,
+            isDarkMode,
           );
           );
           shape.push(...shapes);
           shape.push(...shapes);
         }
         }
@@ -477,7 +501,7 @@ export const _generateElementShape = (
         // generate rough polygon to fill freedraw shape
         // generate rough polygon to fill freedraw shape
         const simplifiedPoints = simplify(element.points, 0.75);
         const simplifiedPoints = simplify(element.points, 0.75);
         shape = generator.curve(simplifiedPoints as [number, number][], {
         shape = generator.curve(simplifiedPoints as [number, number][], {
-          ...generateRoughOptions(element),
+          ...generateRoughOptions(element, undefined, isDarkMode),
           stroke: "none",
           stroke: "none",
         });
         });
       } else {
       } else {

+ 3 - 0
packages/excalidraw/scene/ShapeCache.ts

@@ -9,6 +9,7 @@ import { _generateElementShape } from "./Shape";
 import type { ElementShape, ElementShapes } from "./types";
 import type { ElementShape, ElementShapes } from "./types";
 import { COLOR_PALETTE } from "../colors";
 import { COLOR_PALETTE } from "../colors";
 import type { AppState, EmbedsValidationStatus } from "../types";
 import type { AppState, EmbedsValidationStatus } from "../types";
+import { THEME } from "..";
 
 
 export class ShapeCache {
 export class ShapeCache {
   private static rg = new RoughGenerator();
   private static rg = new RoughGenerator();
@@ -52,6 +53,7 @@ export class ShapeCache {
       isExporting: boolean;
       isExporting: boolean;
       canvasBackgroundColor: AppState["viewBackgroundColor"];
       canvasBackgroundColor: AppState["viewBackgroundColor"];
       embedsValidationStatus: EmbedsValidationStatus;
       embedsValidationStatus: EmbedsValidationStatus;
+      theme: AppState["theme"];
     } | null,
     } | null,
   ) => {
   ) => {
     // when exporting, always regenerated to guarantee the latest shape
     // when exporting, always regenerated to guarantee the latest shape
@@ -74,6 +76,7 @@ export class ShapeCache {
         isExporting: false,
         isExporting: false,
         canvasBackgroundColor: COLOR_PALETTE.white,
         canvasBackgroundColor: COLOR_PALETTE.white,
         embedsValidationStatus: null,
         embedsValidationStatus: null,
+        theme: THEME.LIGHT,
       },
       },
     ) as T["type"] extends keyof ElementShapes
     ) as T["type"] extends keyof ElementShapes
       ? ElementShapes[T["type"]]
       ? ElementShapes[T["type"]]

+ 15 - 3
packages/excalidraw/scene/export.ts

@@ -40,6 +40,7 @@ import { syncInvalidIndices } from "../fractionalIndex";
 import { renderStaticScene } from "../renderer/staticScene";
 import { renderStaticScene } from "../renderer/staticScene";
 import { Fonts } from "../fonts";
 import { Fonts } from "../fonts";
 import type { Font } from "../fonts/ExcalidrawFont";
 import type { Font } from "../fonts/ExcalidrawFont";
+import { applyDarkModeFilter } from "../colors";
 
 
 const SVG_EXPORT_TAG = `<!-- svg-source:excalidraw -->`;
 const SVG_EXPORT_TAG = `<!-- svg-source:excalidraw -->`;
 
 
@@ -214,6 +215,8 @@ export const exportToCanvas = async (
     files,
     files,
   });
   });
 
 
+  const theme = appState.exportWithDarkMode ? THEME.DARK : THEME.LIGHT;
+
   renderStaticScene({
   renderStaticScene({
     canvas,
     canvas,
     rc: rough.canvas(canvas),
     rc: rough.canvas(canvas),
@@ -233,7 +236,7 @@ export const exportToCanvas = async (
       scrollY: -minY + exportPadding,
       scrollY: -minY + exportPadding,
       zoom: defaultAppState.zoom,
       zoom: defaultAppState.zoom,
       shouldCacheIgnoreZoom: false,
       shouldCacheIgnoreZoom: false,
-      theme: appState.exportWithDarkMode ? THEME.DARK : THEME.LIGHT,
+      theme,
     },
     },
     renderConfig: {
     renderConfig: {
       canvasBackgroundColor: viewBackgroundColor,
       canvasBackgroundColor: viewBackgroundColor,
@@ -244,6 +247,7 @@ export const exportToCanvas = async (
       embedsValidationStatus: new Map(),
       embedsValidationStatus: new Map(),
       elementsPendingErasure: new Set(),
       elementsPendingErasure: new Set(),
       pendingFlowchartNodes: null,
       pendingFlowchartNodes: null,
+      theme,
     },
     },
   });
   });
 
 
@@ -330,7 +334,7 @@ export const exportToSvg = async (
   svgRoot.setAttribute("width", `${width * exportScale}`);
   svgRoot.setAttribute("width", `${width * exportScale}`);
   svgRoot.setAttribute("height", `${height * exportScale}`);
   svgRoot.setAttribute("height", `${height * exportScale}`);
   if (exportWithDarkMode) {
   if (exportWithDarkMode) {
-    svgRoot.setAttribute("filter", THEME_FILTER);
+    // svgRoot.setAttribute("filter", THEME_FILTER);
   }
   }
 
 
   const offsetX = -minX + exportPadding;
   const offsetX = -minX + exportPadding;
@@ -376,7 +380,12 @@ export const exportToSvg = async (
     rect.setAttribute("y", "0");
     rect.setAttribute("y", "0");
     rect.setAttribute("width", `${width}`);
     rect.setAttribute("width", `${width}`);
     rect.setAttribute("height", `${height}`);
     rect.setAttribute("height", `${height}`);
-    rect.setAttribute("fill", viewBackgroundColor);
+    rect.setAttribute(
+      "fill",
+      appState.exportWithDarkMode
+        ? applyDarkModeFilter(viewBackgroundColor)
+        : viewBackgroundColor,
+    );
     svgRoot.appendChild(rect);
     svgRoot.appendChild(rect);
   }
   }
 
 
@@ -384,6 +393,8 @@ export const exportToSvg = async (
 
 
   const renderEmbeddables = opts?.renderEmbeddables ?? false;
   const renderEmbeddables = opts?.renderEmbeddables ?? false;
 
 
+  const theme = appState.exportWithDarkMode ? THEME.DARK : THEME.LIGHT;
+
   renderSceneToSvg(
   renderSceneToSvg(
     elementsForRender,
     elementsForRender,
     toBrandedType<RenderableElementsMap>(arrayToMap(elementsForRender)),
     toBrandedType<RenderableElementsMap>(arrayToMap(elementsForRender)),
@@ -405,6 +416,7 @@ export const exportToSvg = async (
               .map((element) => [element.id, true]),
               .map((element) => [element.id, true]),
           )
           )
         : new Map(),
         : new Map(),
+      theme,
     },
     },
   );
   );
 
 

+ 2 - 0
packages/excalidraw/scene/types.ts

@@ -35,6 +35,7 @@ export type StaticCanvasRenderConfig = {
   embedsValidationStatus: EmbedsValidationStatus;
   embedsValidationStatus: EmbedsValidationStatus;
   elementsPendingErasure: ElementsPendingErasure;
   elementsPendingErasure: ElementsPendingErasure;
   pendingFlowchartNodes: PendingExcalidrawElements | null;
   pendingFlowchartNodes: PendingExcalidrawElements | null;
+  theme: AppState["theme"];
 };
 };
 
 
 export type SVGRenderConfig = {
 export type SVGRenderConfig = {
@@ -46,6 +47,7 @@ export type SVGRenderConfig = {
   frameRendering: AppState["frameRendering"];
   frameRendering: AppState["frameRendering"];
   canvasBackgroundColor: AppState["viewBackgroundColor"];
   canvasBackgroundColor: AppState["viewBackgroundColor"];
   embedsValidationStatus: EmbedsValidationStatus;
   embedsValidationStatus: EmbedsValidationStatus;
+  theme: AppState["theme"];
 };
 };
 
 
 export type InteractiveCanvasRenderConfig = {
 export type InteractiveCanvasRenderConfig = {

+ 10 - 0
yarn.lock

@@ -3299,6 +3299,11 @@
   dependencies:
   dependencies:
     "@types/jest" "*"
     "@types/jest" "*"
 
 
+"@types/[email protected]":
+  version "1.4.6"
+  resolved "https://registry.yarnpkg.com/@types/tinycolor2/-/tinycolor2-1.4.6.tgz#670cbc0caf4e58dd61d1e3a6f26386e473087f06"
+  integrity sha512-iEN8J0BoMnsWBqjVbWH/c0G0Hh7O21lpR2/+PrvAVgWdzL7eexIFm4JN/Wn10PTcmNdtS6U67r499mlWMXOxNw==
+
 "@types/trusted-types@^2.0.2":
 "@types/trusted-types@^2.0.2":
   version "2.0.7"
   version "2.0.7"
   resolved "https://registry.yarnpkg.com/@types/trusted-types/-/trusted-types-2.0.7.tgz#baccb07a970b91707df3a3e8ba6896c57ead2d11"
   resolved "https://registry.yarnpkg.com/@types/trusted-types/-/trusted-types-2.0.7.tgz#baccb07a970b91707df3a3e8ba6896c57ead2d11"
@@ -9976,6 +9981,11 @@ tinybench@^2.8.0:
   resolved "https://registry.yarnpkg.com/tinybench/-/tinybench-2.9.0.tgz#103c9f8ba6d7237a47ab6dd1dcff77251863426b"
   resolved "https://registry.yarnpkg.com/tinybench/-/tinybench-2.9.0.tgz#103c9f8ba6d7237a47ab6dd1dcff77251863426b"
   integrity sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==
   integrity sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==
 
 
[email protected]:
+  version "1.6.0"
+  resolved "https://registry.yarnpkg.com/tinycolor2/-/tinycolor2-1.6.0.tgz#f98007460169b0263b97072c5ae92484ce02d09e"
+  integrity sha512-XPaBkWQJdsf3pLKJV9p4qN/S+fm2Oj8AIPo1BTUhg5oxkvm9+SVEGFdhyOz7tTdUTfvxMiAs4sp6/eZO2Ew+pw==
+
 tinypool@^1.0.0:
 tinypool@^1.0.0:
   version "1.0.1"
   version "1.0.1"
   resolved "https://registry.yarnpkg.com/tinypool/-/tinypool-1.0.1.tgz#c64233c4fac4304e109a64340178760116dbe1fe"
   resolved "https://registry.yarnpkg.com/tinypool/-/tinypool-1.0.1.tgz#c64233c4fac4304e109a64340178760116dbe1fe"