mirror of
https://github.com/excalidraw/excalidraw.git
synced 2025-05-03 10:00:07 -04:00
feat: select the export bcg image and bcg color from appState
This commit is contained in:
parent
f15417f864
commit
f57cd7e2d5
20 changed files with 491 additions and 97 deletions
|
@ -11,7 +11,13 @@ import { KEYS } from "../keys";
|
|||
import { register } from "./register";
|
||||
import { CheckboxItem } from "../components/CheckboxItem";
|
||||
import { getExportSize } from "../scene/export";
|
||||
import { DEFAULT_EXPORT_PADDING, EXPORT_SCALES, THEME } from "../constants";
|
||||
import {
|
||||
DEFAULT_EXPORT_BACKGROUND_IMAGE,
|
||||
DEFAULT_EXPORT_PADDING,
|
||||
EXPORT_BACKGROUND_IMAGES,
|
||||
EXPORT_SCALES,
|
||||
THEME,
|
||||
} from "../constants";
|
||||
import { getSelectedElements, isSomeElementSelected } from "../scene";
|
||||
import { getNonDeletedElements } from "../element";
|
||||
import { isImageFileHandle } from "../data/blob";
|
||||
|
@ -19,6 +25,7 @@ import { nativeFileSystemSupported } from "../data/filesystem";
|
|||
import { Theme } from "../element/types";
|
||||
|
||||
import "../components/ToolIcon.scss";
|
||||
import Select from "../components/Select";
|
||||
|
||||
export const actionChangeProjectName = register({
|
||||
name: "changeProjectName",
|
||||
|
@ -107,6 +114,28 @@ export const actionChangeExportBackground = register({
|
|||
),
|
||||
});
|
||||
|
||||
export const actionChangeExportBackgroundImage = register({
|
||||
name: "changeExportBackgroundImage",
|
||||
trackEvent: { category: "export", action: "toggleBackgroundImage" },
|
||||
perform: (_elements, appState, value) => {
|
||||
return {
|
||||
appState: { ...appState, exportBackgroundImage: value },
|
||||
commitToHistory: false,
|
||||
};
|
||||
},
|
||||
PanelComponent: ({ updateData }) => (
|
||||
<Select
|
||||
items={EXPORT_BACKGROUND_IMAGES}
|
||||
ariaLabel={t("imageExportDialog.label.backgroundImage")}
|
||||
placeholder={t("imageExportDialog.label.backgroundImage")}
|
||||
value={DEFAULT_EXPORT_BACKGROUND_IMAGE}
|
||||
onChange={(value) => {
|
||||
updateData(value);
|
||||
}}
|
||||
/>
|
||||
),
|
||||
});
|
||||
|
||||
export const actionChangeExportEmbedScene = register({
|
||||
name: "changeExportEmbedScene",
|
||||
trackEvent: { category: "export", action: "embedScene" },
|
||||
|
|
|
@ -123,7 +123,8 @@ export type ActionName =
|
|||
| "setFrameAsActiveTool"
|
||||
| "setEmbeddableAsActiveTool"
|
||||
| "createContainerFromText"
|
||||
| "wrapTextInContainer";
|
||||
| "wrapTextInContainer"
|
||||
| "changeExportBackgroundImage";
|
||||
|
||||
export type PanelComponentProps = {
|
||||
elements: readonly ExcalidrawElement[];
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
import { COLOR_PALETTE } from "./colors";
|
||||
import {
|
||||
DEFAULT_ELEMENT_PROPS,
|
||||
DEFAULT_EXPORT_BACKGROUND_IMAGE,
|
||||
DEFAULT_FONT_FAMILY,
|
||||
DEFAULT_FONT_SIZE,
|
||||
DEFAULT_TEXT_ALIGN,
|
||||
|
@ -99,6 +100,7 @@ export const getDefaultAppState = (): Omit<
|
|||
pendingImageElementId: null,
|
||||
showHyperlinkPopup: false,
|
||||
selectedLinearElement: null,
|
||||
exportBackgroundImage: DEFAULT_EXPORT_BACKGROUND_IMAGE,
|
||||
};
|
||||
};
|
||||
|
||||
|
@ -206,6 +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 },
|
||||
exportBackgroundImage: { browser: true, export: false, server: false },
|
||||
});
|
||||
|
||||
const _clearAppStateForStorage = <
|
||||
|
|
|
@ -9,6 +9,7 @@ import {
|
|||
actionChangeExportEmbedScene,
|
||||
actionChangeExportScale,
|
||||
actionChangeProjectName,
|
||||
actionChangeExportBackgroundImage,
|
||||
} from "../actions/actionExport";
|
||||
import { probablySupportsClipboardBlob } from "../clipboard";
|
||||
import {
|
||||
|
@ -16,6 +17,8 @@ import {
|
|||
EXPORT_IMAGE_TYPES,
|
||||
isFirefox,
|
||||
EXPORT_SCALES,
|
||||
EXPORT_BACKGROUND_IMAGES,
|
||||
DEFAULT_EXPORT_BACKGROUND_IMAGE,
|
||||
} from "../constants";
|
||||
|
||||
import { canvasToBlob } from "../data/blob";
|
||||
|
@ -34,6 +37,7 @@ import { Tooltip } from "./Tooltip";
|
|||
import "./ImageExportDialog.scss";
|
||||
import { useAppProps } from "./App";
|
||||
import { FilledButton } from "./FilledButton";
|
||||
import Select from "./Select";
|
||||
|
||||
const supportsContextFilters =
|
||||
"filter" in document.createElement("canvas").getContext("2d")!;
|
||||
|
@ -74,6 +78,10 @@ const ImageExportModal = ({
|
|||
const [exportWithBackground, setExportWithBackground] = useState(
|
||||
appState.exportBackground,
|
||||
);
|
||||
const [exportBackgroundImage, setExportBackgroundImage] = useState<string>(
|
||||
DEFAULT_EXPORT_BACKGROUND_IMAGE,
|
||||
);
|
||||
|
||||
const [exportDarkMode, setExportDarkMode] = useState(
|
||||
appState.exportWithDarkMode,
|
||||
);
|
||||
|
@ -169,6 +177,20 @@ const ImageExportModal = ({
|
|||
label={t("imageExportDialog.label.withBackground")}
|
||||
name="exportBackgroundSwitch"
|
||||
>
|
||||
<Select
|
||||
items={EXPORT_BACKGROUND_IMAGES}
|
||||
ariaLabel={t("imageExportDialog.label.backgroundImage")}
|
||||
placeholder={t("imageExportDialog.label.backgroundImage")}
|
||||
value={exportBackgroundImage}
|
||||
onChange={(value) => {
|
||||
setExportBackgroundImage(value);
|
||||
actionManager.executeAction(
|
||||
actionChangeExportBackgroundImage,
|
||||
"ui",
|
||||
value,
|
||||
);
|
||||
}}
|
||||
/>
|
||||
<Switch
|
||||
name="exportBackgroundSwitch"
|
||||
checked={exportWithBackground}
|
||||
|
|
69
src/components/Select.scss
Normal file
69
src/components/Select.scss
Normal file
|
@ -0,0 +1,69 @@
|
|||
.Select {
|
||||
&__trigger {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 0 15px;
|
||||
line-height: 1;
|
||||
gap: 5px;
|
||||
background: transparent;
|
||||
border: none;
|
||||
font-weight: 600;
|
||||
font-size: 0.8rem;
|
||||
color: var(--text-primary-color);
|
||||
}
|
||||
|
||||
&__trigger-icon {
|
||||
width: 15px;
|
||||
height: 15px;
|
||||
}
|
||||
|
||||
&__content {
|
||||
overflow: hidden;
|
||||
box-shadow: var(--shadow-island);
|
||||
border-radius: var(--border-radius-md);
|
||||
padding: calc(var(--padding) * var(--space-factor));
|
||||
background-color: var(--input-bg-color);
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
&__viewport {
|
||||
padding: 0.5rem;
|
||||
}
|
||||
|
||||
&__item {
|
||||
font-size: 0.8rem;
|
||||
line-height: 1;
|
||||
border-radius: var(--border-radius-md);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 0.5rem 1rem;
|
||||
position: relative;
|
||||
user-select: none;
|
||||
font-weight: 600;
|
||||
color: var(--text-primary-color);
|
||||
|
||||
&[data-highlighted] {
|
||||
outline: none;
|
||||
background-color: var(--button-hover-bg);
|
||||
}
|
||||
|
||||
&[data-state="checked"] {
|
||||
background-color: var(--color-primary);
|
||||
color: var(--input-bg-color);
|
||||
}
|
||||
|
||||
&[data-highlighted][data-state="checked"] {
|
||||
background-color: var(--color-primary-darker);
|
||||
}
|
||||
}
|
||||
|
||||
&__scroll-button {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: 25px;
|
||||
background-color: white;
|
||||
cursor: default;
|
||||
}
|
||||
}
|
77
src/components/Select.tsx
Normal file
77
src/components/Select.tsx
Normal file
|
@ -0,0 +1,77 @@
|
|||
import React, { forwardRef } from "react";
|
||||
import clxs from "clsx";
|
||||
import * as RadixSelect from "@radix-ui/react-select";
|
||||
import "./Select.scss";
|
||||
import { tablerChevronDownIcon, tablerChevronUpIcon } from "./icons";
|
||||
|
||||
type SelectItems = Record<string, string>;
|
||||
|
||||
export type SelectProps = {
|
||||
items: SelectItems;
|
||||
value: keyof SelectItems;
|
||||
onChange: (value: keyof SelectItems) => void;
|
||||
placeholder?: string;
|
||||
ariaLabel?: string;
|
||||
};
|
||||
|
||||
const Select = ({
|
||||
items,
|
||||
value,
|
||||
onChange,
|
||||
placeholder,
|
||||
ariaLabel,
|
||||
}: SelectProps) => (
|
||||
<RadixSelect.Root value={value} onValueChange={onChange}>
|
||||
<RadixSelect.Trigger
|
||||
className="Select__trigger"
|
||||
aria-label={ariaLabel ?? placeholder}
|
||||
>
|
||||
{placeholder && <RadixSelect.Value placeholder={placeholder} />}
|
||||
<RadixSelect.Icon className="Select__trigger-icon">
|
||||
{tablerChevronDownIcon}
|
||||
</RadixSelect.Icon>
|
||||
</RadixSelect.Trigger>
|
||||
<RadixSelect.Content
|
||||
className="Select__content"
|
||||
position="popper"
|
||||
align="center"
|
||||
>
|
||||
<RadixSelect.ScrollUpButton className="Select__scroll-button">
|
||||
{tablerChevronUpIcon}
|
||||
</RadixSelect.ScrollUpButton>
|
||||
|
||||
<RadixSelect.Viewport className="Select__viewport">
|
||||
{Object.entries(items).map(([itemValue, itemLabel]) => (
|
||||
<SelectItem value={itemValue} key={itemValue}>
|
||||
{itemLabel}
|
||||
</SelectItem>
|
||||
))}
|
||||
</RadixSelect.Viewport>
|
||||
|
||||
<RadixSelect.ScrollDownButton className="Select__scroll-button">
|
||||
{tablerChevronDownIcon}
|
||||
</RadixSelect.ScrollDownButton>
|
||||
</RadixSelect.Content>
|
||||
</RadixSelect.Root>
|
||||
);
|
||||
|
||||
type SelectItemProps = React.ComponentProps<typeof RadixSelect.Item>;
|
||||
|
||||
const SelectItem = forwardRef(
|
||||
(
|
||||
{ children, className, ...props }: SelectItemProps,
|
||||
forwardedRef: React.ForwardedRef<HTMLDivElement>,
|
||||
) => {
|
||||
return (
|
||||
<RadixSelect.Item
|
||||
className={clxs("Select__item", className)}
|
||||
{...props}
|
||||
ref={forwardedRef}
|
||||
>
|
||||
<RadixSelect.ItemText>{children}</RadixSelect.ItemText>
|
||||
</RadixSelect.Item>
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
export default Select;
|
|
@ -1655,3 +1655,19 @@ export const frameToolIcon = createIcon(
|
|||
</g>,
|
||||
tablerIconProps,
|
||||
);
|
||||
|
||||
export const tablerChevronDownIcon = createIcon(
|
||||
<>
|
||||
<path stroke="none" d="M0 0h24v24H0z" fill="none"></path>
|
||||
<path d="M6 9l6 6l6 -6"></path>
|
||||
</>,
|
||||
tablerIconProps,
|
||||
);
|
||||
|
||||
export const tablerChevronUpIcon = createIcon(
|
||||
<>
|
||||
<path stroke="none" d="M0 0h24v24H0z" fill="none"></path>
|
||||
<path d="M6 15l6 -6l6 6"></path>
|
||||
</>,
|
||||
tablerIconProps,
|
||||
);
|
||||
|
|
|
@ -316,3 +316,15 @@ export const DEFAULT_SIDEBAR = {
|
|||
} as const;
|
||||
|
||||
export const LIBRARY_DISABLED_TYPES = new Set(["embeddable", "image"] as const);
|
||||
|
||||
export const EXPORT_BACKGROUND_IMAGES = {
|
||||
"/backgrounds/bubbles.svg": "bubbles",
|
||||
"/backgrounds/bubbles2.svg": "bubbles 2",
|
||||
"/backgrounds/bricks.svg": "bricks",
|
||||
"/backgrounds/lines.svg": "lines",
|
||||
"/backgrounds/lines2.svg": "lines 2",
|
||||
} as const;
|
||||
|
||||
export const DEFAULT_EXPORT_BACKGROUND_IMAGE = Object.keys(
|
||||
EXPORT_BACKGROUND_IMAGES,
|
||||
)[0];
|
||||
|
|
|
@ -375,6 +375,7 @@
|
|||
"header": "Export image",
|
||||
"label": {
|
||||
"withBackground": "Background",
|
||||
"backgroundImage": "Background image",
|
||||
"onlySelected": "Only selected",
|
||||
"darkMode": "Dark mode",
|
||||
"embedScene": "Embed scene",
|
||||
|
|
|
@ -394,100 +394,6 @@ const getNormalizedCanvasDimensions = (
|
|||
return [canvas.width / scale, canvas.height / scale];
|
||||
};
|
||||
|
||||
const addExportBackground = (
|
||||
canvas: HTMLCanvasElement,
|
||||
normalizedCanvasWidth: number,
|
||||
normalizedCanvasHeight: number,
|
||||
svgUrl: string,
|
||||
rectangleColor: string,
|
||||
): void => {
|
||||
const ctx = canvas.getContext("2d") as CanvasRenderingContext2D;
|
||||
|
||||
// Create a new image object
|
||||
const img = new Image();
|
||||
|
||||
// When the image has loaded
|
||||
img.onload = (): void => {
|
||||
// Draw the image onto the canvas
|
||||
ctx.drawImage(img, 0, 0, normalizedCanvasWidth, canvas.height);
|
||||
|
||||
// Create shadow similar to the CSS box-shadow
|
||||
const shadows = [
|
||||
{
|
||||
offsetX: 0,
|
||||
offsetY: 0.7698959708213806,
|
||||
blur: 1.4945039749145508,
|
||||
alpha: 0.02,
|
||||
},
|
||||
{
|
||||
offsetX: 0,
|
||||
offsetY: 1.1299999952316284,
|
||||
blur: 4.1321120262146,
|
||||
alpha: 0.04,
|
||||
},
|
||||
{
|
||||
offsetX: 0,
|
||||
offsetY: 4.130000114440918,
|
||||
blur: 9.94853401184082,
|
||||
alpha: 0.05,
|
||||
},
|
||||
{ offsetX: 0, offsetY: 13, blur: 33, alpha: 0.07 },
|
||||
];
|
||||
|
||||
shadows.forEach((shadow, index): void => {
|
||||
const MARGIN = 24;
|
||||
const BORDER_RADIUS = 12;
|
||||
|
||||
ctx.shadowColor = `rgba(0, 0, 0, ${shadow.alpha})`;
|
||||
ctx.shadowBlur = shadow.blur;
|
||||
ctx.shadowOffsetX = shadow.offsetX;
|
||||
ctx.shadowOffsetY = shadow.offsetY;
|
||||
|
||||
// Define path for rectangle
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(MARGIN, MARGIN);
|
||||
ctx.lineTo(normalizedCanvasWidth - MARGIN, MARGIN);
|
||||
ctx.quadraticCurveTo(
|
||||
normalizedCanvasWidth,
|
||||
MARGIN,
|
||||
normalizedCanvasWidth,
|
||||
MARGIN + BORDER_RADIUS,
|
||||
);
|
||||
ctx.lineTo(normalizedCanvasWidth, normalizedCanvasHeight - MARGIN);
|
||||
ctx.quadraticCurveTo(
|
||||
normalizedCanvasWidth,
|
||||
normalizedCanvasHeight,
|
||||
normalizedCanvasWidth - MARGIN,
|
||||
normalizedCanvasHeight,
|
||||
);
|
||||
ctx.lineTo(MARGIN, normalizedCanvasHeight);
|
||||
ctx.quadraticCurveTo(
|
||||
0,
|
||||
normalizedCanvasHeight,
|
||||
0,
|
||||
normalizedCanvasHeight - MARGIN,
|
||||
);
|
||||
ctx.lineTo(0, MARGIN + BORDER_RADIUS);
|
||||
ctx.quadraticCurveTo(0, MARGIN, MARGIN, MARGIN);
|
||||
ctx.closePath();
|
||||
|
||||
if (index === shadows.length - 1) {
|
||||
ctx.fillStyle = rectangleColor;
|
||||
ctx.fill();
|
||||
}
|
||||
});
|
||||
|
||||
// Reset shadow properties for future drawings
|
||||
ctx.shadowColor = "transparent";
|
||||
ctx.shadowBlur = 0;
|
||||
ctx.shadowOffsetX = 0;
|
||||
ctx.shadowOffsetY = 0;
|
||||
};
|
||||
|
||||
// Start loading the image
|
||||
img.src = svgUrl;
|
||||
};
|
||||
|
||||
const bootstrapCanvas = ({
|
||||
canvas,
|
||||
scale,
|
||||
|
@ -535,7 +441,6 @@ const bootstrapCanvas = ({
|
|||
return context;
|
||||
};
|
||||
|
||||
<<<<<<< HEAD
|
||||
const _renderInteractiveScene = ({
|
||||
canvas,
|
||||
elements,
|
||||
|
|
|
@ -73,6 +73,7 @@ export const exportToCanvas = async (
|
|||
imageCache,
|
||||
renderGrid: false,
|
||||
isExporting: true,
|
||||
exportBackgroundImage: appState.exportBackgroundImage,
|
||||
},
|
||||
});
|
||||
|
||||
|
|
|
@ -28,6 +28,7 @@ export type InteractiveCanvasRenderConfig = {
|
|||
remotePointerUserStates: { [id: string]: string };
|
||||
remotePointerUsernames: { [id: string]: string };
|
||||
remotePointerButton?: { [id: string]: string | undefined };
|
||||
exportBackgroundImage?: string;
|
||||
selectionColor?: string;
|
||||
// extra options passed to the renderer
|
||||
// ---------------------------------------------------------------------------
|
||||
|
|
|
@ -287,6 +287,7 @@ export type AppState = {
|
|||
pendingImageElementId: ExcalidrawImageElement["id"] | null;
|
||||
showHyperlinkPopup: false | "info" | "editor";
|
||||
selectedLinearElement: LinearElementEditor | null;
|
||||
exportBackgroundImage: string;
|
||||
};
|
||||
|
||||
export type UIAppState = Omit<
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue