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<