From 0458834681ad34b8c77bd7ade520023cda70bf62 Mon Sep 17 00:00:00 2001 From: dwelle <5153846+dwelle@users.noreply.github.com> Date: Mon, 16 Sep 2024 10:46:37 +0200 Subject: [PATCH] wip --- packages/excalidraw/colors.ts | 85 +++++++++++++++++++ packages/excalidraw/components/App.tsx | 7 ++ packages/excalidraw/css/styles.scss | 2 +- packages/excalidraw/css/variables.module.scss | 6 +- packages/excalidraw/element/textWysiwyg.tsx | 8 +- packages/excalidraw/global.d.ts | 10 +++ packages/excalidraw/package.json | 2 + packages/excalidraw/renderer/helpers.ts | 8 +- packages/excalidraw/renderer/renderElement.ts | 27 ++++-- .../excalidraw/renderer/staticSvgScene.ts | 25 +++++- packages/excalidraw/scene/Shape.ts | 44 +++++++--- packages/excalidraw/scene/ShapeCache.ts | 3 + packages/excalidraw/scene/export.ts | 18 +++- packages/excalidraw/scene/types.ts | 2 + yarn.lock | 10 +++ 15 files changed, 228 insertions(+), 29 deletions(-) diff --git a/packages/excalidraw/colors.ts b/packages/excalidraw/colors.ts index e4cd67a94..1eeab39c7 100644 --- a/packages/excalidraw/colors.ts +++ b/packages/excalidraw/colors.ts @@ -1,5 +1,90 @@ import oc from "open-color"; 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 const pick = , K extends readonly (keyof R)[]>( diff --git a/packages/excalidraw/components/App.tsx b/packages/excalidraw/components/App.tsx index fb4ac27b1..cb0b21c43 100644 --- a/packages/excalidraw/components/App.tsx +++ b/packages/excalidraw/components/App.tsx @@ -1700,6 +1700,7 @@ class App extends React.Component { elementsPendingErasure: this.elementsPendingErasure, pendingFlowchartNodes: this.flowChartCreator.pendingNodes, + theme: this.state.theme, }} /> {this.state.newElement && ( @@ -1720,6 +1721,7 @@ class App extends React.Component { elementsPendingErasure: this.elementsPendingErasure, pendingFlowchartNodes: null, + theme: this.state.theme, }} /> )} @@ -2695,6 +2697,11 @@ class App extends React.Component { activeTool: updateActiveTool(this.state, { type: "selection" }), }); } + if (prevState.theme !== this.state.theme) { + this.scene + .getElementsIncludingDeleted() + .forEach((element) => ShapeCache.delete(element)); + } if ( this.state.activeTool.type === "eraser" && prevState.theme !== this.state.theme diff --git a/packages/excalidraw/css/styles.scss b/packages/excalidraw/css/styles.scss index 673106700..73f9562ba 100644 --- a/packages/excalidraw/css/styles.scss +++ b/packages/excalidraw/css/styles.scss @@ -124,7 +124,7 @@ body.excalidraw-cursor-resize * { // recommends surface color of #121212, 93% yields #111111 for #FFF canvas { - filter: var(--theme-filter); + // filter: var(--theme-filter); } } diff --git a/packages/excalidraw/css/variables.module.scss b/packages/excalidraw/css/variables.module.scss index 42e325a4c..1a84beba7 100644 --- a/packages/excalidraw/css/variables.module.scss +++ b/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"; :export { diff --git a/packages/excalidraw/element/textWysiwyg.tsx b/packages/excalidraw/element/textWysiwyg.tsx index 23778cb7b..1d4988b9b 100644 --- a/packages/excalidraw/element/textWysiwyg.tsx +++ b/packages/excalidraw/element/textWysiwyg.tsx @@ -11,7 +11,7 @@ import { isBoundToContainer, isTextElement, } from "./typeChecks"; -import { CLASSES, isSafari, POINTER_BUTTON } from "../constants"; +import { CLASSES, isSafari, POINTER_BUTTON, THEME } from "../constants"; import type { ExcalidrawElement, ExcalidrawLinearElement, @@ -50,6 +50,7 @@ import { originalContainerCache, updateOriginalContainerCache, } from "./containerCache"; +import { applyDarkModeFilter } from "../colors"; const getTransform = ( width: number, @@ -273,10 +274,15 @@ export const textWysiwyg = ({ textAlign, verticalAlign, color: updatedTextElement.strokeColor, + // color: + // appState.theme === THEME.DARK + // ? applyDarkModeFilter(updatedTextElement.strokeColor) + // : updatedTextElement.strokeColor, opacity: updatedTextElement.opacity / 100, filter: "var(--theme-filter)", maxHeight: `${editorMaxHeight}px`, }); + // console.log("...", updatedTextElement.strokeColor); editable.scrollTop = 0; // For some reason updating font attribute doesn't set font family // hence updating font family explicitly for test environment diff --git a/packages/excalidraw/global.d.ts b/packages/excalidraw/global.d.ts index 21a5d2057..e5d5185f7 100644 --- a/packages/excalidraw/global.d.ts +++ b/packages/excalidraw/global.d.ts @@ -104,3 +104,13 @@ declare namespace jest { toBeNonNaNNumber(): void; } } + +declare namespace tinycolor { + interface Instance { + _r: number; + _g: number; + _b: number; + _a: number; + _ok: boolean; + } +} diff --git a/packages/excalidraw/package.json b/packages/excalidraw/package.json index 5d556c04f..c1a692d0b 100644 --- a/packages/excalidraw/package.json +++ b/packages/excalidraw/package.json @@ -63,6 +63,7 @@ "@radix-ui/react-popover": "1.0.3", "@radix-ui/react-tabs": "1.0.2", "@tldraw/vec": "1.7.1", + "@types/tinycolor2": "1.4.6", "browser-fs-access": "0.29.1", "canvas-roundrect-polyfill": "0.0.1", "clsx": "1.1.1", @@ -84,6 +85,7 @@ "pwacompat": "2.0.17", "roughjs": "4.6.4", "sass": "1.51.0", + "tinycolor2": "1.6.0", "tunnel-rat": "0.1.2" }, "devDependencies": { diff --git a/packages/excalidraw/renderer/helpers.ts b/packages/excalidraw/renderer/helpers.ts index 90f40099f..d93f7f022 100644 --- a/packages/excalidraw/renderer/helpers.ts +++ b/packages/excalidraw/renderer/helpers.ts @@ -3,6 +3,7 @@ import type { StaticCanvasAppState, AppState } from "../types"; import type { StaticCanvasRenderConfig } from "../scene/types"; import { THEME, THEME_FILTER } from "../constants"; +import { applyDarkModeFilter } from "../colors"; export const fillCircle = ( context: CanvasRenderingContext2D, @@ -50,7 +51,7 @@ export const bootstrapCanvas = ({ context.scale(scale, scale); if (isExporting && theme === THEME.DARK) { - context.filter = THEME_FILTER; + // context.filter = THEME_FILTER; } // Paint background @@ -64,7 +65,10 @@ export const bootstrapCanvas = ({ context.clearRect(0, 0, normalizedWidth, normalizedHeight); } context.save(); - context.fillStyle = viewBackgroundColor; + context.fillStyle = + theme === THEME.DARK + ? applyDarkModeFilter(viewBackgroundColor) + : viewBackgroundColor; context.fillRect(0, 0, normalizedWidth, normalizedHeight); context.restore(); } else { diff --git a/packages/excalidraw/renderer/renderElement.ts b/packages/excalidraw/renderer/renderElement.ts index 9995c748a..8e53f3c5f 100644 --- a/packages/excalidraw/renderer/renderElement.ts +++ b/packages/excalidraw/renderer/renderElement.ts @@ -61,6 +61,7 @@ import { ShapeCache } from "../scene/ShapeCache"; import { getVerticalOffset } from "../fonts"; import { isRightAngleRads } from "../../math"; import { getCornerRadius } from "../shapes"; +import { applyDarkModeFilter } from "../colors"; // 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 @@ -247,9 +248,9 @@ const generateElementCanvas = ( const rc = rough.canvas(canvas); // 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); @@ -403,7 +404,10 @@ const drawElementOnCanvas = ( case "freedraw": { // Draw directly to canvas context.save(); - context.fillStyle = element.strokeColor; + context.fillStyle = + appState.theme === THEME.DARK + ? applyDarkModeFilter(element.strokeColor) + : element.strokeColor; const path = getFreeDrawPath2D(element) as Path2D; const fillShape = ShapeCache.get(element); @@ -412,7 +416,10 @@ const drawElementOnCanvas = ( rc.draw(fillShape); } - context.fillStyle = element.strokeColor; + context.fillStyle = + appState.theme === THEME.DARK + ? applyDarkModeFilter(element.strokeColor) + : element.strokeColor; context.fill(path); context.restore(); @@ -458,7 +465,10 @@ const drawElementOnCanvas = ( context.canvas.setAttribute("dir", rtl ? "rtl" : "ltr"); context.save(); 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; // Canvas does not support multiline text by default @@ -699,7 +709,10 @@ export const renderElement = ( context.fillStyle = "rgba(0, 0, 200, 0.04)"; 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 if (isMagicFrameElement(element)) { diff --git a/packages/excalidraw/renderer/staticSvgScene.ts b/packages/excalidraw/renderer/staticSvgScene.ts index f0bf98967..80b579dcf 100644 --- a/packages/excalidraw/renderer/staticSvgScene.ts +++ b/packages/excalidraw/renderer/staticSvgScene.ts @@ -5,6 +5,7 @@ import { MAX_DECIMALS_FOR_SVG_EXPORT, MIME_TYPES, SVG_NS, + THEME, } from "../constants"; import { normalizeLink, toValidURL } from "../data/url"; import { getElementAbsoluteCoords } from "../element"; @@ -37,6 +38,7 @@ import { getFontFamilyString, isRTL, isTestEnv } from "../utils"; import { getFreeDrawSvgPath, IMAGE_INVERT_FILTER } from "./renderElement"; import { getVerticalOffset } from "../fonts"; import { getCornerRadius, isPathALoop } from "../shapes"; +import { applyDarkModeFilter } from "../colors"; const roughSVGDrawWithPrecision = ( rsvg: RoughSVG, @@ -139,7 +141,7 @@ const renderElementToSvg = ( case "rectangle": case "diamond": case "ellipse": { - const shape = ShapeCache.generateElementShape(element, null); + const shape = ShapeCache.generateElementShape(element, renderConfig); const node = roughSVGDrawWithPrecision( rsvg, shape, @@ -389,7 +391,12 @@ const renderElementToSvg = ( ); node.setAttribute("stroke", "none"); 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)); node.appendChild(path); @@ -526,7 +533,12 @@ const renderElementToSvg = ( rect.setAttribute("ry", FRAME_STYLE.radius.toString()); 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()); addToRoot(rect, element); @@ -577,7 +589,12 @@ const renderElementToSvg = ( text.setAttribute("y", `${i * lineHeightPx + verticalOffset}`); text.setAttribute("font-family", getFontFamilyString(element)); 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("style", "white-space: pre;"); text.setAttribute("direction", direction); diff --git a/packages/excalidraw/scene/Shape.ts b/packages/excalidraw/scene/Shape.ts index fad0f4f93..2563cc032 100644 --- a/packages/excalidraw/scene/Shape.ts +++ b/packages/excalidraw/scene/Shape.ts @@ -13,7 +13,7 @@ import type { import { generateFreeDrawShape } from "../renderer/renderElement"; import { isTransparent, assertNever } from "../utils"; import { simplify } from "points-on-curve"; -import { ROUGHNESS } from "../constants"; +import { ROUGHNESS, THEME } from "../constants"; import { isElbowArrow, isEmbeddableElement, @@ -22,7 +22,7 @@ import { isLinearElement, } from "../element/typeChecks"; import { canChangeRoundness } from "./comparisons"; -import type { EmbedsValidationStatus } from "../types"; +import type { AppState, EmbedsValidationStatus } from "../types"; import { point, pointDistance, @@ -30,6 +30,7 @@ import { type LocalPoint, } from "../../math"; import { getCornerRadius, isPathALoop } from "../shapes"; +import { applyDarkModeFilter } from "../colors"; const getDashArrayDashed = (strokeWidth: number) => [8, 8 + strokeWidth]; @@ -61,6 +62,7 @@ function adjustRoughness(element: ExcalidrawElement): number { export const generateRoughOptions = ( element: ExcalidrawElement, continuousPath = false, + isDarkMode: boolean = false, ): Options => { const options: Options = { seed: element.seed, @@ -85,7 +87,9 @@ export const generateRoughOptions = ( fillWeight: element.strokeWidth / 2, hachureGap: element.strokeWidth * 4, roughness: adjustRoughness(element), - stroke: element.strokeColor, + stroke: isDarkMode + ? applyDarkModeFilter(element.strokeColor) + : element.strokeColor, preserveVertices: continuousPath || element.roughness < ROUGHNESS.cartoonist, }; @@ -99,6 +103,8 @@ export const generateRoughOptions = ( options.fillStyle = element.fillStyle; options.fill = isTransparent(element.backgroundColor) ? undefined + : isDarkMode + ? applyDarkModeFilter(element.backgroundColor) : element.backgroundColor; if (element.type === "ellipse") { options.curveFitting = 1; @@ -112,6 +118,8 @@ export const generateRoughOptions = ( options.fill = element.backgroundColor === "transparent" ? undefined + : isDarkMode + ? applyDarkModeFilter(element.backgroundColor) : element.backgroundColor; } return options; @@ -165,6 +173,7 @@ const getArrowheadShapes = ( generator: RoughGenerator, options: Options, canvasBackgroundColor: string, + isDarkMode: boolean, ) => { const arrowheadPoints = getArrowheadPoints( element, @@ -192,10 +201,14 @@ const getArrowheadShapes = ( fill: arrowhead === "circle_outline" ? canvasBackgroundColor + : isDarkMode + ? applyDarkModeFilter(element.strokeColor) : element.strokeColor, fillStyle: "solid", - stroke: element.strokeColor, + stroke: isDarkMode + ? applyDarkModeFilter(element.strokeColor) + : element.strokeColor, roughness: Math.min(0.5, options.roughness || 0), }), ]; @@ -220,6 +233,8 @@ const getArrowheadShapes = ( fill: arrowhead === "triangle_outline" ? canvasBackgroundColor + : isDarkMode + ? applyDarkModeFilter(element.strokeColor) : element.strokeColor, fillStyle: "solid", roughness: Math.min(1, options.roughness || 0), @@ -248,6 +263,8 @@ const getArrowheadShapes = ( fill: arrowhead === "diamond_outline" ? canvasBackgroundColor + : isDarkMode + ? applyDarkModeFilter(element.strokeColor) : element.strokeColor, fillStyle: "solid", roughness: Math.min(1, options.roughness || 0), @@ -291,12 +308,15 @@ export const _generateElementShape = ( isExporting, canvasBackgroundColor, embedsValidationStatus, + theme, }: { isExporting: boolean; canvasBackgroundColor: string; embedsValidationStatus: EmbedsValidationStatus | null; + theme: AppState["theme"]; }, ): Drawable | Drawable[] | null => { + const isDarkMode = theme === THEME.DARK; switch (element.type) { case "rectangle": case "iframe": @@ -322,6 +342,7 @@ export const _generateElementShape = ( embedsValidationStatus, ), true, + isDarkMode, ), ); } else { @@ -337,6 +358,7 @@ export const _generateElementShape = ( embedsValidationStatus, ), false, + isDarkMode, ), ); } @@ -374,7 +396,7 @@ export const _generateElementShape = ( C ${topX} ${topY}, ${topX} ${topY}, ${topX + verticalRadius} ${ topY + horizontalRadius }`, - generateRoughOptions(element, true), + generateRoughOptions(element, true, isDarkMode), ); } else { shape = generator.polygon( @@ -384,7 +406,7 @@ export const _generateElementShape = ( [bottomX, bottomY], [leftX, leftY], ], - generateRoughOptions(element), + generateRoughOptions(element, undefined, isDarkMode), ); } return shape; @@ -395,14 +417,14 @@ export const _generateElementShape = ( element.height / 2, element.width, element.height, - generateRoughOptions(element), + generateRoughOptions(element, undefined, isDarkMode), ); return shape; } case "line": case "arrow": { 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 // initial position to it @@ -414,7 +436,7 @@ export const _generateElementShape = ( shape = [ generator.path( generateElbowArrowShape(points, 16), - generateRoughOptions(element, true), + generateRoughOptions(element, true, isDarkMode), ), ]; } else if (!element.roundness) { @@ -446,6 +468,7 @@ export const _generateElementShape = ( generator, options, canvasBackgroundColor, + isDarkMode, ); shape.push(...shapes); } @@ -463,6 +486,7 @@ export const _generateElementShape = ( generator, options, canvasBackgroundColor, + isDarkMode, ); shape.push(...shapes); } @@ -477,7 +501,7 @@ export const _generateElementShape = ( // generate rough polygon to fill freedraw shape const simplifiedPoints = simplify(element.points, 0.75); shape = generator.curve(simplifiedPoints as [number, number][], { - ...generateRoughOptions(element), + ...generateRoughOptions(element, undefined, isDarkMode), stroke: "none", }); } else { diff --git a/packages/excalidraw/scene/ShapeCache.ts b/packages/excalidraw/scene/ShapeCache.ts index 39d388a7b..86747b6f6 100644 --- a/packages/excalidraw/scene/ShapeCache.ts +++ b/packages/excalidraw/scene/ShapeCache.ts @@ -9,6 +9,7 @@ import { _generateElementShape } from "./Shape"; import type { ElementShape, ElementShapes } from "./types"; import { COLOR_PALETTE } from "../colors"; import type { AppState, EmbedsValidationStatus } from "../types"; +import { THEME } from ".."; export class ShapeCache { private static rg = new RoughGenerator(); @@ -52,6 +53,7 @@ export class ShapeCache { isExporting: boolean; canvasBackgroundColor: AppState["viewBackgroundColor"]; embedsValidationStatus: EmbedsValidationStatus; + theme: AppState["theme"]; } | null, ) => { // when exporting, always regenerated to guarantee the latest shape @@ -74,6 +76,7 @@ export class ShapeCache { isExporting: false, canvasBackgroundColor: COLOR_PALETTE.white, embedsValidationStatus: null, + theme: THEME.LIGHT, }, ) as T["type"] extends keyof ElementShapes ? ElementShapes[T["type"]] diff --git a/packages/excalidraw/scene/export.ts b/packages/excalidraw/scene/export.ts index b120d0cc9..4c384ee21 100644 --- a/packages/excalidraw/scene/export.ts +++ b/packages/excalidraw/scene/export.ts @@ -40,6 +40,7 @@ import { syncInvalidIndices } from "../fractionalIndex"; import { renderStaticScene } from "../renderer/staticScene"; import { Fonts } from "../fonts"; import type { Font } from "../fonts/ExcalidrawFont"; +import { applyDarkModeFilter } from "../colors"; const SVG_EXPORT_TAG = ``; @@ -214,6 +215,8 @@ export const exportToCanvas = async ( files, }); + const theme = appState.exportWithDarkMode ? THEME.DARK : THEME.LIGHT; + renderStaticScene({ canvas, rc: rough.canvas(canvas), @@ -233,7 +236,7 @@ export const exportToCanvas = async ( scrollY: -minY + exportPadding, zoom: defaultAppState.zoom, shouldCacheIgnoreZoom: false, - theme: appState.exportWithDarkMode ? THEME.DARK : THEME.LIGHT, + theme, }, renderConfig: { canvasBackgroundColor: viewBackgroundColor, @@ -244,6 +247,7 @@ export const exportToCanvas = async ( embedsValidationStatus: new Map(), elementsPendingErasure: new Set(), pendingFlowchartNodes: null, + theme, }, }); @@ -330,7 +334,7 @@ export const exportToSvg = async ( svgRoot.setAttribute("width", `${width * exportScale}`); svgRoot.setAttribute("height", `${height * exportScale}`); if (exportWithDarkMode) { - svgRoot.setAttribute("filter", THEME_FILTER); + // svgRoot.setAttribute("filter", THEME_FILTER); } const offsetX = -minX + exportPadding; @@ -376,7 +380,12 @@ export const exportToSvg = async ( rect.setAttribute("y", "0"); rect.setAttribute("width", `${width}`); rect.setAttribute("height", `${height}`); - rect.setAttribute("fill", viewBackgroundColor); + rect.setAttribute( + "fill", + appState.exportWithDarkMode + ? applyDarkModeFilter(viewBackgroundColor) + : viewBackgroundColor, + ); svgRoot.appendChild(rect); } @@ -384,6 +393,8 @@ export const exportToSvg = async ( const renderEmbeddables = opts?.renderEmbeddables ?? false; + const theme = appState.exportWithDarkMode ? THEME.DARK : THEME.LIGHT; + renderSceneToSvg( elementsForRender, toBrandedType(arrayToMap(elementsForRender)), @@ -405,6 +416,7 @@ export const exportToSvg = async ( .map((element) => [element.id, true]), ) : new Map(), + theme, }, ); diff --git a/packages/excalidraw/scene/types.ts b/packages/excalidraw/scene/types.ts index 67ab3e3ab..7df7703d8 100644 --- a/packages/excalidraw/scene/types.ts +++ b/packages/excalidraw/scene/types.ts @@ -35,6 +35,7 @@ export type StaticCanvasRenderConfig = { embedsValidationStatus: EmbedsValidationStatus; elementsPendingErasure: ElementsPendingErasure; pendingFlowchartNodes: PendingExcalidrawElements | null; + theme: AppState["theme"]; }; export type SVGRenderConfig = { @@ -46,6 +47,7 @@ export type SVGRenderConfig = { frameRendering: AppState["frameRendering"]; canvasBackgroundColor: AppState["viewBackgroundColor"]; embedsValidationStatus: EmbedsValidationStatus; + theme: AppState["theme"]; }; export type InteractiveCanvasRenderConfig = { diff --git a/yarn.lock b/yarn.lock index ffdcab4e2..209a35799 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3299,6 +3299,11 @@ dependencies: "@types/jest" "*" +"@types/tinycolor2@1.4.6": + 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": version "2.0.7" 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" integrity sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg== +tinycolor2@1.6.0: + 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: version "1.0.1" resolved "https://registry.yarnpkg.com/tinypool/-/tinypool-1.0.1.tgz#c64233c4fac4304e109a64340178760116dbe1fe"