feat: export with solid color only

This commit is contained in:
Arnošt Pleskot 2023-08-07 13:05:55 +02:00
parent e6ae8177ab
commit a7d1bd1411
No known key found for this signature in database
5 changed files with 120 additions and 40 deletions

View File

@ -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 { @include isMobile {
margin-top: 24px; margin-top: 24px;
max-width: unset; max-width: unset;

View File

@ -1,4 +1,5 @@
import React, { useEffect, useRef, useState } from "react"; import React, { useEffect, useRef, useState } from "react";
import clsx from "clsx";
import type { ActionManager } from "../actions/manager"; import type { ActionManager } from "../actions/manager";
import type { AppClassProperties, BinaryFiles, UIAppState } from "../types"; import type { AppClassProperties, BinaryFiles, UIAppState } from "../types";
@ -62,6 +63,12 @@ type ImageExportModalProps = {
onExportImage: AppClassProperties["onExportImage"]; onExportImage: AppClassProperties["onExportImage"];
}; };
function isBackgroundImageKey(
key: string,
): key is keyof typeof EXPORT_BACKGROUND_IMAGES {
return key in EXPORT_BACKGROUND_IMAGES;
}
const ImageExportModal = ({ const ImageExportModal = ({
appState, appState,
elements, elements,
@ -78,9 +85,9 @@ const ImageExportModal = ({
const [exportWithBackground, setExportWithBackground] = useState( const [exportWithBackground, setExportWithBackground] = useState(
appState.exportBackground, appState.exportBackground,
); );
const [exportBackgroundImage, setExportBackgroundImage] = useState<string>( const [exportBackgroundImage, setExportBackgroundImage] = useState<
DEFAULT_EXPORT_BACKGROUND_IMAGE, keyof typeof EXPORT_BACKGROUND_IMAGES
); >(DEFAULT_EXPORT_BACKGROUND_IMAGE);
const [exportDarkMode, setExportDarkMode] = useState( const [exportDarkMode, setExportDarkMode] = useState(
appState.exportWithDarkMode, appState.exportWithDarkMode,
@ -105,6 +112,7 @@ const ImageExportModal = ({
} }
const maxWidth = previewNode.offsetWidth; const maxWidth = previewNode.offsetWidth;
const maxHeight = previewNode.offsetHeight; const maxHeight = previewNode.offsetHeight;
if (!maxWidth) { if (!maxWidth) {
return; return;
} }
@ -127,13 +135,25 @@ const ImageExportModal = ({
console.error(error); console.error(error);
setRenderError(error); setRenderError(error);
}); });
}, [appState, files, exportedElements]); }, [
appState,
appState.exportBackground,
appState.exportBackgroundImage,
files,
exportedElements,
]);
return ( return (
<div className="ImageExportModal"> <div className="ImageExportModal">
<h3>{t("imageExportDialog.header")}</h3> <h3>{t("imageExportDialog.header")}</h3>
<div className="ImageExportModal__preview"> <div className="ImageExportModal__preview">
<div className="ImageExportModal__preview__canvas" ref={previewRef}> <div
className={clsx("ImageExportModal__preview__canvas", {
"ImageExportModal__preview__canvas--img-bcg":
appState.exportBackground && appState.exportBackgroundImage,
})}
ref={previewRef}
>
{renderError && <ErrorCanvasPreview />} {renderError && <ErrorCanvasPreview />}
</div> </div>
</div> </div>
@ -183,12 +203,14 @@ const ImageExportModal = ({
placeholder={t("imageExportDialog.label.backgroundImage")} placeholder={t("imageExportDialog.label.backgroundImage")}
value={exportBackgroundImage} value={exportBackgroundImage}
onChange={(value) => { onChange={(value) => {
if (isBackgroundImageKey(value)) {
setExportBackgroundImage(value); setExportBackgroundImage(value);
actionManager.executeAction( actionManager.executeAction(
actionChangeExportBackgroundImage, actionChangeExportBackgroundImage,
"ui", "ui",
value, EXPORT_BACKGROUND_IMAGES[value].path,
); );
}
}} }}
/> />
<Switch <Switch

View File

@ -4,7 +4,7 @@ import * as RadixSelect from "@radix-ui/react-select";
import "./Select.scss"; import "./Select.scss";
import { tablerChevronDownIcon, tablerChevronUpIcon } from "./icons"; import { tablerChevronDownIcon, tablerChevronUpIcon } from "./icons";
type SelectItems = Record<string, string>; type SelectItems = Record<string, { path: string | null; label: string }>;
export type SelectProps = { export type SelectProps = {
items: SelectItems; items: SelectItems;
@ -43,7 +43,7 @@ const Select = ({
<RadixSelect.Viewport className="Select__viewport"> <RadixSelect.Viewport className="Select__viewport">
{Object.entries(items).map(([itemValue, itemLabel]) => ( {Object.entries(items).map(([itemValue, itemLabel]) => (
<SelectItem value={itemValue} key={itemValue}> <SelectItem value={itemValue} key={itemValue}>
{itemLabel} {itemLabel.label}
</SelectItem> </SelectItem>
))} ))}
</RadixSelect.Viewport> </RadixSelect.Viewport>

View File

@ -225,6 +225,8 @@ export const MAX_DECIMALS_FOR_SVG_EXPORT = 2;
export const EXPORT_SCALES = [1, 2, 3]; export const EXPORT_SCALES = [1, 2, 3];
export const DEFAULT_EXPORT_PADDING = 10; // px 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; 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 LIBRARY_DISABLED_TYPES = new Set(["embeddable", "image"] as const);
export const EXPORT_BACKGROUND_IMAGES = { export const EXPORT_BACKGROUND_IMAGES = {
"/backgrounds/bubbles.svg": "bubbles", solid: { path: null, label: "solid color" },
"/backgrounds/bubbles2.svg": "bubbles 2", bubbles: { path: "/backgrounds/bubbles.svg", label: "bubbles" },
"/backgrounds/bricks.svg": "bricks", bubbles2: { path: "/backgrounds/bubbles2.svg", label: "bubbles 2" },
"/backgrounds/lines.svg": "lines", bricks: { path: "/backgrounds/bricks.svg", label: "bricks" },
"/backgrounds/lines2.svg": "lines 2", lines: { path: "/backgrounds/lines.svg", label: "lines" },
lines2: { path: "/backgrounds/lines2.svg", label: "lines 2" },
} as const; } as const;
export const DEFAULT_EXPORT_BACKGROUND_IMAGE = Object.keys( export const DEFAULT_EXPORT_BACKGROUND_IMAGE: keyof typeof EXPORT_BACKGROUND_IMAGES =
EXPORT_BACKGROUND_IMAGES, "solid" as const;
)[0];

View File

@ -57,7 +57,12 @@ import {
isOnlyExportingSingleFrame, isOnlyExportingSingleFrame,
} from "../utils"; } from "../utils";
import { UserIdleState } from "../types"; 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 { import {
EXTERNAL_LINK_IMG, EXTERNAL_LINK_IMG,
getLinkHandleFromCoords, 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 = ( const addExportBackground = (
context: CanvasRenderingContext2D, context: CanvasRenderingContext2D,
normalizedCanvasWidth: number, normalizedCanvasWidth: number,
@ -393,9 +418,6 @@ const addExportBackground = (
rectangleColor: string, rectangleColor: string,
): Promise<void> => { ): Promise<void> => {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
const MARGIN = 24;
const BORDER_RADIUS = 12;
// Create a new image object // Create a new image object
const img = new Image(); const img = new Image();
@ -410,7 +432,7 @@ const addExportBackground = (
0, 0,
normalizedCanvasWidth, normalizedCanvasWidth,
normalizedCanvasHeight, normalizedCanvasHeight,
BORDER_RADIUS, EXPORT_BG_BORDER_RADIUS,
); );
} else { } else {
roundRect( roundRect(
@ -419,12 +441,12 @@ const addExportBackground = (
0, 0,
normalizedCanvasWidth, normalizedCanvasWidth,
normalizedCanvasHeight, normalizedCanvasHeight,
BORDER_RADIUS, EXPORT_BG_BORDER_RADIUS,
); );
} }
const scale = Math.max( const scale = getScaleToFill(
normalizedCanvasWidth / img.width, { w: img.width, h: img.height },
normalizedCanvasHeight / img.height, { w: normalizedCanvasWidth, h: normalizedCanvasHeight },
); );
const x = (normalizedCanvasWidth - img.width * scale) / 2; const x = (normalizedCanvasWidth - img.width * scale) / 2;
const y = (normalizedCanvasHeight - img.height * scale) / 2; const y = (normalizedCanvasHeight - img.height * scale) / 2;
@ -466,20 +488,20 @@ const addExportBackground = (
if (context.roundRect) { if (context.roundRect) {
context.roundRect( context.roundRect(
MARGIN, EXPORT_BG_PADDING,
MARGIN, EXPORT_BG_PADDING,
normalizedCanvasWidth - MARGIN * 2, normalizedCanvasWidth - EXPORT_BG_PADDING * 2,
normalizedCanvasHeight - MARGIN * 2, normalizedCanvasHeight - EXPORT_BG_PADDING * 2,
BORDER_RADIUS, EXPORT_BG_BORDER_RADIUS,
); );
} else { } else {
roundRect( roundRect(
context, context,
MARGIN, EXPORT_BG_PADDING,
MARGIN, EXPORT_BG_PADDING,
normalizedCanvasWidth - MARGIN * 2, normalizedCanvasWidth - EXPORT_BG_PADDING * 2,
normalizedCanvasHeight - MARGIN * 2, normalizedCanvasHeight - EXPORT_BG_PADDING * 2,
BORDER_RADIUS, 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<string>(); const groupsToBeAddedToFrame = new Set<string>();
visibleElements.forEach((element) => { visibleElements.forEach((element) => {