feat: dark theme export background

This commit is contained in:
Arnošt Pleskot 2023-08-13 20:00:17 +02:00
parent baa133cbb7
commit 787f5d68cf
No known key found for this signature in database
11 changed files with 172 additions and 75 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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