From a7d1bd141137909db6d83b696b88de6c2986a558 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Arno=C5=A1t=20Pleskot?= Date: Mon, 7 Aug 2023 13:05:55 +0200 Subject: [PATCH] feat: export with solid color only --- src/components/ImageExportDialog.scss | 12 ++++ src/components/ImageExportDialog.tsx | 44 ++++++++++---- src/components/Select.tsx | 4 +- src/constants.ts | 18 +++--- src/renderer/renderScene.ts | 82 ++++++++++++++++++++------- 5 files changed, 120 insertions(+), 40 deletions(-) diff --git a/src/components/ImageExportDialog.scss b/src/components/ImageExportDialog.scss index bb64fb1e6..d9ccd68b9 100644 --- a/src/components/ImageExportDialog.scss +++ b/src/components/ImageExportDialog.scss @@ -87,6 +87,18 @@ } } + &--img-bcg { + padding: 0; + background: none; + border: none; + border-radius: 0; + + & > canvas { + max-width: calc(100%); + max-height: calc(100%); + } + } + @include isMobile { margin-top: 24px; max-width: unset; diff --git a/src/components/ImageExportDialog.tsx b/src/components/ImageExportDialog.tsx index 0ae98727b..52a30a0f7 100644 --- a/src/components/ImageExportDialog.tsx +++ b/src/components/ImageExportDialog.tsx @@ -1,4 +1,5 @@ import React, { useEffect, useRef, useState } from "react"; +import clsx from "clsx"; import type { ActionManager } from "../actions/manager"; import type { AppClassProperties, BinaryFiles, UIAppState } from "../types"; @@ -62,6 +63,12 @@ type ImageExportModalProps = { onExportImage: AppClassProperties["onExportImage"]; }; +function isBackgroundImageKey( + key: string, +): key is keyof typeof EXPORT_BACKGROUND_IMAGES { + return key in EXPORT_BACKGROUND_IMAGES; +} + const ImageExportModal = ({ appState, elements, @@ -78,9 +85,9 @@ const ImageExportModal = ({ const [exportWithBackground, setExportWithBackground] = useState( appState.exportBackground, ); - const [exportBackgroundImage, setExportBackgroundImage] = useState( - DEFAULT_EXPORT_BACKGROUND_IMAGE, - ); + const [exportBackgroundImage, setExportBackgroundImage] = useState< + keyof typeof EXPORT_BACKGROUND_IMAGES + >(DEFAULT_EXPORT_BACKGROUND_IMAGE); const [exportDarkMode, setExportDarkMode] = useState( appState.exportWithDarkMode, @@ -105,6 +112,7 @@ const ImageExportModal = ({ } const maxWidth = previewNode.offsetWidth; const maxHeight = previewNode.offsetHeight; + if (!maxWidth) { return; } @@ -127,13 +135,25 @@ const ImageExportModal = ({ console.error(error); setRenderError(error); }); - }, [appState, files, exportedElements]); + }, [ + appState, + appState.exportBackground, + appState.exportBackgroundImage, + files, + exportedElements, + ]); return (

{t("imageExportDialog.header")}

-
+
{renderError && }
@@ -183,12 +203,14 @@ const ImageExportModal = ({ placeholder={t("imageExportDialog.label.backgroundImage")} value={exportBackgroundImage} onChange={(value) => { - setExportBackgroundImage(value); - actionManager.executeAction( - actionChangeExportBackgroundImage, - "ui", - value, - ); + if (isBackgroundImageKey(value)) { + setExportBackgroundImage(value); + actionManager.executeAction( + actionChangeExportBackgroundImage, + "ui", + EXPORT_BACKGROUND_IMAGES[value].path, + ); + } }} /> ; +type SelectItems = Record; export type SelectProps = { items: SelectItems; @@ -43,7 +43,7 @@ const Select = ({ {Object.entries(items).map(([itemValue, itemLabel]) => ( - {itemLabel} + {itemLabel.label} ))} diff --git a/src/constants.ts b/src/constants.ts index 57d43156e..ade72f9eb 100644 --- a/src/constants.ts +++ b/src/constants.ts @@ -225,6 +225,8 @@ export const MAX_DECIMALS_FOR_SVG_EXPORT = 2; export const EXPORT_SCALES = [1, 2, 3]; export const DEFAULT_EXPORT_PADDING = 10; // px +export const EXPORT_BG_PADDING = 24; // px +export const EXPORT_BG_BORDER_RADIUS = 12; // px export const DEFAULT_MAX_IMAGE_WIDTH_OR_HEIGHT = 1440; @@ -317,13 +319,13 @@ export const DEFAULT_SIDEBAR = { 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", + solid: { path: null, label: "solid color" }, + bubbles: { path: "/backgrounds/bubbles.svg", label: "bubbles" }, + bubbles2: { path: "/backgrounds/bubbles2.svg", label: "bubbles 2" }, + bricks: { path: "/backgrounds/bricks.svg", label: "bricks" }, + lines: { path: "/backgrounds/lines.svg", label: "lines" }, + lines2: { path: "/backgrounds/lines2.svg", label: "lines 2" }, } as const; -export const DEFAULT_EXPORT_BACKGROUND_IMAGE = Object.keys( - EXPORT_BACKGROUND_IMAGES, -)[0]; +export const DEFAULT_EXPORT_BACKGROUND_IMAGE: keyof typeof EXPORT_BACKGROUND_IMAGES = + "solid" as const; diff --git a/src/renderer/renderScene.ts b/src/renderer/renderScene.ts index 4e85f9b76..fa3f3ccb6 100644 --- a/src/renderer/renderScene.ts +++ b/src/renderer/renderScene.ts @@ -57,7 +57,12 @@ import { isOnlyExportingSingleFrame, } from "../utils"; import { UserIdleState } from "../types"; -import { FRAME_STYLE, THEME_FILTER } from "../constants"; +import { + EXPORT_BG_BORDER_RADIUS, + EXPORT_BG_PADDING, + FRAME_STYLE, + THEME_FILTER, +} from "../constants"; import { EXTERNAL_LINK_IMG, getLinkHandleFromCoords, @@ -385,6 +390,26 @@ const frameClip = ( ); }; +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 addExportBackground = ( context: CanvasRenderingContext2D, normalizedCanvasWidth: number, @@ -393,9 +418,6 @@ const addExportBackground = ( rectangleColor: string, ): Promise => { return new Promise((resolve, reject) => { - const MARGIN = 24; - const BORDER_RADIUS = 12; - // Create a new image object const img = new Image(); @@ -410,7 +432,7 @@ const addExportBackground = ( 0, normalizedCanvasWidth, normalizedCanvasHeight, - BORDER_RADIUS, + EXPORT_BG_BORDER_RADIUS, ); } else { roundRect( @@ -419,12 +441,12 @@ const addExportBackground = ( 0, normalizedCanvasWidth, normalizedCanvasHeight, - BORDER_RADIUS, + EXPORT_BG_BORDER_RADIUS, ); } - const scale = Math.max( - normalizedCanvasWidth / img.width, - normalizedCanvasHeight / img.height, + const scale = getScaleToFill( + { w: img.width, h: img.height }, + { w: normalizedCanvasWidth, h: normalizedCanvasHeight }, ); const x = (normalizedCanvasWidth - img.width * scale) / 2; const y = (normalizedCanvasHeight - img.height * scale) / 2; @@ -466,20 +488,20 @@ const addExportBackground = ( if (context.roundRect) { context.roundRect( - MARGIN, - MARGIN, - normalizedCanvasWidth - MARGIN * 2, - normalizedCanvasHeight - MARGIN * 2, - BORDER_RADIUS, + EXPORT_BG_PADDING, + EXPORT_BG_PADDING, + normalizedCanvasWidth - EXPORT_BG_PADDING * 2, + normalizedCanvasHeight - EXPORT_BG_PADDING * 2, + EXPORT_BG_BORDER_RADIUS, ); } else { roundRect( context, - MARGIN, - MARGIN, - normalizedCanvasWidth - MARGIN * 2, - normalizedCanvasHeight - MARGIN * 2, - BORDER_RADIUS, + EXPORT_BG_PADDING, + EXPORT_BG_PADDING, + normalizedCanvasWidth - EXPORT_BG_PADDING * 2, + normalizedCanvasHeight - EXPORT_BG_PADDING * 2, + EXPORT_BG_BORDER_RADIUS, ); } @@ -645,6 +667,28 @@ export const _renderScene = ({ ), ); + if (isExporting && exportBackgroundImage) { + context.save(); + + const contentAreaSize: Dimensions = { + w: canvas.width - (EXPORT_BG_PADDING + EXPORT_BG_BORDER_RADIUS) * 2, + h: canvas.height - (EXPORT_BG_PADDING + EXPORT_BG_BORDER_RADIUS) * 2, + }; + + const scale = getScaleToFit( + { + w: canvas.width, + h: canvas.height, + }, + contentAreaSize, + ); + context.translate( + EXPORT_BG_PADDING + EXPORT_BG_BORDER_RADIUS, + EXPORT_BG_PADDING + EXPORT_BG_BORDER_RADIUS, + ); + context.scale(scale, scale); + } + const groupsToBeAddedToFrame = new Set(); visibleElements.forEach((element) => {