diff --git a/src/appState.ts b/src/appState.ts
index b51b430e0..a1f8835e6 100644
--- a/src/appState.ts
+++ b/src/appState.ts
@@ -72,7 +72,7 @@ export const getDefaultAppState = (): Omit<
openMenu: null,
openPopup: null,
openSidebar: null,
- openDialog: null,
+ openDialog: "imageExport",
pasteDialog: { shown: false, data: null },
previousSelectedElementIds: {},
resizingElement: null,
@@ -101,7 +101,7 @@ export const getDefaultAppState = (): Omit<
pendingImageElementId: null,
showHyperlinkPopup: false,
selectedLinearElement: null,
- exportBackgroundImage:
+ fancyBackgroundImageUrl:
EXPORT_BACKGROUND_IMAGES[DEFAULT_EXPORT_BACKGROUND_IMAGE].path,
};
};
diff --git a/src/components/ImageExportDialog.tsx b/src/components/ImageExportDialog.tsx
index 10013172b..dc6ced64a 100644
--- a/src/components/ImageExportDialog.tsx
+++ b/src/components/ImageExportDialog.tsx
@@ -138,7 +138,7 @@ const ImageExportModal = ({
}, [
appState,
appState.exportBackground,
- appState.exportBackgroundImage,
+ appState.fancyBackgroundImageUrl,
files,
exportedElements,
]);
@@ -150,7 +150,7 @@ const ImageExportModal = ({
@@ -159,8 +159,8 @@ const ImageExportModal = ({
{t("imageExportDialog.header")}
-
- {!nativeFileSystemSupported && (
+ {!nativeFileSystemSupported && (
+
- )}
-
+
+ )}
{someElementIsSelected && (
`;
@@ -54,6 +55,14 @@ export const exportToCanvas = async (
const onlyExportingSingleFrame = isOnlyExportingSingleFrame(elements);
+ if (appState.fancyBackgroundImageUrl) {
+ await applyFancyBackground(
+ canvas,
+ appState.fancyBackgroundImageUrl,
+ viewBackgroundColor,
+ );
+ }
+
renderStaticScene({
canvas,
rc: rough.canvas(canvas),
diff --git a/src/scene/fancyBackground.ts b/src/scene/fancyBackground.ts
new file mode 100644
index 000000000..44f1c6e6e
--- /dev/null
+++ b/src/scene/fancyBackground.ts
@@ -0,0 +1,147 @@
+import { EXPORT_BG_BORDER_RADIUS, EXPORT_BG_PADDING } from "../constants";
+import { loadHTMLImageElement } from "../element/image";
+import { roundRect } from "../renderer/roundRect";
+import { DataURL } from "../types";
+
+type Dimensions = { w: number; h: number };
+
+const getScaleToFill = (contentSize: Dimensions, containerSize: Dimensions) => {
+ const scale = Math.max(
+ containerSize.w / contentSize.w,
+ containerSize.h / contentSize.h,
+ );
+
+ return scale;
+};
+
+const getScaleToFit = (contentSize: Dimensions, containerSize: Dimensions) => {
+ const scale = Math.min(
+ containerSize.w / contentSize.w,
+ containerSize.h / contentSize.h,
+ );
+
+ return scale;
+};
+
+const addImageBackground = (
+ context: CanvasRenderingContext2D,
+ canvasWidth: number,
+ canvasHeight: number,
+ fancyBackgroundImage: HTMLImageElement,
+) => {
+ context.save();
+ context.beginPath();
+ if (context.roundRect) {
+ context.roundRect(0, 0, canvasWidth, canvasHeight, EXPORT_BG_BORDER_RADIUS);
+ } else {
+ roundRect(
+ context,
+ 0,
+ 0,
+ canvasWidth,
+ canvasHeight,
+ EXPORT_BG_BORDER_RADIUS,
+ );
+ }
+ const scale = getScaleToFill(
+ { w: fancyBackgroundImage.width, h: fancyBackgroundImage.height },
+ { w: canvasWidth, h: canvasHeight },
+ );
+ const x = (canvasWidth - fancyBackgroundImage.width * scale) / 2;
+ const y = (canvasHeight - fancyBackgroundImage.height * scale) / 2;
+ context.clip();
+ context.drawImage(
+ fancyBackgroundImage,
+ x,
+ y,
+ fancyBackgroundImage.width * scale,
+ fancyBackgroundImage.height * scale,
+ );
+ context.closePath();
+ context.restore();
+};
+
+const addContentBackground = (
+ context: CanvasRenderingContext2D,
+ canvasWidth: number,
+ canvasHeight: number,
+ contentBackgroundColor: string,
+) => {
+ 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 => {
+ context.save();
+ context.beginPath();
+ context.shadowColor = `rgba(0, 0, 0, ${shadow.alpha})`;
+ context.shadowBlur = shadow.blur;
+ context.shadowOffsetX = shadow.offsetX;
+ context.shadowOffsetY = shadow.offsetY;
+
+ if (context.roundRect) {
+ context.roundRect(
+ EXPORT_BG_PADDING,
+ EXPORT_BG_PADDING,
+ canvasWidth - EXPORT_BG_PADDING * 2,
+ canvasHeight - EXPORT_BG_PADDING * 2,
+ EXPORT_BG_BORDER_RADIUS,
+ );
+ } else {
+ roundRect(
+ context,
+ EXPORT_BG_PADDING,
+ EXPORT_BG_PADDING,
+ canvasWidth - EXPORT_BG_PADDING * 2,
+ canvasHeight - EXPORT_BG_PADDING * 2,
+ EXPORT_BG_BORDER_RADIUS,
+ );
+ }
+
+ if (index === shadows.length - 1) {
+ context.fillStyle = contentBackgroundColor;
+ context.fill();
+ }
+ context.closePath();
+ context.restore();
+ });
+};
+
+export const applyFancyBackground = async (
+ canvas: HTMLCanvasElement,
+ fancyBackgroundImageUrl: DataURL,
+ backgroundColor: string,
+) => {
+ const context = canvas.getContext("2d")!;
+
+ const fancyBackgroundImage = await loadHTMLImageElement(
+ fancyBackgroundImageUrl,
+ );
+
+ addImageBackground(
+ context,
+ canvas.width,
+ canvas.height,
+ fancyBackgroundImage,
+ );
+
+ addContentBackground(context, canvas.width, canvas.height, backgroundColor);
+};
diff --git a/src/types.ts b/src/types.ts
index 4b58f1837..f10e6b923 100644
--- a/src/types.ts
+++ b/src/types.ts
@@ -287,7 +287,7 @@ export type AppState = {
pendingImageElementId: ExcalidrawImageElement["id"] | null;
showHyperlinkPopup: false | "info" | "editor";
selectedLinearElement: LinearElementEditor | null;
- exportBackgroundImage: string | null;
+ fancyBackgroundImageUrl: DataURL | null;
};
export type UIAppState = Omit<