feat: select the export bcg image and bcg color from appState

This commit is contained in:
Arnošt Pleskot 2023-07-28 23:45:01 +02:00
parent f15417f864
commit f57cd7e2d5
No known key found for this signature in database
20 changed files with 491 additions and 97 deletions

View file

@ -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" },

View file

@ -123,7 +123,8 @@ export type ActionName =
| "setFrameAsActiveTool"
| "setEmbeddableAsActiveTool"
| "createContainerFromText"
| "wrapTextInContainer";
| "wrapTextInContainer"
| "changeExportBackgroundImage";
export type PanelComponentProps = {
elements: readonly ExcalidrawElement[];

View file

@ -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 = <

View file

@ -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}

View 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
View 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;

View file

@ -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,
);

View file

@ -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];

View file

@ -375,6 +375,7 @@
"header": "Export image",
"label": {
"withBackground": "Background",
"backgroundImage": "Background image",
"onlySelected": "Only selected",
"darkMode": "Dark mode",
"embedScene": "Embed scene",

View file

@ -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,

View file

@ -73,6 +73,7 @@ export const exportToCanvas = async (
imageCache,
renderGrid: false,
isExporting: true,
exportBackgroundImage: appState.exportBackgroundImage,
},
});

View file

@ -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
// ---------------------------------------------------------------------------

View file

@ -287,6 +287,7 @@ export type AppState = {
pendingImageElementId: ExcalidrawImageElement["id"] | null;
showHyperlinkPopup: false | "info" | "editor";
selectedLinearElement: LinearElementEditor | null;
exportBackgroundImage: string;
};
export type UIAppState = Omit<