feat: auto white text outline for dark colors
This commit is contained in:
parent
68e8636782
commit
00a139af1b
@ -50,6 +50,7 @@ import {
|
|||||||
DEFAULT_FONT_FAMILY,
|
DEFAULT_FONT_FAMILY,
|
||||||
DEFAULT_FONT_SIZE,
|
DEFAULT_FONT_SIZE,
|
||||||
FONT_FAMILY,
|
FONT_FAMILY,
|
||||||
|
FONT_SIZE,
|
||||||
ROUNDNESS,
|
ROUNDNESS,
|
||||||
VERTICAL_ALIGN,
|
VERTICAL_ALIGN,
|
||||||
} from "../constants";
|
} from "../constants";
|
||||||
@ -567,25 +568,25 @@ export const actionChangeFontSize = register({
|
|||||||
group="font-size"
|
group="font-size"
|
||||||
options={[
|
options={[
|
||||||
{
|
{
|
||||||
value: 16,
|
value: FONT_SIZE.small,
|
||||||
text: t("labels.small"),
|
text: t("labels.small"),
|
||||||
icon: FontSizeSmallIcon,
|
icon: FontSizeSmallIcon,
|
||||||
testId: "fontSize-small",
|
testId: "fontSize-small",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
value: 20,
|
value: FONT_SIZE.medium,
|
||||||
text: t("labels.medium"),
|
text: t("labels.medium"),
|
||||||
icon: FontSizeMediumIcon,
|
icon: FontSizeMediumIcon,
|
||||||
testId: "fontSize-medium",
|
testId: "fontSize-medium",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
value: 28,
|
value: FONT_SIZE.large,
|
||||||
text: t("labels.large"),
|
text: t("labels.large"),
|
||||||
icon: FontSizeLargeIcon,
|
icon: FontSizeLargeIcon,
|
||||||
testId: "fontSize-large",
|
testId: "fontSize-large",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
value: 36,
|
value: FONT_SIZE.veryLarge,
|
||||||
text: t("labels.veryLarge"),
|
text: t("labels.veryLarge"),
|
||||||
icon: FontSizeExtraLargeIcon,
|
icon: FontSizeExtraLargeIcon,
|
||||||
testId: "fontSize-veryLarge",
|
testId: "fontSize-veryLarge",
|
||||||
|
@ -6,6 +6,7 @@ import {
|
|||||||
import {
|
import {
|
||||||
DEFAULT_FONT_FAMILY,
|
DEFAULT_FONT_FAMILY,
|
||||||
DEFAULT_FONT_SIZE,
|
DEFAULT_FONT_SIZE,
|
||||||
|
FONT_SIZE,
|
||||||
ENV,
|
ENV,
|
||||||
VERTICAL_ALIGN,
|
VERTICAL_ALIGN,
|
||||||
} from "./constants";
|
} from "./constants";
|
||||||
@ -205,7 +206,7 @@ const chartXLabels = (
|
|||||||
y: y + BAR_GAP / 2,
|
y: y + BAR_GAP / 2,
|
||||||
width: BAR_WIDTH,
|
width: BAR_WIDTH,
|
||||||
angle: 5.87,
|
angle: 5.87,
|
||||||
fontSize: 16,
|
fontSize: FONT_SIZE.small,
|
||||||
textAlign: "center",
|
textAlign: "center",
|
||||||
verticalAlign: "top",
|
verticalAlign: "top",
|
||||||
});
|
});
|
||||||
|
@ -167,4 +167,55 @@ export const getAllColorsSpecificShade = (index: 0 | 1 | 2 | 3 | 4) =>
|
|||||||
export const rgbToHex = (r: number, g: number, b: number) =>
|
export const rgbToHex = (r: number, g: number, b: number) =>
|
||||||
`#${((1 << 24) + (r << 16) + (g << 8) + b).toString(16).slice(1)}`;
|
`#${((1 << 24) + (r << 16) + (g << 8) + b).toString(16).slice(1)}`;
|
||||||
|
|
||||||
|
const parseRGBFunction = (
|
||||||
|
/**
|
||||||
|
* `rgb()` or `rgba()` color string
|
||||||
|
*/
|
||||||
|
rgbFunctionValue: string,
|
||||||
|
) => {
|
||||||
|
const match = rgbFunctionValue.match(/(rgba?|hsla?)\(([^)]+)\)/i);
|
||||||
|
if (!match) {
|
||||||
|
return { r: 0, g: 0, b: 0 };
|
||||||
|
}
|
||||||
|
const values = match[2].split(",");
|
||||||
|
const r = parseInt(values[0].trim());
|
||||||
|
const g = parseInt(values[1].trim());
|
||||||
|
const b = parseInt(values[2].trim());
|
||||||
|
|
||||||
|
return { r, g, b };
|
||||||
|
};
|
||||||
|
|
||||||
|
export const colorToRGB = (
|
||||||
|
/**
|
||||||
|
* HTML color, HEX(A), rgba()...
|
||||||
|
*/
|
||||||
|
color: string,
|
||||||
|
) => {
|
||||||
|
const style = new Option().style;
|
||||||
|
style.color = color;
|
||||||
|
|
||||||
|
if (style.color) {
|
||||||
|
return parseRGBFunction(style.color);
|
||||||
|
}
|
||||||
|
return { r: 0, g: 0, b: 0 };
|
||||||
|
};
|
||||||
|
|
||||||
|
// inspiration from https://stackoverflow.com/a/11868398
|
||||||
|
export const getContrastingBWColor = (
|
||||||
|
color: string,
|
||||||
|
/**
|
||||||
|
* <0-1>, the closer to one the more light the input color needs to be
|
||||||
|
* to get `black` return value
|
||||||
|
*/
|
||||||
|
threshold = 0.627,
|
||||||
|
) => {
|
||||||
|
if (color === "transparent") {
|
||||||
|
return "black";
|
||||||
|
}
|
||||||
|
|
||||||
|
const { r, g, b } = colorToRGB(color);
|
||||||
|
const yiq = (r * 299 + g * 587 + b * 114) / 1000;
|
||||||
|
return yiq / 255 >= threshold ? "black" : "white";
|
||||||
|
};
|
||||||
|
|
||||||
// -----------------------------------------------------------------------------
|
// -----------------------------------------------------------------------------
|
||||||
|
@ -54,7 +54,7 @@ export const CustomColorList = ({
|
|||||||
key={i}
|
key={i}
|
||||||
>
|
>
|
||||||
<div className="color-picker__button-outline" />
|
<div className="color-picker__button-outline" />
|
||||||
<HotkeyLabel color={c} keyLabel={i + 1} isCustomColor />
|
<HotkeyLabel color={c} keyLabel={i + 1} />
|
||||||
</button>
|
</button>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
|
@ -1,23 +1,21 @@
|
|||||||
import React from "react";
|
import React from "react";
|
||||||
import { getContrastYIQ } from "./colorPickerUtils";
|
import { getContrastingBWColor } from "../../colors";
|
||||||
|
|
||||||
interface HotkeyLabelProps {
|
interface HotkeyLabelProps {
|
||||||
color: string;
|
color: string;
|
||||||
keyLabel: string | number;
|
keyLabel: string | number;
|
||||||
isCustomColor?: boolean;
|
|
||||||
isShade?: boolean;
|
isShade?: boolean;
|
||||||
}
|
}
|
||||||
const HotkeyLabel = ({
|
const HotkeyLabel = ({
|
||||||
color,
|
color,
|
||||||
keyLabel,
|
keyLabel,
|
||||||
isCustomColor = false,
|
|
||||||
isShade = false,
|
isShade = false,
|
||||||
}: HotkeyLabelProps) => {
|
}: HotkeyLabelProps) => {
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className="color-picker__button__hotkey-label"
|
className="color-picker__button__hotkey-label"
|
||||||
style={{
|
style={{
|
||||||
color: getContrastYIQ(color, isCustomColor),
|
color: getContrastingBWColor(color, 0.7),
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{isShade && "⇧"}
|
{isShade && "⇧"}
|
||||||
|
@ -93,43 +93,6 @@ export type ActiveColorPickerSectionAtomType =
|
|||||||
export const activeColorPickerSectionAtom =
|
export const activeColorPickerSectionAtom =
|
||||||
atom<ActiveColorPickerSectionAtomType>(null);
|
atom<ActiveColorPickerSectionAtomType>(null);
|
||||||
|
|
||||||
const calculateContrast = (r: number, g: number, b: number) => {
|
|
||||||
const yiq = (r * 299 + g * 587 + b * 114) / 1000;
|
|
||||||
return yiq >= 160 ? "black" : "white";
|
|
||||||
};
|
|
||||||
|
|
||||||
// inspiration from https://stackoverflow.com/a/11868398
|
|
||||||
export const getContrastYIQ = (bgHex: string, isCustomColor: boolean) => {
|
|
||||||
if (isCustomColor) {
|
|
||||||
const style = new Option().style;
|
|
||||||
style.color = bgHex;
|
|
||||||
|
|
||||||
if (style.color) {
|
|
||||||
const rgb = style.color
|
|
||||||
.replace(/^(rgb|rgba)\(/, "")
|
|
||||||
.replace(/\)$/, "")
|
|
||||||
.replace(/\s/g, "")
|
|
||||||
.split(",");
|
|
||||||
const r = parseInt(rgb[0]);
|
|
||||||
const g = parseInt(rgb[1]);
|
|
||||||
const b = parseInt(rgb[2]);
|
|
||||||
|
|
||||||
return calculateContrast(r, g, b);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO: ? is this wanted?
|
|
||||||
if (bgHex === "transparent") {
|
|
||||||
return "black";
|
|
||||||
}
|
|
||||||
|
|
||||||
const r = parseInt(bgHex.substring(1, 3), 16);
|
|
||||||
const g = parseInt(bgHex.substring(3, 5), 16);
|
|
||||||
const b = parseInt(bgHex.substring(5, 7), 16);
|
|
||||||
|
|
||||||
return calculateContrast(r, g, b);
|
|
||||||
};
|
|
||||||
|
|
||||||
export type ColorPickerType =
|
export type ColorPickerType =
|
||||||
| "canvasBackground"
|
| "canvasBackground"
|
||||||
| "elementBackground"
|
| "elementBackground"
|
||||||
|
@ -96,7 +96,14 @@ export const THEME = {
|
|||||||
|
|
||||||
export const WINDOWS_EMOJI_FALLBACK_FONT = "Segoe UI Emoji";
|
export const WINDOWS_EMOJI_FALLBACK_FONT = "Segoe UI Emoji";
|
||||||
|
|
||||||
export const DEFAULT_FONT_SIZE = 20;
|
export const FONT_SIZE = {
|
||||||
|
small: 16,
|
||||||
|
medium: 20,
|
||||||
|
large: 28,
|
||||||
|
veryLarge: 36,
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
export const DEFAULT_FONT_SIZE = FONT_SIZE.medium;
|
||||||
export const DEFAULT_FONT_FAMILY: FontFamilyValues = FONT_FAMILY.Virgil;
|
export const DEFAULT_FONT_FAMILY: FontFamilyValues = FONT_FAMILY.Virgil;
|
||||||
export const DEFAULT_TEXT_ALIGN = "left";
|
export const DEFAULT_TEXT_ALIGN = "left";
|
||||||
export const DEFAULT_VERTICAL_ALIGN = "top";
|
export const DEFAULT_VERTICAL_ALIGN = "top";
|
||||||
@ -295,3 +302,6 @@ export const DEFAULT_SIDEBAR = {
|
|||||||
name: "default",
|
name: "default",
|
||||||
defaultTab: LIBRARY_SIDEBAR_TAB,
|
defaultTab: LIBRARY_SIDEBAR_TAB,
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
|
export const TEXT_OUTLINE_DEFAULT_WIDTH = 2;
|
||||||
|
export const TEXT_OUTLINE_CONTRAST_THRESHOLD = 0.62;
|
||||||
|
@ -44,6 +44,10 @@ import { actionZoomIn, actionZoomOut } from "../actions/actionCanvas";
|
|||||||
import App from "../components/App";
|
import App from "../components/App";
|
||||||
import { LinearElementEditor } from "./linearElementEditor";
|
import { LinearElementEditor } from "./linearElementEditor";
|
||||||
import { parseClipboard } from "../clipboard";
|
import { parseClipboard } from "../clipboard";
|
||||||
|
import {
|
||||||
|
getTextOutlineColor,
|
||||||
|
getTextOutlineWidth,
|
||||||
|
} from "../renderer/renderElement";
|
||||||
|
|
||||||
const getTransform = (
|
const getTransform = (
|
||||||
width: number,
|
width: number,
|
||||||
@ -297,6 +301,31 @@ export const textWysiwyg = ({
|
|||||||
if (isTestEnv()) {
|
if (isTestEnv()) {
|
||||||
editable.style.fontFamily = getFontFamilyString(updatedTextElement);
|
editable.style.fontFamily = getFontFamilyString(updatedTextElement);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const textOutlineColor = getTextOutlineColor(
|
||||||
|
updatedTextElement.strokeColor,
|
||||||
|
appState.viewBackgroundColor,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (textOutlineColor) {
|
||||||
|
// text-shadow must be around half of the outline width
|
||||||
|
const textShadowWidth =
|
||||||
|
getTextOutlineWidth(
|
||||||
|
updatedTextElement.fontSize,
|
||||||
|
appState.zoom.value,
|
||||||
|
) / 2;
|
||||||
|
const p = `${textShadowWidth}px`;
|
||||||
|
const n = `-${textShadowWidth}px`;
|
||||||
|
|
||||||
|
editable.style.setProperty(
|
||||||
|
"text-shadow",
|
||||||
|
`${n} ${n} 0 ${textOutlineColor}, ${p} ${n} 0 ${textOutlineColor},
|
||||||
|
${n} ${p} 0 ${textOutlineColor}, ${p} ${p} 0 ${textOutlineColor}`,
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
editable.style.removeProperty("text-shadow");
|
||||||
|
}
|
||||||
|
|
||||||
mutateElement(updatedTextElement, { x: coordX, y: coordY });
|
mutateElement(updatedTextElement, { x: coordX, y: coordY });
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
@ -30,14 +30,17 @@ import { RenderConfig } from "../scene/types";
|
|||||||
import { distance, getFontString, getFontFamilyString, isRTL } from "../utils";
|
import { distance, getFontString, getFontFamilyString, isRTL } from "../utils";
|
||||||
import { getCornerRadius, isPathALoop, isRightAngle } from "../math";
|
import { getCornerRadius, isPathALoop, isRightAngle } from "../math";
|
||||||
import rough from "roughjs/bin/rough";
|
import rough from "roughjs/bin/rough";
|
||||||
import { AppState, BinaryFiles, Zoom } from "../types";
|
import { AppState, BinaryFiles, NormalizedZoomValue, Zoom } from "../types";
|
||||||
import { getDefaultAppState } from "../appState";
|
import { getDefaultAppState } from "../appState";
|
||||||
import {
|
import {
|
||||||
BOUND_TEXT_PADDING,
|
BOUND_TEXT_PADDING,
|
||||||
|
FONT_SIZE,
|
||||||
MAX_DECIMALS_FOR_SVG_EXPORT,
|
MAX_DECIMALS_FOR_SVG_EXPORT,
|
||||||
MIME_TYPES,
|
MIME_TYPES,
|
||||||
ROUGHNESS,
|
ROUGHNESS,
|
||||||
SVG_NS,
|
SVG_NS,
|
||||||
|
TEXT_OUTLINE_CONTRAST_THRESHOLD,
|
||||||
|
TEXT_OUTLINE_DEFAULT_WIDTH,
|
||||||
} from "../constants";
|
} from "../constants";
|
||||||
import { getStroke, StrokeOptions } from "perfect-freehand";
|
import { getStroke, StrokeOptions } from "perfect-freehand";
|
||||||
import {
|
import {
|
||||||
@ -49,6 +52,31 @@ import {
|
|||||||
getBoundTextMaxWidth,
|
getBoundTextMaxWidth,
|
||||||
} from "../element/textElement";
|
} from "../element/textElement";
|
||||||
import { LinearElementEditor } from "../element/linearElementEditor";
|
import { LinearElementEditor } from "../element/linearElementEditor";
|
||||||
|
import { COLOR_PALETTE, getContrastingBWColor } from "../colors";
|
||||||
|
|
||||||
|
/** @returns null if outline should be disabled */
|
||||||
|
export const getTextOutlineColor = (
|
||||||
|
strokeColor: ExcalidrawElement["strokeColor"],
|
||||||
|
viewBackgroundColor: AppState["viewBackgroundColor"] | null,
|
||||||
|
) => {
|
||||||
|
return getContrastingBWColor(strokeColor, TEXT_OUTLINE_CONTRAST_THRESHOLD) ===
|
||||||
|
"white"
|
||||||
|
? viewBackgroundColor || COLOR_PALETTE.white
|
||||||
|
: null;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getTextOutlineWidth = (
|
||||||
|
fontSize: ExcalidrawTextElement["fontSize"],
|
||||||
|
zoom: NormalizedZoomValue,
|
||||||
|
) => {
|
||||||
|
const normalizedZoom = Math.max(1, zoom);
|
||||||
|
const width = Math.max(
|
||||||
|
TEXT_OUTLINE_DEFAULT_WIDTH / 2,
|
||||||
|
TEXT_OUTLINE_DEFAULT_WIDTH / normalizedZoom,
|
||||||
|
);
|
||||||
|
|
||||||
|
return width * (fontSize / FONT_SIZE.medium);
|
||||||
|
};
|
||||||
|
|
||||||
// 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
|
||||||
@ -338,7 +366,26 @@ const drawElementOnCanvas = (
|
|||||||
element.lineHeight,
|
element.lineHeight,
|
||||||
);
|
);
|
||||||
const verticalOffset = element.height - element.baseline;
|
const verticalOffset = element.height - element.baseline;
|
||||||
|
|
||||||
|
const textOutlineColor = getTextOutlineColor(
|
||||||
|
element.strokeColor,
|
||||||
|
renderConfig.viewBackgroundColor,
|
||||||
|
);
|
||||||
|
|
||||||
for (let index = 0; index < lines.length; index++) {
|
for (let index = 0; index < lines.length; index++) {
|
||||||
|
if (textOutlineColor) {
|
||||||
|
context.strokeStyle = textOutlineColor;
|
||||||
|
context.lineWidth = getTextOutlineWidth(
|
||||||
|
element.fontSize,
|
||||||
|
renderConfig.zoom.value,
|
||||||
|
);
|
||||||
|
|
||||||
|
context.strokeText(
|
||||||
|
lines[index],
|
||||||
|
horizontalOffset,
|
||||||
|
(index + 1) * lineHeightPx - verticalOffset,
|
||||||
|
);
|
||||||
|
}
|
||||||
context.fillText(
|
context.fillText(
|
||||||
lines[index],
|
lines[index],
|
||||||
horizontalOffset,
|
horizontalOffset,
|
||||||
@ -1128,6 +1175,7 @@ export const renderElementToSvg = (
|
|||||||
files: BinaryFiles,
|
files: BinaryFiles,
|
||||||
offsetX: number,
|
offsetX: number,
|
||||||
offsetY: number,
|
offsetY: number,
|
||||||
|
viewBackgroundColor: AppState["viewBackgroundColor"],
|
||||||
exportWithDarkMode?: boolean,
|
exportWithDarkMode?: boolean,
|
||||||
) => {
|
) => {
|
||||||
const [x1, y1, x2, y2] = getElementAbsoluteCoords(element);
|
const [x1, y1, x2, y2] = getElementAbsoluteCoords(element);
|
||||||
@ -1396,6 +1444,39 @@ export const renderElementToSvg = (
|
|||||||
? "end"
|
? "end"
|
||||||
: "start";
|
: "start";
|
||||||
for (let i = 0; i < lines.length; i++) {
|
for (let i = 0; i < lines.length; i++) {
|
||||||
|
const textOutlineColor = getTextOutlineColor(
|
||||||
|
element.strokeColor,
|
||||||
|
viewBackgroundColor,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (textOutlineColor) {
|
||||||
|
// create a text stroke
|
||||||
|
const stroke = svgRoot.ownerDocument!.createElementNS(
|
||||||
|
SVG_NS,
|
||||||
|
"text",
|
||||||
|
);
|
||||||
|
stroke.textContent = lines[i];
|
||||||
|
stroke.setAttribute("x", `${horizontalOffset}`);
|
||||||
|
stroke.setAttribute("y", `${i * lineHeightPx}`);
|
||||||
|
stroke.setAttribute("font-family", getFontFamilyString(element));
|
||||||
|
stroke.setAttribute("font-size", `${element.fontSize}px`);
|
||||||
|
stroke.setAttribute("fill", element.strokeColor);
|
||||||
|
stroke.setAttribute("stroke", textOutlineColor);
|
||||||
|
stroke.setAttribute(
|
||||||
|
"stroke-wiidth",
|
||||||
|
`${getTextOutlineWidth(
|
||||||
|
element.fontSize,
|
||||||
|
1 as NormalizedZoomValue,
|
||||||
|
)}px`,
|
||||||
|
);
|
||||||
|
stroke.setAttribute("text-anchor", textAnchor);
|
||||||
|
stroke.setAttribute("style", "white-space: pre;");
|
||||||
|
stroke.setAttribute("direction", direction);
|
||||||
|
stroke.setAttribute("dominant-baseline", "text-before-edge");
|
||||||
|
node.appendChild(stroke);
|
||||||
|
}
|
||||||
|
|
||||||
|
// create a text fill
|
||||||
const text = svgRoot.ownerDocument!.createElementNS(SVG_NS, "text");
|
const text = svgRoot.ownerDocument!.createElementNS(SVG_NS, "text");
|
||||||
text.textContent = lines[i];
|
text.textContent = lines[i];
|
||||||
text.setAttribute("x", `${horizontalOffset}`);
|
text.setAttribute("x", `${horizontalOffset}`);
|
||||||
|
@ -1135,11 +1135,13 @@ export const renderSceneToSvg = (
|
|||||||
offsetX = 0,
|
offsetX = 0,
|
||||||
offsetY = 0,
|
offsetY = 0,
|
||||||
exportWithDarkMode = false,
|
exportWithDarkMode = false,
|
||||||
|
viewBackgroundColor,
|
||||||
}: {
|
}: {
|
||||||
offsetX?: number;
|
offsetX?: number;
|
||||||
offsetY?: number;
|
offsetY?: number;
|
||||||
exportWithDarkMode?: boolean;
|
exportWithDarkMode?: boolean;
|
||||||
} = {},
|
viewBackgroundColor: AppState["viewBackgroundColor"];
|
||||||
|
},
|
||||||
) => {
|
) => {
|
||||||
if (!svgRoot) {
|
if (!svgRoot) {
|
||||||
return;
|
return;
|
||||||
@ -1155,6 +1157,7 @@ export const renderSceneToSvg = (
|
|||||||
files,
|
files,
|
||||||
element.x + offsetX,
|
element.x + offsetX,
|
||||||
element.y + offsetY,
|
element.y + offsetY,
|
||||||
|
viewBackgroundColor,
|
||||||
exportWithDarkMode,
|
exportWithDarkMode,
|
||||||
);
|
);
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
|
@ -172,6 +172,7 @@ export const exportToSvg = async (
|
|||||||
offsetX: -minX + exportPadding,
|
offsetX: -minX + exportPadding,
|
||||||
offsetY: -minY + exportPadding,
|
offsetY: -minY + exportPadding,
|
||||||
exportWithDarkMode: appState.exportWithDarkMode,
|
exportWithDarkMode: appState.exportWithDarkMode,
|
||||||
|
viewBackgroundColor: appState.viewBackgroundColor,
|
||||||
});
|
});
|
||||||
|
|
||||||
return svgRoot;
|
return svgRoot;
|
||||||
|
Loading…
x
Reference in New Issue
Block a user