feat: separate fancyBackground from renderScene
This commit is contained in:
parent
a005c789c1
commit
a1a31b4371
@ -72,7 +72,7 @@ export const getDefaultAppState = (): Omit<
|
|||||||
openMenu: null,
|
openMenu: null,
|
||||||
openPopup: null,
|
openPopup: null,
|
||||||
openSidebar: null,
|
openSidebar: null,
|
||||||
openDialog: null,
|
openDialog: "imageExport",
|
||||||
pasteDialog: { shown: false, data: null },
|
pasteDialog: { shown: false, data: null },
|
||||||
previousSelectedElementIds: {},
|
previousSelectedElementIds: {},
|
||||||
resizingElement: null,
|
resizingElement: null,
|
||||||
@ -101,7 +101,7 @@ export const getDefaultAppState = (): Omit<
|
|||||||
pendingImageElementId: null,
|
pendingImageElementId: null,
|
||||||
showHyperlinkPopup: false,
|
showHyperlinkPopup: false,
|
||||||
selectedLinearElement: null,
|
selectedLinearElement: null,
|
||||||
exportBackgroundImage:
|
fancyBackgroundImageUrl:
|
||||||
EXPORT_BACKGROUND_IMAGES[DEFAULT_EXPORT_BACKGROUND_IMAGE].path,
|
EXPORT_BACKGROUND_IMAGES[DEFAULT_EXPORT_BACKGROUND_IMAGE].path,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
@ -138,7 +138,7 @@ const ImageExportModal = ({
|
|||||||
}, [
|
}, [
|
||||||
appState,
|
appState,
|
||||||
appState.exportBackground,
|
appState.exportBackground,
|
||||||
appState.exportBackgroundImage,
|
appState.fancyBackgroundImageUrl,
|
||||||
files,
|
files,
|
||||||
exportedElements,
|
exportedElements,
|
||||||
]);
|
]);
|
||||||
@ -150,7 +150,7 @@ const ImageExportModal = ({
|
|||||||
<div
|
<div
|
||||||
className={clsx("ImageExportModal__preview__canvas", {
|
className={clsx("ImageExportModal__preview__canvas", {
|
||||||
"ImageExportModal__preview__canvas--img-bcg":
|
"ImageExportModal__preview__canvas--img-bcg":
|
||||||
appState.exportBackground && appState.exportBackgroundImage,
|
appState.exportBackground && appState.fancyBackgroundImageUrl,
|
||||||
})}
|
})}
|
||||||
ref={previewRef}
|
ref={previewRef}
|
||||||
>
|
>
|
||||||
@ -159,8 +159,8 @@ const ImageExportModal = ({
|
|||||||
</div>
|
</div>
|
||||||
<div className="ImageExportModal__settings">
|
<div className="ImageExportModal__settings">
|
||||||
<h3>{t("imageExportDialog.header")}</h3>
|
<h3>{t("imageExportDialog.header")}</h3>
|
||||||
<div className="ImageExportModal__settings__filename">
|
{!nativeFileSystemSupported && (
|
||||||
{!nativeFileSystemSupported && (
|
<div className="ImageExportModal__settings__filename">
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
className="TextInput"
|
className="TextInput"
|
||||||
@ -177,8 +177,8 @@ const ImageExportModal = ({
|
|||||||
);
|
);
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
)}
|
</div>
|
||||||
</div>
|
)}
|
||||||
{someElementIsSelected && (
|
{someElementIsSelected && (
|
||||||
<ExportSetting
|
<ExportSetting
|
||||||
label={t("imageExportDialog.label.onlySelected")}
|
label={t("imageExportDialog.label.onlySelected")}
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import cssVariables from "./css/variables.module.scss";
|
import cssVariables from "./css/variables.module.scss";
|
||||||
import { AppProps } from "./types";
|
import { AppProps, DataURL } from "./types";
|
||||||
import { ExcalidrawElement, FontFamilyValues } from "./element/types";
|
import { ExcalidrawElement, FontFamilyValues } from "./element/types";
|
||||||
import { COLOR_PALETTE } from "./colors";
|
import { COLOR_PALETTE } from "./colors";
|
||||||
|
|
||||||
@ -320,11 +320,14 @@ export const LIBRARY_DISABLED_TYPES = new Set(["embeddable", "image"] as const);
|
|||||||
|
|
||||||
export const EXPORT_BACKGROUND_IMAGES = {
|
export const EXPORT_BACKGROUND_IMAGES = {
|
||||||
solid: { path: null, label: "solid color" },
|
solid: { path: null, label: "solid color" },
|
||||||
bubbles: { path: "/backgrounds/bubbles.svg", label: "bubbles" },
|
bubbles: { path: "/backgrounds/bubbles.svg" as DataURL, label: "bubbles" },
|
||||||
bubbles2: { path: "/backgrounds/bubbles2.svg", label: "bubbles 2" },
|
bubbles2: {
|
||||||
bricks: { path: "/backgrounds/bricks.svg", label: "bricks" },
|
path: "/backgrounds/bubbles2.svg" as DataURL,
|
||||||
lines: { path: "/backgrounds/lines.svg", label: "lines" },
|
label: "bubbles 2",
|
||||||
lines2: { path: "/backgrounds/lines2.svg", label: "lines 2" },
|
},
|
||||||
|
bricks: { path: "/backgrounds/bricks.svg" as DataURL, label: "bricks" },
|
||||||
|
lines: { path: "/backgrounds/lines.svg" as DataURL, label: "lines" },
|
||||||
|
lines2: { path: "/backgrounds/lines2.svg" as DataURL, label: "lines 2" },
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
export const DEFAULT_EXPORT_BACKGROUND_IMAGE: keyof typeof EXPORT_BACKGROUND_IMAGES =
|
export const DEFAULT_EXPORT_BACKGROUND_IMAGE: keyof typeof EXPORT_BACKGROUND_IMAGES =
|
||||||
|
@ -22,7 +22,7 @@ import {
|
|||||||
} from "../element";
|
} from "../element";
|
||||||
|
|
||||||
import { roundRect } from "./roundRect";
|
import { roundRect } from "./roundRect";
|
||||||
import { RenderConfig, ScrollBars } from "../scene/types";
|
import { RenderConfig } from "../scene/types";
|
||||||
import {
|
import {
|
||||||
getScrollBars,
|
getScrollBars,
|
||||||
SCROLLBAR_COLOR,
|
SCROLLBAR_COLOR,
|
||||||
@ -57,12 +57,7 @@ import {
|
|||||||
isOnlyExportingSingleFrame,
|
isOnlyExportingSingleFrame,
|
||||||
} from "../utils";
|
} from "../utils";
|
||||||
import { UserIdleState } from "../types";
|
import { UserIdleState } from "../types";
|
||||||
import {
|
import { FRAME_STYLE, THEME_FILTER } from "../constants";
|
||||||
EXPORT_BG_BORDER_RADIUS,
|
|
||||||
EXPORT_BG_PADDING,
|
|
||||||
FRAME_STYLE,
|
|
||||||
THEME_FILTER,
|
|
||||||
} from "../constants";
|
|
||||||
import {
|
import {
|
||||||
EXTERNAL_LINK_IMG,
|
EXTERNAL_LINK_IMG,
|
||||||
getLinkHandleFromCoords,
|
getLinkHandleFromCoords,
|
||||||
@ -82,7 +77,6 @@ import {
|
|||||||
isElementInFrame,
|
isElementInFrame,
|
||||||
} from "../frame";
|
} from "../frame";
|
||||||
import "canvas-roundrect-polyfill";
|
import "canvas-roundrect-polyfill";
|
||||||
import { Unpromisify } from "../utility-types";
|
|
||||||
|
|
||||||
export const DEFAULT_SPACING = 2;
|
export const DEFAULT_SPACING = 2;
|
||||||
|
|
||||||
@ -391,194 +385,7 @@ const frameClip = (
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
type Dimensions = { w: number; h: number };
|
export const _renderScene = ({
|
||||||
|
|
||||||
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,
|
|
||||||
normalizedCanvasHeight: number,
|
|
||||||
svgUrl: string,
|
|
||||||
rectangleColor: string,
|
|
||||||
): Promise<void> => {
|
|
||||||
return new Promise((resolve, reject) => {
|
|
||||||
// Create a new image object
|
|
||||||
const img = new Image();
|
|
||||||
|
|
||||||
// When the image has loaded
|
|
||||||
img.onload = (): void => {
|
|
||||||
// Scale image to fill canvas and draw it onto the canvas
|
|
||||||
context.save();
|
|
||||||
context.beginPath();
|
|
||||||
if (context.roundRect) {
|
|
||||||
context.roundRect(
|
|
||||||
0,
|
|
||||||
0,
|
|
||||||
normalizedCanvasWidth,
|
|
||||||
normalizedCanvasHeight,
|
|
||||||
EXPORT_BG_BORDER_RADIUS,
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
roundRect(
|
|
||||||
context,
|
|
||||||
0,
|
|
||||||
0,
|
|
||||||
normalizedCanvasWidth,
|
|
||||||
normalizedCanvasHeight,
|
|
||||||
EXPORT_BG_BORDER_RADIUS,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
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;
|
|
||||||
context.clip();
|
|
||||||
context.drawImage(img, x, y, img.width * scale, img.height * scale);
|
|
||||||
context.closePath();
|
|
||||||
context.restore();
|
|
||||||
|
|
||||||
// Create shadow similar to the CSS box-shadow
|
|
||||||
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,
|
|
||||||
normalizedCanvasWidth - EXPORT_BG_PADDING * 2,
|
|
||||||
normalizedCanvasHeight - EXPORT_BG_PADDING * 2,
|
|
||||||
EXPORT_BG_BORDER_RADIUS,
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
roundRect(
|
|
||||||
context,
|
|
||||||
EXPORT_BG_PADDING,
|
|
||||||
EXPORT_BG_PADDING,
|
|
||||||
normalizedCanvasWidth - EXPORT_BG_PADDING * 2,
|
|
||||||
normalizedCanvasHeight - EXPORT_BG_PADDING * 2,
|
|
||||||
EXPORT_BG_BORDER_RADIUS,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (index === shadows.length - 1) {
|
|
||||||
context.fillStyle = rectangleColor;
|
|
||||||
context.fill();
|
|
||||||
}
|
|
||||||
context.closePath();
|
|
||||||
context.restore();
|
|
||||||
});
|
|
||||||
|
|
||||||
// Reset shadow properties for future drawings
|
|
||||||
context.shadowColor = "transparent";
|
|
||||||
context.shadowBlur = 0;
|
|
||||||
context.shadowOffsetX = 0;
|
|
||||||
context.shadowOffsetY = 0;
|
|
||||||
|
|
||||||
resolve();
|
|
||||||
};
|
|
||||||
|
|
||||||
img.onerror = (): void => {
|
|
||||||
reject(new Error(`Failed to load image with URL ${svgUrl}`));
|
|
||||||
};
|
|
||||||
|
|
||||||
// Start loading the image
|
|
||||||
img.src = svgUrl;
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
export const paintBackground = async (
|
|
||||||
context: CanvasRenderingContext2D,
|
|
||||||
normalizedCanvasWidth: number,
|
|
||||||
normalizedCanvasHeight: number,
|
|
||||||
{
|
|
||||||
viewBackgroundColor,
|
|
||||||
isExporting,
|
|
||||||
exportBackgroundImage,
|
|
||||||
}: Pick<
|
|
||||||
RenderConfig,
|
|
||||||
"viewBackgroundColor" | "isExporting" | "exportBackgroundImage"
|
|
||||||
>,
|
|
||||||
): Promise<void> => {
|
|
||||||
if (typeof viewBackgroundColor === "string") {
|
|
||||||
const hasTransparence =
|
|
||||||
viewBackgroundColor === "transparent" ||
|
|
||||||
viewBackgroundColor.length === 5 || // #RGBA
|
|
||||||
viewBackgroundColor.length === 9 || // #RRGGBBA
|
|
||||||
/(hsla|rgba)\(/.test(viewBackgroundColor);
|
|
||||||
if (hasTransparence) {
|
|
||||||
context.clearRect(0, 0, normalizedCanvasWidth, normalizedCanvasHeight);
|
|
||||||
}
|
|
||||||
context.save();
|
|
||||||
if (isExporting && exportBackgroundImage) {
|
|
||||||
try {
|
|
||||||
await addExportBackground(
|
|
||||||
context,
|
|
||||||
normalizedCanvasWidth,
|
|
||||||
normalizedCanvasHeight,
|
|
||||||
exportBackgroundImage,
|
|
||||||
viewBackgroundColor,
|
|
||||||
);
|
|
||||||
} catch (error) {
|
|
||||||
console.error("Failed to add background:", error);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
context.fillStyle = viewBackgroundColor;
|
|
||||||
context.fillRect(0, 0, normalizedCanvasWidth, normalizedCanvasHeight);
|
|
||||||
}
|
|
||||||
|
|
||||||
context.restore();
|
|
||||||
} else {
|
|
||||||
context.clearRect(0, 0, normalizedCanvasWidth, normalizedCanvasHeight);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
export const _renderScene = async ({
|
|
||||||
elements,
|
elements,
|
||||||
appState,
|
appState,
|
||||||
scale,
|
scale,
|
||||||
@ -592,24 +399,22 @@ export const _renderScene = async ({
|
|||||||
rc: RoughCanvas;
|
rc: RoughCanvas;
|
||||||
canvas: HTMLCanvasElement;
|
canvas: HTMLCanvasElement;
|
||||||
renderConfig: RenderConfig;
|
renderConfig: RenderConfig;
|
||||||
}): Promise<{
|
}) =>
|
||||||
atLeastOneVisibleElement: boolean;
|
|
||||||
scrollBars: ScrollBars | undefined;
|
|
||||||
}> =>
|
|
||||||
// extra options passed to the renderer
|
// extra options passed to the renderer
|
||||||
{
|
{
|
||||||
if (canvas === null) {
|
if (canvas === null) {
|
||||||
return { atLeastOneVisibleElement: false, scrollBars: undefined };
|
return { atLeastOneVisibleElement: false };
|
||||||
}
|
}
|
||||||
const {
|
const {
|
||||||
renderScrollbars = false,
|
renderScrollbars = false,
|
||||||
renderSelection = true,
|
renderSelection = true,
|
||||||
renderGrid = true,
|
renderGrid = true,
|
||||||
isExporting,
|
isExporting,
|
||||||
viewBackgroundColor,
|
|
||||||
exportBackgroundImage,
|
|
||||||
} = renderConfig;
|
} = renderConfig;
|
||||||
|
|
||||||
|
const preserveCanvasContent =
|
||||||
|
isExporting && appState.fancyBackgroundImageUrl;
|
||||||
|
|
||||||
const selectionColor = renderConfig.selectionColor || oc.black;
|
const selectionColor = renderConfig.selectionColor || oc.black;
|
||||||
|
|
||||||
const context = canvas.getContext("2d")!;
|
const context = canvas.getContext("2d")!;
|
||||||
@ -625,16 +430,24 @@ export const _renderScene = async ({
|
|||||||
context.filter = THEME_FILTER;
|
context.filter = THEME_FILTER;
|
||||||
}
|
}
|
||||||
|
|
||||||
await paintBackground(
|
// Paint background
|
||||||
context,
|
if (typeof renderConfig.viewBackgroundColor === "string") {
|
||||||
normalizedCanvasWidth,
|
const hasTransparence =
|
||||||
normalizedCanvasHeight,
|
renderConfig.viewBackgroundColor === "transparent" ||
|
||||||
{
|
renderConfig.viewBackgroundColor.length === 5 || // #RGBA
|
||||||
isExporting,
|
renderConfig.viewBackgroundColor.length === 9 || // #RRGGBBA
|
||||||
viewBackgroundColor,
|
/(hsla|rgba)\(/.test(renderConfig.viewBackgroundColor);
|
||||||
exportBackgroundImage,
|
if (hasTransparence && !preserveCanvasContent) {
|
||||||
},
|
context.clearRect(0, 0, normalizedCanvasWidth, normalizedCanvasHeight);
|
||||||
);
|
}
|
||||||
|
context.save();
|
||||||
|
context.fillStyle = renderConfig.viewBackgroundColor;
|
||||||
|
context.fillRect(0, 0, normalizedCanvasWidth, normalizedCanvasHeight);
|
||||||
|
context.restore();
|
||||||
|
} else if (!preserveCanvasContent) {
|
||||||
|
context.clearRect(0, 0, normalizedCanvasWidth, normalizedCanvasHeight);
|
||||||
|
}
|
||||||
|
|
||||||
// Apply zoom
|
// Apply zoom
|
||||||
context.save();
|
context.save();
|
||||||
context.scale(renderConfig.zoom.value, renderConfig.zoom.value);
|
context.scale(renderConfig.zoom.value, renderConfig.zoom.value);
|
||||||
@ -663,28 +476,6 @@ export const _renderScene = async ({
|
|||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
|
||||||
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) => {
|
||||||
@ -1206,31 +997,27 @@ export const _renderScene = async ({
|
|||||||
}
|
}
|
||||||
|
|
||||||
context.restore();
|
context.restore();
|
||||||
|
return { atLeastOneVisibleElement: visibleElements.length > 0, scrollBars };
|
||||||
return {
|
|
||||||
atLeastOneVisibleElement: visibleElements.length > 0,
|
|
||||||
scrollBars,
|
|
||||||
};
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const renderSceneThrottled = throttleRAF(
|
const renderSceneThrottled = throttleRAF(
|
||||||
async (config: {
|
(config: {
|
||||||
elements: readonly NonDeletedExcalidrawElement[];
|
elements: readonly NonDeletedExcalidrawElement[];
|
||||||
appState: AppState;
|
appState: AppState;
|
||||||
scale: number;
|
scale: number;
|
||||||
rc: RoughCanvas;
|
rc: RoughCanvas;
|
||||||
canvas: HTMLCanvasElement;
|
canvas: HTMLCanvasElement;
|
||||||
renderConfig: RenderConfig;
|
renderConfig: RenderConfig;
|
||||||
callback?: (data: Unpromisify<ReturnType<typeof _renderScene>>) => void;
|
callback?: (data: ReturnType<typeof _renderScene>) => void;
|
||||||
}) => {
|
}) => {
|
||||||
const ret = await _renderScene(config);
|
const ret = _renderScene(config);
|
||||||
config.callback?.(ret);
|
config.callback?.(ret);
|
||||||
},
|
},
|
||||||
{ trailing: true },
|
{ trailing: true },
|
||||||
);
|
);
|
||||||
|
|
||||||
/** renderScene throttled to animation framerate */
|
/** renderScene throttled to animation framerate */
|
||||||
export const renderScene = async <T extends boolean = false>(
|
export const renderScene = <T extends boolean = false>(
|
||||||
config: {
|
config: {
|
||||||
elements: readonly NonDeletedExcalidrawElement[];
|
elements: readonly NonDeletedExcalidrawElement[];
|
||||||
appState: AppState;
|
appState: AppState;
|
||||||
@ -1238,25 +1025,19 @@ export const renderScene = async <T extends boolean = false>(
|
|||||||
rc: RoughCanvas;
|
rc: RoughCanvas;
|
||||||
canvas: HTMLCanvasElement;
|
canvas: HTMLCanvasElement;
|
||||||
renderConfig: RenderConfig;
|
renderConfig: RenderConfig;
|
||||||
callback?: (data: Unpromisify<ReturnType<typeof _renderScene>>) => void;
|
callback?: (data: ReturnType<typeof _renderScene>) => void;
|
||||||
},
|
},
|
||||||
/** Whether to throttle rendering. Defaults to false.
|
/** Whether to throttle rendering. Defaults to false.
|
||||||
* When throttling, no value is returned. Use the callback instead. */
|
* When throttling, no value is returned. Use the callback instead. */
|
||||||
throttle?: T,
|
throttle?: T,
|
||||||
): Promise<
|
): T extends true ? void : ReturnType<typeof _renderScene> => {
|
||||||
T extends true ? void : Unpromisify<ReturnType<typeof _renderScene>>
|
|
||||||
> => {
|
|
||||||
if (throttle) {
|
if (throttle) {
|
||||||
renderSceneThrottled(config);
|
renderSceneThrottled(config);
|
||||||
return undefined as T extends true
|
return undefined as T extends true ? void : ReturnType<typeof _renderScene>;
|
||||||
? void
|
|
||||||
: Unpromisify<ReturnType<typeof _renderScene>>;
|
|
||||||
}
|
}
|
||||||
const ret = await _renderScene(config);
|
const ret = _renderScene(config);
|
||||||
config.callback?.(ret);
|
config.callback?.(ret);
|
||||||
return ret as T extends true
|
return ret as T extends true ? void : ReturnType<typeof _renderScene>;
|
||||||
? void
|
|
||||||
: Unpromisify<ReturnType<typeof _renderScene>>;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const renderTransformHandles = (
|
const renderTransformHandles = (
|
||||||
|
@ -12,6 +12,7 @@ import {
|
|||||||
updateImageCache,
|
updateImageCache,
|
||||||
} from "../element/image";
|
} from "../element/image";
|
||||||
import Scene from "./Scene";
|
import Scene from "./Scene";
|
||||||
|
import { applyFancyBackground } from "./fancyBackground";
|
||||||
|
|
||||||
export const SVG_EXPORT_TAG = `<!-- svg-source:excalidraw -->`;
|
export const SVG_EXPORT_TAG = `<!-- svg-source:excalidraw -->`;
|
||||||
|
|
||||||
@ -54,14 +55,25 @@ export const exportToCanvas = async (
|
|||||||
|
|
||||||
const onlyExportingSingleFrame = isOnlyExportingSingleFrame(elements);
|
const onlyExportingSingleFrame = isOnlyExportingSingleFrame(elements);
|
||||||
|
|
||||||
await renderScene({
|
if (appState.fancyBackgroundImageUrl) {
|
||||||
|
await applyFancyBackground(
|
||||||
|
canvas,
|
||||||
|
appState.fancyBackgroundImageUrl,
|
||||||
|
viewBackgroundColor,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
renderScene({
|
||||||
elements,
|
elements,
|
||||||
appState,
|
appState,
|
||||||
scale,
|
scale,
|
||||||
rc: rough.canvas(canvas),
|
rc: rough.canvas(canvas),
|
||||||
canvas,
|
canvas,
|
||||||
renderConfig: {
|
renderConfig: {
|
||||||
viewBackgroundColor: exportBackground ? viewBackgroundColor : null,
|
viewBackgroundColor:
|
||||||
|
exportBackground && !appState.fancyBackgroundImageUrl
|
||||||
|
? viewBackgroundColor
|
||||||
|
: null,
|
||||||
scrollX: -minX + (onlyExportingSingleFrame ? 0 : exportPadding),
|
scrollX: -minX + (onlyExportingSingleFrame ? 0 : exportPadding),
|
||||||
scrollY: -minY + (onlyExportingSingleFrame ? 0 : exportPadding),
|
scrollY: -minY + (onlyExportingSingleFrame ? 0 : exportPadding),
|
||||||
zoom: defaultAppState.zoom,
|
zoom: defaultAppState.zoom,
|
||||||
@ -76,7 +88,7 @@ export const exportToCanvas = async (
|
|||||||
renderSelection: false,
|
renderSelection: false,
|
||||||
renderGrid: false,
|
renderGrid: false,
|
||||||
isExporting: true,
|
isExporting: true,
|
||||||
exportBackgroundImage: appState.exportBackgroundImage,
|
exportBackgroundImage: appState.fancyBackgroundImageUrl,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
147
src/scene/fancyBackground.ts
Normal file
147
src/scene/fancyBackground.ts
Normal file
@ -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);
|
||||||
|
};
|
@ -238,7 +238,7 @@ export type AppState = {
|
|||||||
pendingImageElementId: ExcalidrawImageElement["id"] | null;
|
pendingImageElementId: ExcalidrawImageElement["id"] | null;
|
||||||
showHyperlinkPopup: false | "info" | "editor";
|
showHyperlinkPopup: false | "info" | "editor";
|
||||||
selectedLinearElement: LinearElementEditor | null;
|
selectedLinearElement: LinearElementEditor | null;
|
||||||
exportBackgroundImage: string | null;
|
fancyBackgroundImageUrl: DataURL | null;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type UIAppState = Omit<
|
export type UIAppState = Omit<
|
||||||
|
Loading…
x
Reference in New Issue
Block a user