feat: export with solid color only
This commit is contained in:
parent
e6ae8177ab
commit
a7d1bd1411
@ -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;
|
||||||
|
@ -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) => {
|
||||||
setExportBackgroundImage(value);
|
if (isBackgroundImageKey(value)) {
|
||||||
actionManager.executeAction(
|
setExportBackgroundImage(value);
|
||||||
actionChangeExportBackgroundImage,
|
actionManager.executeAction(
|
||||||
"ui",
|
actionChangeExportBackgroundImage,
|
||||||
value,
|
"ui",
|
||||||
);
|
EXPORT_BACKGROUND_IMAGES[value].path,
|
||||||
|
);
|
||||||
|
}
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<Switch
|
<Switch
|
||||||
|
@ -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>
|
||||||
|
@ -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];
|
|
||||||
|
@ -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) => {
|
||||||
|
Loading…
x
Reference in New Issue
Block a user