feat: stop using CSS filters for dark mode (static canvas) (#10578)
* feat: stop using CSS filters for dark mode (static canvas) * fix comment * remove conditional dark mode export * make shape cache theme-aware * refactor * refactor * fixes and notes
This commit is contained in:
@@ -47,7 +47,6 @@ import {
|
||||
TAP_TWICE_TIMEOUT,
|
||||
TEXT_TO_CENTER_SNAP_THRESHOLD,
|
||||
THEME,
|
||||
THEME_FILTER,
|
||||
TOUCH_CTX_MENU_TIMEOUT,
|
||||
VERTICAL_ALIGN,
|
||||
YOUTUBE_STATES,
|
||||
@@ -89,6 +88,7 @@ import {
|
||||
getDateTime,
|
||||
isShallowEqual,
|
||||
arrayToMap,
|
||||
applyDarkModeFilter,
|
||||
type EXPORT_IMAGE_TYPES,
|
||||
randomInteger,
|
||||
CLASSES,
|
||||
@@ -1770,8 +1770,9 @@ class App extends React.Component<AppProps, AppState> {
|
||||
}
|
||||
}}
|
||||
style={{
|
||||
background: this.state.viewBackgroundColor,
|
||||
filter: isDarkTheme ? THEME_FILTER : "none",
|
||||
background: isDarkTheme
|
||||
? applyDarkModeFilter(this.state.viewBackgroundColor)
|
||||
: this.state.viewBackgroundColor,
|
||||
zIndex: 2,
|
||||
border: "none",
|
||||
display: "block",
|
||||
@@ -1781,7 +1782,9 @@ class App extends React.Component<AppProps, AppState> {
|
||||
fontFamily: "Assistant",
|
||||
fontSize: `${FRAME_STYLE.nameFontSize}px`,
|
||||
transform: `translate(-${FRAME_NAME_EDIT_PADDING}px, ${FRAME_NAME_EDIT_PADDING}px)`,
|
||||
color: "var(--color-gray-80)",
|
||||
color: isDarkTheme
|
||||
? FRAME_STYLE.nameColorDarkTheme
|
||||
: FRAME_STYLE.nameColorLightTheme,
|
||||
overflow: "hidden",
|
||||
maxWidth: `${
|
||||
document.body.clientWidth - x1 - FRAME_NAME_EDIT_PADDING
|
||||
@@ -2116,6 +2119,7 @@ class App extends React.Component<AppProps, AppState> {
|
||||
elementsPendingErasure: this.elementsPendingErasure,
|
||||
pendingFlowchartNodes:
|
||||
this.flowChartCreator.pendingNodes,
|
||||
theme: this.state.theme,
|
||||
}}
|
||||
/>
|
||||
{this.state.newElement && (
|
||||
@@ -2136,6 +2140,7 @@ class App extends React.Component<AppProps, AppState> {
|
||||
elementsPendingErasure:
|
||||
this.elementsPendingErasure,
|
||||
pendingFlowchartNodes: null,
|
||||
theme: this.state.theme,
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
@@ -3181,6 +3186,7 @@ class App extends React.Component<AppProps, AppState> {
|
||||
) {
|
||||
setEraserCursor(this.interactiveCanvas, this.state.theme);
|
||||
}
|
||||
|
||||
// Hide hyperlink popup if shown when element type is not selection
|
||||
if (
|
||||
prevState.activeTool.type === "selection" &&
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
import clsx from "clsx";
|
||||
import { useCallback, useEffect, useRef, useState } from "react";
|
||||
|
||||
import { KEYS } from "@excalidraw/common";
|
||||
import { isTransparent, KEYS } from "@excalidraw/common";
|
||||
|
||||
import tinycolor from "tinycolor2";
|
||||
|
||||
import { getShortcutKey } from "../..//shortcut";
|
||||
import { useAtom } from "../../editor-jotai";
|
||||
@@ -10,18 +12,32 @@ import { useEditorInterface } from "../App";
|
||||
import { activeEyeDropperAtom } from "../EyeDropper";
|
||||
import { eyeDropperIcon } from "../icons";
|
||||
|
||||
import { getColor } from "./ColorPicker";
|
||||
import { activeColorPickerSectionAtom } from "./colorPickerUtils";
|
||||
|
||||
import type { ColorPickerType } from "./colorPickerUtils";
|
||||
|
||||
interface ColorInputProps {
|
||||
color: string;
|
||||
onChange: (color: string) => void;
|
||||
label: string;
|
||||
colorPickerType: ColorPickerType;
|
||||
placeholder?: string;
|
||||
}
|
||||
/**
|
||||
* tries to keep the input color as-is if it's valid, making minimal adjustments
|
||||
* (trimming whitespace or adding `#` to hex colors)
|
||||
*/
|
||||
export const normalizeInputColor = (color: string): string | null => {
|
||||
color = color.trim();
|
||||
if (isTransparent(color)) {
|
||||
return color;
|
||||
}
|
||||
|
||||
const tc = tinycolor(color);
|
||||
if (tc.isValid()) {
|
||||
// testing for `#` first fixes a bug on Electron (more specfically, an
|
||||
// Obsidian popout window), where a hex color without `#` is considered valid
|
||||
if (tc.getFormat() === "hex" && !color.startsWith("#")) {
|
||||
return `#${color}`;
|
||||
}
|
||||
return color;
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
export const ColorInput = ({
|
||||
color,
|
||||
@@ -29,7 +45,13 @@ export const ColorInput = ({
|
||||
label,
|
||||
colorPickerType,
|
||||
placeholder,
|
||||
}: ColorInputProps) => {
|
||||
}: {
|
||||
color: string;
|
||||
onChange: (color: string) => void;
|
||||
label: string;
|
||||
colorPickerType: ColorPickerType;
|
||||
placeholder?: string;
|
||||
}) => {
|
||||
const editorInterface = useEditorInterface();
|
||||
const [innerValue, setInnerValue] = useState(color);
|
||||
const [activeSection, setActiveColorPickerSection] = useAtom(
|
||||
@@ -43,7 +65,7 @@ export const ColorInput = ({
|
||||
const changeColor = useCallback(
|
||||
(inputValue: string) => {
|
||||
const value = inputValue.toLowerCase();
|
||||
const color = getColor(value);
|
||||
const color = normalizeInputColor(value);
|
||||
|
||||
if (color) {
|
||||
onChange(color);
|
||||
|
||||
@@ -5,7 +5,6 @@ import { useRef, useEffect } from "react";
|
||||
import {
|
||||
COLOR_OUTLINE_CONTRAST_THRESHOLD,
|
||||
COLOR_PALETTE,
|
||||
isTransparent,
|
||||
isWritableElement,
|
||||
} from "@excalidraw/common";
|
||||
|
||||
@@ -38,27 +37,6 @@ import type { ColorPickerType } from "./colorPickerUtils";
|
||||
|
||||
import type { AppState } from "../../types";
|
||||
|
||||
const isValidColor = (color: string) => {
|
||||
const style = new Option().style;
|
||||
style.color = color;
|
||||
return !!style.color;
|
||||
};
|
||||
|
||||
export const getColor = (color: string): string | null => {
|
||||
if (isTransparent(color)) {
|
||||
return color;
|
||||
}
|
||||
|
||||
// testing for `#` first fixes a bug on Electron (more specfically, an
|
||||
// Obsidian popout window), where a hex color without `#` is (incorrectly)
|
||||
// considered valid
|
||||
return isValidColor(`#${color}`)
|
||||
? `#${color}`
|
||||
: isValidColor(color)
|
||||
? color
|
||||
: null;
|
||||
};
|
||||
|
||||
interface ColorPickerProps {
|
||||
type: ColorPickerType;
|
||||
/**
|
||||
|
||||
@@ -1,4 +1,8 @@
|
||||
import { MAX_CUSTOM_COLORS_USED_IN_CANVAS } from "@excalidraw/common";
|
||||
import {
|
||||
isTransparent,
|
||||
MAX_CUSTOM_COLORS_USED_IN_CANVAS,
|
||||
tinycolor,
|
||||
} from "@excalidraw/common";
|
||||
|
||||
import type { ExcalidrawElement } from "@excalidraw/element/types";
|
||||
|
||||
@@ -108,48 +112,17 @@ export const isColorDark = (color: string, threshold = 160): boolean => {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (color === "transparent") {
|
||||
if (isTransparent(color)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// a string color (white etc) or any other format -> convert to rgb by way
|
||||
// of creating a DOM node and retrieving the computeStyle
|
||||
if (!color.startsWith("#")) {
|
||||
const node = document.createElement("div");
|
||||
node.style.color = color;
|
||||
|
||||
if (node.style.color) {
|
||||
// making invisible so document doesn't reflow (hopefully).
|
||||
// display=none works too, but supposedly not in all browsers
|
||||
node.style.position = "absolute";
|
||||
node.style.visibility = "hidden";
|
||||
node.style.width = "0";
|
||||
node.style.height = "0";
|
||||
|
||||
// needs to be in DOM else browser won't compute the style
|
||||
document.body.appendChild(node);
|
||||
const computedColor = getComputedStyle(node).color;
|
||||
document.body.removeChild(node);
|
||||
// computed style is in rgb() format
|
||||
const rgb = computedColor
|
||||
.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) < threshold;
|
||||
}
|
||||
// invalid color -> assume it default to black
|
||||
const tc = tinycolor(color);
|
||||
if (!tc.isValid()) {
|
||||
// invalid color -> assume it defaults to black
|
||||
return true;
|
||||
}
|
||||
|
||||
const r = parseInt(color.slice(1, 3), 16);
|
||||
const g = parseInt(color.slice(3, 5), 16);
|
||||
const b = parseInt(color.slice(5, 7), 16);
|
||||
|
||||
const { r, g, b } = tc.toRgb();
|
||||
return calculateContrast(r, g, b) < threshold;
|
||||
};
|
||||
|
||||
|
||||
@@ -40,9 +40,6 @@ import type { ActionManager } from "../actions/manager";
|
||||
|
||||
import type { AppClassProperties, BinaryFiles, UIAppState } from "../types";
|
||||
|
||||
const supportsContextFilters =
|
||||
"filter" in document.createElement("canvas").getContext("2d")!;
|
||||
|
||||
export const ErrorCanvasPreview = () => {
|
||||
return (
|
||||
<div>
|
||||
@@ -230,25 +227,23 @@ const ImageExportModal = ({
|
||||
}}
|
||||
/>
|
||||
</ExportSetting>
|
||||
{supportsContextFilters && (
|
||||
<ExportSetting
|
||||
label={t("imageExportDialog.label.darkMode")}
|
||||
<ExportSetting
|
||||
label={t("imageExportDialog.label.darkMode")}
|
||||
name="exportDarkModeSwitch"
|
||||
>
|
||||
<Switch
|
||||
name="exportDarkModeSwitch"
|
||||
>
|
||||
<Switch
|
||||
name="exportDarkModeSwitch"
|
||||
checked={exportDarkMode}
|
||||
onChange={(checked) => {
|
||||
setExportDarkMode(checked);
|
||||
actionManager.executeAction(
|
||||
actionExportWithDarkMode,
|
||||
"ui",
|
||||
checked,
|
||||
);
|
||||
}}
|
||||
/>
|
||||
</ExportSetting>
|
||||
)}
|
||||
checked={exportDarkMode}
|
||||
onChange={(checked) => {
|
||||
setExportDarkMode(checked);
|
||||
actionManager.executeAction(
|
||||
actionExportWithDarkMode,
|
||||
"ui",
|
||||
checked,
|
||||
);
|
||||
}}
|
||||
/>
|
||||
</ExportSetting>
|
||||
<ExportSetting
|
||||
label={t("imageExportDialog.label.embedScene")}
|
||||
tooltip={t("imageExportDialog.tooltip.embedScene")}
|
||||
|
||||
@@ -101,6 +101,12 @@ export const convertMermaidToExcalidraw = async ({
|
||||
maxWidthOrHeight:
|
||||
Math.max(parent.offsetWidth, parent.offsetHeight) *
|
||||
window.devicePixelRatio,
|
||||
appState: {
|
||||
// TODO hack (will be refactored in TTD v2)
|
||||
exportWithDarkMode: document
|
||||
.querySelector(".excalidraw-container")
|
||||
?.classList.contains("theme--dark"),
|
||||
},
|
||||
});
|
||||
// if converting to blob fails, there's some problem that will
|
||||
// likely prevent preview and export (e.g. canvas too big)
|
||||
|
||||
Reference in New Issue
Block a user