mirror of
https://github.com/excalidraw/excalidraw.git
synced 2025-09-25 18:31:15 +02:00
Compare commits
5 Commits
dwelle/fix
...
dwelle/fix
Author | SHA1 | Date | |
---|---|---|---|
![]() |
de81123cee | ||
![]() |
a3c20e6663 | ||
![]() |
a218bec343 | ||
![]() |
a6fe2d91a6 | ||
![]() |
f11b81c2e5 |
@@ -7,7 +7,7 @@
|
|||||||
|
|
||||||
<h4 align="center">
|
<h4 align="center">
|
||||||
<a href="https://excalidraw.com">Excalidraw Editor</a> |
|
<a href="https://excalidraw.com">Excalidraw Editor</a> |
|
||||||
<a href="https://blog.excalidraw.com">Blog</a> |
|
<a href="https://plus.excalidraw.com/blog">Blog</a> |
|
||||||
<a href="https://docs.excalidraw.com">Documentation</a> |
|
<a href="https://docs.excalidraw.com">Documentation</a> |
|
||||||
<a href="https://plus.excalidraw.com">Excalidraw+</a>
|
<a href="https://plus.excalidraw.com">Excalidraw+</a>
|
||||||
</h4>
|
</h4>
|
||||||
|
@@ -66,7 +66,7 @@ const config = {
|
|||||||
label: "Docs",
|
label: "Docs",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
to: "https://blog.excalidraw.com",
|
to: "https://plus.excalidraw.com/blog",
|
||||||
label: "Blog",
|
label: "Blog",
|
||||||
position: "left",
|
position: "left",
|
||||||
},
|
},
|
||||||
@@ -111,7 +111,7 @@ const config = {
|
|||||||
items: [
|
items: [
|
||||||
{
|
{
|
||||||
label: "Blog",
|
label: "Blog",
|
||||||
to: "https://blog.excalidraw.com",
|
to: "https://plus.excalidraw.com/blog",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: "GitHub",
|
label: "GitHub",
|
||||||
|
@@ -26,7 +26,6 @@ import {
|
|||||||
StoreAction,
|
StoreAction,
|
||||||
reconcileElements,
|
reconcileElements,
|
||||||
exportToCanvas,
|
exportToCanvas,
|
||||||
exportToSvg,
|
|
||||||
} from "../packages/excalidraw";
|
} from "../packages/excalidraw";
|
||||||
import {
|
import {
|
||||||
exportToBlob,
|
exportToBlob,
|
||||||
@@ -134,8 +133,7 @@ import DebugCanvas, {
|
|||||||
import { AIComponents } from "./components/AI";
|
import { AIComponents } from "./components/AI";
|
||||||
import { ExcalidrawPlusIframeExport } from "./ExcalidrawPlusIframeExport";
|
import { ExcalidrawPlusIframeExport } from "./ExcalidrawPlusIframeExport";
|
||||||
import { fileSave } from "../packages/excalidraw/data/filesystem";
|
import { fileSave } from "../packages/excalidraw/data/filesystem";
|
||||||
import { type ExportSceneConfig } from "../packages/excalidraw/scene/export";
|
import type { ExportToCanvasConfig } from "../packages/excalidraw/scene/export";
|
||||||
import { round } from "../packages/math";
|
|
||||||
|
|
||||||
polyfill();
|
polyfill();
|
||||||
|
|
||||||
@@ -617,13 +615,18 @@ const ExcalidrawWrapper = () => {
|
|||||||
}, [excalidrawAPI]);
|
}, [excalidrawAPI]);
|
||||||
|
|
||||||
const canvasPreviewContainerRef = useRef<HTMLDivElement>(null);
|
const canvasPreviewContainerRef = useRef<HTMLDivElement>(null);
|
||||||
const svgPreviewContainerRef = useRef<HTMLDivElement>(null);
|
|
||||||
|
|
||||||
const [config, setConfig] = useState<ExportSceneConfig>({
|
const [config, setConfig] = useState<ExportToCanvasConfig>(
|
||||||
scale: 1,
|
JSON.parse(localStorage.getItem("_exportConfig") || "null") || {
|
||||||
position: "center",
|
width: 300,
|
||||||
fit: "contain",
|
height: 100,
|
||||||
});
|
padding: 2,
|
||||||
|
scale: 1,
|
||||||
|
position: "none",
|
||||||
|
fit: "contain",
|
||||||
|
canvasBackgroundColor: "yellow",
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
localStorage.setItem("_exportConfig", JSON.stringify(config));
|
localStorage.setItem("_exportConfig", JSON.stringify(config));
|
||||||
@@ -638,83 +641,90 @@ const ExcalidrawWrapper = () => {
|
|||||||
collabAPI.syncElements(elements);
|
collabAPI.syncElements(elements);
|
||||||
}
|
}
|
||||||
|
|
||||||
const nonDeletedElements = getNonDeletedElements(elements);
|
{
|
||||||
|
const frame = elements.find(
|
||||||
|
(el) => el.strokeStyle === "dashed" && !el.isDeleted,
|
||||||
|
);
|
||||||
|
|
||||||
const frame = nonDeletedElements.find(
|
exportToCanvas({
|
||||||
(el) => el.strokeStyle === "dashed" && el.type === "rectangle",
|
data: {
|
||||||
);
|
elements: getNonDeletedElements(elements).filter(
|
||||||
|
(x) => x.id !== frame?.id,
|
||||||
|
),
|
||||||
|
// .concat(
|
||||||
|
// restoreElements(
|
||||||
|
// [
|
||||||
|
// // @ts-ignore
|
||||||
|
// {
|
||||||
|
// type: "rectangle",
|
||||||
|
// width: appState.width / zoom,
|
||||||
|
// height: appState.height / zoom,
|
||||||
|
// x: -appState.scrollX,
|
||||||
|
// y: -appState.scrollY,
|
||||||
|
// fillStyle: "solid",
|
||||||
|
// strokeColor: "transparent",
|
||||||
|
// backgroundColor: "rgba(0,0,0,0.05)",
|
||||||
|
// roundness: { type: ROUNDNESS.ADAPTIVE_RADIUS, value: 40 },
|
||||||
|
// },
|
||||||
|
// ],
|
||||||
|
// null,
|
||||||
|
// ),
|
||||||
|
// ),
|
||||||
|
appState,
|
||||||
|
files,
|
||||||
|
},
|
||||||
|
config: {
|
||||||
|
// // light yellow
|
||||||
|
// // canvasBackgroundColor: "#fff9c4",
|
||||||
|
// // width,
|
||||||
|
// // maxWidthOrHeight: 120,
|
||||||
|
// // scale: 0.01,
|
||||||
|
// // scale: 2,
|
||||||
|
// // origin: "content",
|
||||||
|
// // fit: "cover",
|
||||||
|
// // scale: 2,
|
||||||
|
// // x: 0,
|
||||||
|
// // y: 0,
|
||||||
|
// padding: 20,
|
||||||
|
|
||||||
exportToCanvas({
|
// ...config,
|
||||||
data: {
|
|
||||||
elements: nonDeletedElements.filter((x) => x.id !== frame?.id),
|
|
||||||
// .concat(
|
|
||||||
// restoreElements(
|
|
||||||
// [
|
|
||||||
// // @ts-ignore
|
|
||||||
// {
|
|
||||||
// type: "rectangle",
|
|
||||||
// width: appState.width / zoom,
|
|
||||||
// height: appState.height / zoom,
|
|
||||||
// x: -appState.scrollX,
|
|
||||||
// y: -appState.scrollY,
|
|
||||||
// fillStyle: "solid",
|
|
||||||
// strokeColor: "transparent",
|
|
||||||
// backgroundColor: "rgba(0,0,0,0.05)",
|
|
||||||
// roundness: { type: ROUNDNESS.ADAPTIVE_RADIUS, value: 40 },
|
|
||||||
// },
|
|
||||||
// ],
|
|
||||||
// null,
|
|
||||||
// ),
|
|
||||||
// ),
|
|
||||||
appState,
|
|
||||||
files,
|
|
||||||
},
|
|
||||||
config: {
|
|
||||||
...(frame
|
|
||||||
? {
|
|
||||||
...config,
|
|
||||||
width: frame.width,
|
|
||||||
height: frame.height,
|
|
||||||
x: frame.x,
|
|
||||||
y: frame.y,
|
|
||||||
}
|
|
||||||
: config),
|
|
||||||
},
|
|
||||||
}).then((canvas) => {
|
|
||||||
if (canvasPreviewContainerRef.current) {
|
|
||||||
canvasPreviewContainerRef.current.replaceChildren(canvas);
|
|
||||||
document.querySelector(
|
|
||||||
".canvas_dims",
|
|
||||||
)!.innerHTML = `${canvas.width}x${canvas.height} (canvas)`;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
exportToSvg({
|
// width: config.width,
|
||||||
data: {
|
// height: config.height,
|
||||||
elements: nonDeletedElements.filter((x) => x.id !== frame?.id),
|
// maxWidthOrHeight: config.maxWidthOrHeight,
|
||||||
appState,
|
// widthOrHeight: config.widthOrHeight,
|
||||||
files,
|
// padding: config.padding,
|
||||||
},
|
...(frame
|
||||||
config: {
|
? {
|
||||||
...(frame
|
...config,
|
||||||
? {
|
width: frame.width,
|
||||||
...config,
|
height: frame.height,
|
||||||
width: frame.width,
|
x: frame.x,
|
||||||
height: frame.height,
|
y: frame.y,
|
||||||
x: frame.x,
|
}
|
||||||
y: frame.y,
|
: config),
|
||||||
}
|
// // height: 140,
|
||||||
: config),
|
// // x: -appState.scrollX,
|
||||||
},
|
// // y: -appState.scrollY,
|
||||||
}).then((svg) => {
|
// // height: 150,
|
||||||
if (svgPreviewContainerRef.current) {
|
// // height: appState.height,
|
||||||
svgPreviewContainerRef.current.replaceChildren(svg);
|
// // scale,
|
||||||
document.querySelector(".svg_dims")!.innerHTML = `${round(
|
// // zoom: { value: appState.zoom.value },
|
||||||
parseFloat(svg.getAttribute("width") ?? ""),
|
// // getDimensions(width,height) {
|
||||||
0,
|
// // setCanvasSize({ width, height })
|
||||||
)}x${round(parseFloat(svg.getAttribute("height") ?? ""), 0)} (svg)`;
|
// // return {width: 300, height: 150}
|
||||||
}
|
// // }
|
||||||
});
|
},
|
||||||
|
}).then((canvas) => {
|
||||||
|
if (canvasPreviewContainerRef.current) {
|
||||||
|
canvasPreviewContainerRef.current.replaceChildren(canvas);
|
||||||
|
document.querySelector(
|
||||||
|
".dims",
|
||||||
|
)!.innerHTML = `${canvas.width}x${canvas.height}`;
|
||||||
|
// canvas.style.width = "100%";
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
// this check is redundant, but since this is a hot path, it's best
|
// this check is redundant, but since this is a hot path, it's best
|
||||||
// not to evaludate the nested expression every time
|
// not to evaludate the nested expression every time
|
||||||
@@ -1260,6 +1270,7 @@ const ExcalidrawWrapper = () => {
|
|||||||
>
|
>
|
||||||
<option value="none">none</option>
|
<option value="none">none</option>
|
||||||
<option value="contain">contain</option>
|
<option value="contain">contain</option>
|
||||||
|
<option value="cover">cover</option>
|
||||||
</select>
|
</select>
|
||||||
</label>
|
</label>
|
||||||
<label>
|
<label>
|
||||||
@@ -1420,7 +1431,7 @@ const ExcalidrawWrapper = () => {
|
|||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="canvas_dims">0x0</div>
|
<div className="dims">0x0</div>
|
||||||
<div
|
<div
|
||||||
ref={canvasPreviewContainerRef}
|
ref={canvasPreviewContainerRef}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
@@ -1446,32 +1457,6 @@ const ExcalidrawWrapper = () => {
|
|||||||
backgroundColor: "pink",
|
backgroundColor: "pink",
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<div className="svg_dims">0x0</div>
|
|
||||||
<div
|
|
||||||
ref={svgPreviewContainerRef}
|
|
||||||
onClick={() => {
|
|
||||||
exportToBlob({
|
|
||||||
data: {
|
|
||||||
elements: excalidrawAPI!.getSceneElements(),
|
|
||||||
files: excalidrawAPI?.getFiles() || null,
|
|
||||||
},
|
|
||||||
config,
|
|
||||||
}).then((blob) => {
|
|
||||||
fileSave(blob, {
|
|
||||||
name: "xx",
|
|
||||||
extension: "png",
|
|
||||||
description: "xxx",
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}}
|
|
||||||
style={{
|
|
||||||
borderRadius: 12,
|
|
||||||
border: "1px solid #777",
|
|
||||||
overflow: "hidden",
|
|
||||||
padding: 10,
|
|
||||||
backgroundColor: "pink",
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
@@ -8,7 +8,7 @@ export const EncryptedIcon = () => {
|
|||||||
return (
|
return (
|
||||||
<a
|
<a
|
||||||
className="encrypted-icon tooltip"
|
className="encrypted-icon tooltip"
|
||||||
href="https://blog.excalidraw.com/end-to-end-encryption/"
|
href="https://plus.excalidraw.com/blog/end-to-end-encryption/"
|
||||||
target="_blank"
|
target="_blank"
|
||||||
rel="noopener noreferrer"
|
rel="noopener noreferrer"
|
||||||
aria-label={t("encrypted.link")}
|
aria-label={t("encrypted.link")}
|
||||||
|
@@ -54,6 +54,8 @@
|
|||||||
content="https://excalidraw.com/og-image-3.png"
|
content="https://excalidraw.com/og-image-3.png"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
<link rel="canonical" href="https://excalidraw.com" />
|
||||||
|
|
||||||
<!------------------------------------------------------------------------->
|
<!------------------------------------------------------------------------->
|
||||||
<!-- to minimize white flash on load when user has dark mode enabled -->
|
<!-- to minimize white flash on load when user has dark mode enabled -->
|
||||||
<script>
|
<script>
|
||||||
|
@@ -50,6 +50,9 @@ const ChartPreviewBtn = (props: {
|
|||||||
},
|
},
|
||||||
files: null,
|
files: null,
|
||||||
},
|
},
|
||||||
|
config: {
|
||||||
|
skipInliningFonts: true,
|
||||||
|
},
|
||||||
});
|
});
|
||||||
svg.querySelector(".style-fonts")?.remove();
|
svg.querySelector(".style-fonts")?.remove();
|
||||||
previewNode.replaceChildren();
|
previewNode.replaceChildren();
|
||||||
|
@@ -140,6 +140,9 @@ const SingleLibraryItem = ({
|
|||||||
},
|
},
|
||||||
files: null,
|
files: null,
|
||||||
},
|
},
|
||||||
|
config: {
|
||||||
|
skipInliningFonts: true,
|
||||||
|
},
|
||||||
});
|
});
|
||||||
node.innerHTML = svg.outerHTML;
|
node.innerHTML = svg.outerHTML;
|
||||||
})();
|
})();
|
||||||
|
@@ -309,7 +309,6 @@ 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 DEFAULT_SMALLEST_EXPORT_SIZE = 20; // px
|
|
||||||
|
|
||||||
export const DEFAULT_MAX_IMAGE_WIDTH_OR_HEIGHT = 1440;
|
export const DEFAULT_MAX_IMAGE_WIDTH_OR_HEIGHT = 1440;
|
||||||
|
|
||||||
|
@@ -121,12 +121,13 @@ export const exportAsImage = async ({
|
|||||||
exportBackground: cfg.exportBackground,
|
exportBackground: cfg.exportBackground,
|
||||||
exportWithDarkMode: data.appState.exportWithDarkMode,
|
exportWithDarkMode: data.appState.exportWithDarkMode,
|
||||||
viewBackgroundColor: data.appState.viewBackgroundColor,
|
viewBackgroundColor: data.appState.viewBackgroundColor,
|
||||||
|
exportPadding: cfg.padding,
|
||||||
exportScale: data.appState.exportScale,
|
exportScale: data.appState.exportScale,
|
||||||
exportEmbedScene: data.appState.exportEmbedScene && type === "svg",
|
exportEmbedScene: data.appState.exportEmbedScene && type === "svg",
|
||||||
},
|
},
|
||||||
files: data.files,
|
files: data.files,
|
||||||
},
|
},
|
||||||
config: { exportingFrame: cfg.exportingFrame, padding: cfg.padding },
|
config: { exportingFrame: cfg.exportingFrame },
|
||||||
});
|
});
|
||||||
if (type === "svg") {
|
if (type === "svg") {
|
||||||
return fileSave(
|
return fileSave(
|
||||||
|
@@ -19,6 +19,10 @@ const exportLibraryItemToSvg = async (elements: LibraryItem["elements"]) => {
|
|||||||
},
|
},
|
||||||
files: null,
|
files: null,
|
||||||
},
|
},
|
||||||
|
config: {
|
||||||
|
renderEmbeddables: false,
|
||||||
|
skipInliningFonts: true,
|
||||||
|
},
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@@ -14,6 +14,7 @@ import { arrayToMap, distance, getFontString, toBrandedType } from "../utils";
|
|||||||
import type { AppState, BinaryFiles } from "../types";
|
import type { AppState, BinaryFiles } from "../types";
|
||||||
import {
|
import {
|
||||||
COLOR_WHITE,
|
COLOR_WHITE,
|
||||||
|
DEFAULT_EXPORT_PADDING,
|
||||||
DEFAULT_ZOOM_VALUE,
|
DEFAULT_ZOOM_VALUE,
|
||||||
FRAME_STYLE,
|
FRAME_STYLE,
|
||||||
FONT_FAMILY,
|
FONT_FAMILY,
|
||||||
@@ -21,7 +22,6 @@ import {
|
|||||||
THEME,
|
THEME,
|
||||||
THEME_FILTER,
|
THEME_FILTER,
|
||||||
MIME_TYPES,
|
MIME_TYPES,
|
||||||
DEFAULT_SMALLEST_EXPORT_SIZE,
|
|
||||||
} from "../constants";
|
} from "../constants";
|
||||||
import { getDefaultAppState } from "../appState";
|
import { getDefaultAppState } from "../appState";
|
||||||
import { serializeAsJSON } from "../data/json";
|
import { serializeAsJSON } from "../data/json";
|
||||||
@@ -29,14 +29,14 @@ import {
|
|||||||
getInitializedImageElements,
|
getInitializedImageElements,
|
||||||
updateImageCache,
|
updateImageCache,
|
||||||
} from "../element/image";
|
} from "../element/image";
|
||||||
import { restoreAppState } from "../data/restore";
|
import { restore, restoreAppState } from "../data/restore";
|
||||||
import {
|
import {
|
||||||
getElementsOverlappingFrame,
|
getElementsOverlappingFrame,
|
||||||
getFrameLikeElements,
|
getFrameLikeElements,
|
||||||
getFrameLikeTitle,
|
getFrameLikeTitle,
|
||||||
getRootElements,
|
getRootElements,
|
||||||
} from "../frame";
|
} from "../frame";
|
||||||
import { newTextElement } from "../element";
|
import { getNonDeletedElements, newTextElement } from "../element";
|
||||||
import { type Mutable } from "../utility-types";
|
import { type Mutable } from "../utility-types";
|
||||||
import { newElementWith } from "../element/mutateElement";
|
import { newElementWith } from "../element/mutateElement";
|
||||||
import { isFrameLikeElement } from "../element/typeChecks";
|
import { isFrameLikeElement } from "../element/typeChecks";
|
||||||
@@ -164,13 +164,13 @@ type ExportToCanvasAppState = Partial<
|
|||||||
Omit<AppState, "offsetTop" | "offsetLeft">
|
Omit<AppState, "offsetTop" | "offsetLeft">
|
||||||
>;
|
>;
|
||||||
|
|
||||||
export type ExportSceneData = {
|
export type ExportToCanvasData = {
|
||||||
elements: readonly NonDeletedExcalidrawElement[];
|
elements: readonly NonDeletedExcalidrawElement[];
|
||||||
appState?: ExportToCanvasAppState;
|
appState?: ExportToCanvasAppState;
|
||||||
files: BinaryFiles | null;
|
files: BinaryFiles | null;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type ExportSceneConfig = {
|
export type ExportToCanvasConfig = {
|
||||||
theme?: Theme;
|
theme?: Theme;
|
||||||
/**
|
/**
|
||||||
* Canvas background. Valid values are:
|
* Canvas background. Valid values are:
|
||||||
@@ -197,6 +197,8 @@ export type ExportSceneConfig = {
|
|||||||
* order to maintain the aspect ratio. It is recommended to set `position`
|
* order to maintain the aspect ratio. It is recommended to set `position`
|
||||||
* to `center` when using `fit=contain`.
|
* to `center` when using `fit=contain`.
|
||||||
*
|
*
|
||||||
|
* When `fit` is set to `cover`, padding is disabled (set to 0).
|
||||||
|
*
|
||||||
* When `fit` is set to `none` and either `width` or `height` or
|
* When `fit` is set to `none` and either `width` or `height` or
|
||||||
* `maxWidthOrHeight` is set, padding is simply adding to the bounding box
|
* `maxWidthOrHeight` is set, padding is simply adding to the bounding box
|
||||||
* and the content may overflow the canvas, thus right or bottom padding
|
* and the content may overflow the canvas, thus right or bottom padding
|
||||||
@@ -277,6 +279,8 @@ export type ExportSceneConfig = {
|
|||||||
*
|
*
|
||||||
* - `none` - no scaling.
|
* - `none` - no scaling.
|
||||||
* - `contain` - scale to fit the frame. Includes `padding`.
|
* - `contain` - scale to fit the frame. Includes `padding`.
|
||||||
|
* - `cover` - scale to fill the frame while maintaining aspect ratio. If
|
||||||
|
* content overflows, it will be cropped.
|
||||||
*
|
*
|
||||||
* If `maxWidthOrHeight` or `widthOrHeight` is set, `fit` is ignored.
|
* If `maxWidthOrHeight` or `widthOrHeight` is set, `fit` is ignored.
|
||||||
*
|
*
|
||||||
@@ -284,7 +288,7 @@ export type ExportSceneConfig = {
|
|||||||
* `widthOrHeight` is specified in which case `none` is the default (can be
|
* `widthOrHeight` is specified in which case `none` is the default (can be
|
||||||
* changed). If `x` or `y` are specified, `none` is forced.
|
* changed). If `x` or `y` are specified, `none` is forced.
|
||||||
*/
|
*/
|
||||||
fit?: "none" | "contain";
|
fit?: "none" | "contain" | "cover";
|
||||||
/**
|
/**
|
||||||
* When either `x` or `y` are not specified, indicates how the canvas should
|
* When either `x` or `y` are not specified, indicates how the canvas should
|
||||||
* be aligned on the respective axis.
|
* be aligned on the respective axis.
|
||||||
@@ -335,16 +339,21 @@ export type ExportSceneConfig = {
|
|||||||
loadFonts?: () => Promise<void>;
|
loadFonts?: () => Promise<void>;
|
||||||
};
|
};
|
||||||
|
|
||||||
const configExportDimension = async ({
|
/**
|
||||||
|
* This API is usually used as a precursor to searializing to Blob or PNG,
|
||||||
|
* but can also be used to create a canvas for other purposes.
|
||||||
|
*/
|
||||||
|
export const exportToCanvas = async ({
|
||||||
data,
|
data,
|
||||||
config,
|
config,
|
||||||
}: {
|
}: {
|
||||||
data: ExportSceneData;
|
data: ExportToCanvasData;
|
||||||
config?: ExportSceneConfig;
|
config?: ExportToCanvasConfig;
|
||||||
}) => {
|
}) => {
|
||||||
// clone
|
// clone
|
||||||
const cfg = Object.assign({}, config);
|
const cfg = Object.assign({}, config);
|
||||||
|
|
||||||
|
const { files } = data;
|
||||||
const { exportingFrame } = cfg;
|
const { exportingFrame } = cfg;
|
||||||
|
|
||||||
const elements = data.elements;
|
const elements = data.elements;
|
||||||
@@ -384,6 +393,19 @@ const configExportDimension = async ({
|
|||||||
? "contain"
|
? "contain"
|
||||||
: "none");
|
: "none");
|
||||||
|
|
||||||
|
const containPadding = cfg.fit === "contain";
|
||||||
|
|
||||||
|
if (cfg.x != null || cfg.x != null) {
|
||||||
|
cfg.fit = "none";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (cfg.fit === "cover") {
|
||||||
|
if (cfg.padding && !import.meta.env.PROD) {
|
||||||
|
console.warn("`padding` is ignored when `fit` is set to `cover`");
|
||||||
|
}
|
||||||
|
cfg.padding = 0;
|
||||||
|
}
|
||||||
|
|
||||||
cfg.padding = cfg.padding ?? 0;
|
cfg.padding = cfg.padding ?? 0;
|
||||||
cfg.scale = cfg.scale ?? 1;
|
cfg.scale = cfg.scale ?? 1;
|
||||||
|
|
||||||
@@ -421,7 +443,7 @@ const configExportDimension = async ({
|
|||||||
// make the canvas fit into the frame (e.g. for `cfg.fit` set to `contain`).
|
// make the canvas fit into the frame (e.g. for `cfg.fit` set to `contain`).
|
||||||
// If `cfg.scale` is set, we multiply the resulting canvasScale by it to
|
// If `cfg.scale` is set, we multiply the resulting canvasScale by it to
|
||||||
// scale the output further.
|
// scale the output further.
|
||||||
let exportScale = 1;
|
let canvasScale = 1;
|
||||||
|
|
||||||
const origCanvasSize = getCanvasSize(
|
const origCanvasSize = getCanvasSize(
|
||||||
exportingFrame ? [exportingFrame] : getRootElements(elementsForRender),
|
exportingFrame ? [exportingFrame] : getRootElements(elementsForRender),
|
||||||
@@ -435,47 +457,36 @@ const configExportDimension = async ({
|
|||||||
// variables for target bounding box
|
// variables for target bounding box
|
||||||
let [x, y, width, height] = origCanvasSize;
|
let [x, y, width, height] = origCanvasSize;
|
||||||
|
|
||||||
x = cfg.x ?? x;
|
if (cfg.width != null) {
|
||||||
y = cfg.y ?? y;
|
width = cfg.width;
|
||||||
width = cfg.width ?? width;
|
|
||||||
height = cfg.height ?? height;
|
|
||||||
|
|
||||||
if (cfg.fit === "contain" || cfg.widthOrHeight || cfg.maxWidthOrHeight) {
|
if (cfg.padding && containPadding) {
|
||||||
cfg.padding =
|
width -= cfg.padding * 2;
|
||||||
cfg.padding && cfg.padding > 0
|
|
||||||
? Math.min(
|
|
||||||
cfg.padding,
|
|
||||||
(width - DEFAULT_SMALLEST_EXPORT_SIZE) / 2,
|
|
||||||
(height - DEFAULT_SMALLEST_EXPORT_SIZE) / 2,
|
|
||||||
)
|
|
||||||
: 0;
|
|
||||||
|
|
||||||
if (cfg.getDimensions != null) {
|
|
||||||
const ret = cfg.getDimensions(width, height);
|
|
||||||
|
|
||||||
width = ret.width;
|
|
||||||
height = ret.height;
|
|
||||||
|
|
||||||
cfg.padding = Math.min(
|
|
||||||
cfg.padding,
|
|
||||||
(width - DEFAULT_SMALLEST_EXPORT_SIZE) / 2,
|
|
||||||
(height - DEFAULT_SMALLEST_EXPORT_SIZE) / 2,
|
|
||||||
);
|
|
||||||
} else if (cfg.widthOrHeight != null) {
|
|
||||||
cfg.padding = Math.min(
|
|
||||||
cfg.padding,
|
|
||||||
(cfg.widthOrHeight - DEFAULT_SMALLEST_EXPORT_SIZE) / 2,
|
|
||||||
);
|
|
||||||
} else if (cfg.maxWidthOrHeight != null) {
|
|
||||||
cfg.padding = Math.min(
|
|
||||||
cfg.padding,
|
|
||||||
(cfg.maxWidthOrHeight - DEFAULT_SMALLEST_EXPORT_SIZE) / 2,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (cfg.height) {
|
||||||
|
height = cfg.height;
|
||||||
|
if (cfg.padding && containPadding) {
|
||||||
|
height -= cfg.padding * 2;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// if height not specified, scale the original height to match the new
|
||||||
|
// width while maintaining aspect ratio
|
||||||
|
height *= width / origWidth;
|
||||||
|
}
|
||||||
|
} else if (cfg.height != null) {
|
||||||
|
height = cfg.height;
|
||||||
|
|
||||||
|
if (cfg.padding && containPadding) {
|
||||||
|
height -= cfg.padding * 2;
|
||||||
|
}
|
||||||
|
// width not specified, so scale the original width to match the new
|
||||||
|
// height while maintaining aspect ratio
|
||||||
|
width *= height / origHeight;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (cfg.maxWidthOrHeight != null || cfg.widthOrHeight != null) {
|
if (cfg.maxWidthOrHeight != null || cfg.widthOrHeight != null) {
|
||||||
if (cfg.padding) {
|
if (containPadding && cfg.padding) {
|
||||||
if (cfg.maxWidthOrHeight != null) {
|
if (cfg.maxWidthOrHeight != null) {
|
||||||
cfg.maxWidthOrHeight -= cfg.padding * 2;
|
cfg.maxWidthOrHeight -= cfg.padding * 2;
|
||||||
} else if (cfg.widthOrHeight != null) {
|
} else if (cfg.widthOrHeight != null) {
|
||||||
@@ -487,27 +498,50 @@ const configExportDimension = async ({
|
|||||||
if (cfg.widthOrHeight != null) {
|
if (cfg.widthOrHeight != null) {
|
||||||
// calculate by how much do we need to scale the canvas to fit into the
|
// calculate by how much do we need to scale the canvas to fit into the
|
||||||
// target dimension (e.g. target: max 50px, actual: 70x100px => scale: 0.5)
|
// target dimension (e.g. target: max 50px, actual: 70x100px => scale: 0.5)
|
||||||
exportScale = cfg.widthOrHeight / max;
|
canvasScale = cfg.widthOrHeight / max;
|
||||||
} else if (cfg.maxWidthOrHeight != null) {
|
} else if (cfg.maxWidthOrHeight != null) {
|
||||||
exportScale = cfg.maxWidthOrHeight < max ? cfg.maxWidthOrHeight / max : 1;
|
canvasScale = cfg.maxWidthOrHeight < max ? cfg.maxWidthOrHeight / max : 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
width *= exportScale;
|
width *= canvasScale;
|
||||||
height *= exportScale;
|
height *= canvasScale;
|
||||||
} else if (cfg.getDimensions) {
|
} else if (cfg.getDimensions) {
|
||||||
const ret = cfg.getDimensions(width, height);
|
const ret = cfg.getDimensions(width, height);
|
||||||
|
|
||||||
width = ret.width;
|
width = ret.width;
|
||||||
height = ret.height;
|
height = ret.height;
|
||||||
cfg.scale = ret.scale ?? cfg.scale;
|
cfg.scale = ret.scale ?? cfg.scale;
|
||||||
} else if (cfg.fit === "contain") {
|
} else if (
|
||||||
|
containPadding &&
|
||||||
|
cfg.padding &&
|
||||||
|
cfg.width == null &&
|
||||||
|
cfg.height == null
|
||||||
|
) {
|
||||||
|
const whRatio = width / height;
|
||||||
width -= cfg.padding * 2;
|
width -= cfg.padding * 2;
|
||||||
height -= cfg.padding * 2;
|
height -= (cfg.padding * 2) / whRatio;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
(cfg.fit === "contain" && !cfg.maxWidthOrHeight) ||
|
||||||
|
(containPadding && cfg.padding)
|
||||||
|
) {
|
||||||
|
if (cfg.fit === "contain") {
|
||||||
|
const wRatio = width / origWidth;
|
||||||
|
const hRatio = height / origHeight;
|
||||||
|
// scale the orig canvas to fit in the target frame
|
||||||
|
canvasScale = Math.min(wRatio, hRatio);
|
||||||
|
} else {
|
||||||
|
const wRatio = (width - cfg.padding * 2) / width;
|
||||||
|
const hRatio = (height - cfg.padding * 2) / height;
|
||||||
|
canvasScale = Math.min(wRatio, hRatio);
|
||||||
|
}
|
||||||
|
} else if (cfg.fit === "cover") {
|
||||||
const wRatio = width / origWidth;
|
const wRatio = width / origWidth;
|
||||||
const hRatio = height / origHeight;
|
const hRatio = height / origHeight;
|
||||||
// scale the orig canvas to fit in the target region
|
// scale the orig canvas to fill the the target frame
|
||||||
exportScale = Math.min(wRatio, hRatio);
|
// (opposite of "contain")
|
||||||
|
canvasScale = Math.max(wRatio, hRatio);
|
||||||
}
|
}
|
||||||
|
|
||||||
x = cfg.x ?? origX;
|
x = cfg.x ?? origX;
|
||||||
@@ -529,83 +563,38 @@ const configExportDimension = async ({
|
|||||||
// aspect ratio dimensions.
|
// aspect ratio dimensions.
|
||||||
if (cfg.position === "center") {
|
if (cfg.position === "center") {
|
||||||
x -=
|
x -=
|
||||||
width / exportScale / 2 -
|
width / canvasScale / 2 -
|
||||||
(cfg.x == null ? origWidth : width + cfg.padding * 2) / 2;
|
(cfg.x == null ? origWidth : width + cfg.padding * 2) / 2;
|
||||||
y -=
|
y -=
|
||||||
height / exportScale / 2 -
|
height / canvasScale / 2 -
|
||||||
(cfg.y == null ? origHeight : height + cfg.padding * 2) / 2;
|
(cfg.y == null ? origHeight : height + cfg.padding * 2) / 2;
|
||||||
}
|
}
|
||||||
|
|
||||||
// rescale padding based on current canvasScale factor so that the resulting
|
|
||||||
// padding is kept the same as supplied by user (with the exception of
|
|
||||||
// `cfg.scale` being set, which also scales the padding)
|
|
||||||
const normalizedPadding = cfg.padding / exportScale;
|
|
||||||
|
|
||||||
// scale the whole frame by cfg.scale (on top of whatever canvasScale we
|
|
||||||
// calculated above)
|
|
||||||
exportScale *= cfg.scale;
|
|
||||||
|
|
||||||
width *= cfg.scale;
|
|
||||||
height *= cfg.scale;
|
|
||||||
|
|
||||||
const exportWidth = width + cfg.padding * 2 * cfg.scale;
|
|
||||||
const exportHeight = height + cfg.padding * 2 * cfg.scale;
|
|
||||||
|
|
||||||
return {
|
|
||||||
config: cfg,
|
|
||||||
normalizedPadding,
|
|
||||||
contentWidth: width,
|
|
||||||
contentHeight: height,
|
|
||||||
exportWidth,
|
|
||||||
exportHeight,
|
|
||||||
exportScale,
|
|
||||||
x,
|
|
||||||
y,
|
|
||||||
elementsForRender,
|
|
||||||
appState,
|
|
||||||
frameRendering,
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* This API is usually used as a precursor to searializing to Blob or PNG,
|
|
||||||
* but can also be used to create a canvas for other purposes.
|
|
||||||
*/
|
|
||||||
export const exportToCanvas = async ({
|
|
||||||
data,
|
|
||||||
config,
|
|
||||||
}: {
|
|
||||||
data: ExportSceneData;
|
|
||||||
config?: ExportSceneConfig;
|
|
||||||
}) => {
|
|
||||||
const {
|
|
||||||
config: cfg,
|
|
||||||
normalizedPadding,
|
|
||||||
contentWidth: width,
|
|
||||||
contentHeight: height,
|
|
||||||
exportWidth,
|
|
||||||
exportHeight,
|
|
||||||
exportScale,
|
|
||||||
x,
|
|
||||||
y,
|
|
||||||
elementsForRender,
|
|
||||||
appState,
|
|
||||||
frameRendering,
|
|
||||||
} = await configExportDimension({ data, config });
|
|
||||||
|
|
||||||
const canvas = cfg.createCanvas
|
const canvas = cfg.createCanvas
|
||||||
? cfg.createCanvas()
|
? cfg.createCanvas()
|
||||||
: document.createElement("canvas");
|
: document.createElement("canvas");
|
||||||
|
|
||||||
canvas.width = exportWidth;
|
// rescale padding based on current canvasScale factor so that the resulting
|
||||||
canvas.height = exportHeight;
|
// padding is kept the same as supplied by user (with the exception of
|
||||||
|
// `cfg.scale` being set, which also scales the padding)
|
||||||
|
const normalizedPadding = cfg.padding / canvasScale;
|
||||||
|
|
||||||
|
// scale the whole frame by cfg.scale (on top of whatever canvasScale we
|
||||||
|
// calculated above)
|
||||||
|
canvasScale *= cfg.scale;
|
||||||
|
|
||||||
|
width *= cfg.scale;
|
||||||
|
height *= cfg.scale;
|
||||||
|
|
||||||
|
canvas.width = width + cfg.padding * 2 * cfg.scale;
|
||||||
|
canvas.height = height + cfg.padding * 2 * cfg.scale;
|
||||||
|
|
||||||
const { imageCache } = await updateImageCache({
|
const { imageCache } = await updateImageCache({
|
||||||
imageCache: new Map(),
|
imageCache: new Map(),
|
||||||
fileIds: getInitializedImageElements(elementsForRender).map(
|
fileIds: getInitializedImageElements(elementsForRender).map(
|
||||||
(element) => element.fileId,
|
(element) => element.fileId,
|
||||||
),
|
),
|
||||||
files: data.files || {},
|
files: files || {},
|
||||||
});
|
});
|
||||||
|
|
||||||
renderStaticScene({
|
renderStaticScene({
|
||||||
@@ -615,7 +604,7 @@ export const exportToCanvas = async ({
|
|||||||
arrayToMap(elementsForRender),
|
arrayToMap(elementsForRender),
|
||||||
),
|
),
|
||||||
allElementsMap: toBrandedType<NonDeletedSceneElementsMap>(
|
allElementsMap: toBrandedType<NonDeletedSceneElementsMap>(
|
||||||
arrayToMap(syncInvalidIndices(data.elements)),
|
arrayToMap(syncInvalidIndices(elements)),
|
||||||
),
|
),
|
||||||
visibleElements: elementsForRender,
|
visibleElements: elementsForRender,
|
||||||
appState: {
|
appState: {
|
||||||
@@ -632,7 +621,7 @@ export const exportToCanvas = async ({
|
|||||||
shouldCacheIgnoreZoom: false,
|
shouldCacheIgnoreZoom: false,
|
||||||
theme: cfg.theme || THEME.LIGHT,
|
theme: cfg.theme || THEME.LIGHT,
|
||||||
},
|
},
|
||||||
scale: exportScale,
|
scale: canvasScale,
|
||||||
renderConfig: {
|
renderConfig: {
|
||||||
canvasBackgroundColor:
|
canvasBackgroundColor:
|
||||||
cfg.canvasBackgroundColor === false
|
cfg.canvasBackgroundColor === false
|
||||||
@@ -655,7 +644,7 @@ export const exportToCanvas = async ({
|
|||||||
};
|
};
|
||||||
|
|
||||||
type ExportToSvgConfig = Pick<
|
type ExportToSvgConfig = Pick<
|
||||||
ExportSceneConfig,
|
ExportToCanvasConfig,
|
||||||
"canvasBackgroundColor" | "padding" | "theme" | "exportingFrame"
|
"canvasBackgroundColor" | "padding" | "theme" | "exportingFrame"
|
||||||
> & {
|
> & {
|
||||||
/**
|
/**
|
||||||
@@ -670,75 +659,108 @@ export const exportToSvg = async ({
|
|||||||
data,
|
data,
|
||||||
config,
|
config,
|
||||||
}: {
|
}: {
|
||||||
data: ExportSceneData;
|
data: {
|
||||||
config?: ExportSceneConfig;
|
elements: readonly NonDeletedExcalidrawElement[];
|
||||||
}) => {
|
appState: {
|
||||||
const {
|
exportBackground: boolean;
|
||||||
config: cfg,
|
exportScale?: number;
|
||||||
normalizedPadding,
|
viewBackgroundColor: string;
|
||||||
exportWidth,
|
exportWithDarkMode?: boolean;
|
||||||
exportHeight,
|
exportEmbedScene?: boolean;
|
||||||
exportScale,
|
frameRendering?: AppState["frameRendering"];
|
||||||
x,
|
gridModeEnabled?: boolean;
|
||||||
y,
|
};
|
||||||
elementsForRender,
|
files: BinaryFiles | null;
|
||||||
appState,
|
};
|
||||||
frameRendering,
|
config?: ExportToSvgConfig;
|
||||||
} = await configExportDimension({ data, config });
|
}): Promise<SVGSVGElement> => {
|
||||||
|
// clone
|
||||||
|
const cfg = Object.assign({}, config);
|
||||||
|
|
||||||
const offsetX = -(x - normalizedPadding);
|
cfg.exportingFrame = cfg.exportingFrame ?? null;
|
||||||
const offsetY = -(y - normalizedPadding);
|
|
||||||
|
|
||||||
const { elements } = data;
|
const { elements: restoredElements } = restore(
|
||||||
|
{ ...data, files: data.files || {} },
|
||||||
// initialize SVG root
|
null,
|
||||||
const svgRoot = document.createElementNS(SVG_NS, "svg");
|
null,
|
||||||
svgRoot.setAttribute("version", "1.1");
|
|
||||||
svgRoot.setAttribute("xmlns", SVG_NS);
|
|
||||||
svgRoot.setAttribute(
|
|
||||||
"viewBox",
|
|
||||||
`0 0 ${exportWidth / exportScale} ${exportHeight / exportScale}`,
|
|
||||||
);
|
);
|
||||||
svgRoot.setAttribute("width", `${exportWidth}`);
|
const elements = getNonDeletedElements(restoredElements);
|
||||||
svgRoot.setAttribute("height", `${exportHeight}`);
|
|
||||||
if (cfg.theme === THEME.DARK) {
|
const frameRendering = getFrameRenderingConfig(
|
||||||
svgRoot.setAttribute("filter", THEME_FILTER);
|
cfg?.exportingFrame ?? null,
|
||||||
|
data.appState.frameRendering ?? null,
|
||||||
|
);
|
||||||
|
|
||||||
|
let {
|
||||||
|
exportWithDarkMode = false,
|
||||||
|
viewBackgroundColor,
|
||||||
|
exportScale = 1,
|
||||||
|
exportEmbedScene,
|
||||||
|
} = data.appState;
|
||||||
|
|
||||||
|
let padding = cfg.padding ?? 0;
|
||||||
|
|
||||||
|
const elementsForRender = prepareElementsForRender({
|
||||||
|
elements,
|
||||||
|
exportingFrame: cfg.exportingFrame,
|
||||||
|
exportWithDarkMode,
|
||||||
|
frameRendering,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (cfg.exportingFrame) {
|
||||||
|
padding = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
const fontFaces = cfg.loadFonts
|
|
||||||
? await Fonts.generateFontFaceDeclarations(elements)
|
|
||||||
: [];
|
|
||||||
|
|
||||||
const delimiter = "\n "; // 6 spaces
|
|
||||||
|
|
||||||
let metadata = "";
|
let metadata = "";
|
||||||
|
|
||||||
// we need to serialize the "original" elements before we put them through
|
// we need to serialize the "original" elements before we put them through
|
||||||
// the tempScene hack which duplicates and regenerates ids
|
// the tempScene hack which duplicates and regenerates ids
|
||||||
if (appState.exportEmbedScene) {
|
if (exportEmbedScene) {
|
||||||
try {
|
try {
|
||||||
metadata = (await import("../data/image")).encodeSvgMetadata({
|
metadata = (await import("../data/image")).encodeSvgMetadata({
|
||||||
// when embedding scene, we want to embed the origionally supplied
|
// when embedding scene, we want to embed the origionally supplied
|
||||||
// elements which don't contain the temp frame labels.
|
// elements which don't contain the temp frame labels.
|
||||||
// But it also requires that the exportToSvg is being supplied with
|
// But it also requires that the exportToSvg is being supplied with
|
||||||
// only the elements that we're exporting, and no extra.
|
// only the elements that we're exporting, and no extra.
|
||||||
text: serializeAsJSON(elements, appState, data.files || {}, "local"),
|
text: serializeAsJSON(
|
||||||
|
elements,
|
||||||
|
data.appState,
|
||||||
|
data.files || {},
|
||||||
|
"local",
|
||||||
|
),
|
||||||
});
|
});
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
console.error(error);
|
console.error(error);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let exportContentClipPath = "";
|
let [minX, minY, width, height] = getCanvasSize(
|
||||||
if (cfg.width != null && cfg.height != null) {
|
cfg.exportingFrame
|
||||||
exportContentClipPath = `<clipPath id="content">
|
? [cfg.exportingFrame]
|
||||||
<rect x="${offsetX}" y="${offsetY}" width="${exportWidth}" height="${exportWidth}"></rect>
|
: getRootElements(elementsForRender),
|
||||||
</clipPath>`;
|
);
|
||||||
|
|
||||||
|
width += padding * 2;
|
||||||
|
height += padding * 2;
|
||||||
|
|
||||||
|
// initialize SVG root
|
||||||
|
const svgRoot = document.createElementNS(SVG_NS, "svg");
|
||||||
|
svgRoot.setAttribute("version", "1.1");
|
||||||
|
svgRoot.setAttribute("xmlns", SVG_NS);
|
||||||
|
svgRoot.setAttribute("viewBox", `0 0 ${width} ${height}`);
|
||||||
|
svgRoot.setAttribute("width", `${width * exportScale}`);
|
||||||
|
svgRoot.setAttribute("height", `${height * exportScale}`);
|
||||||
|
if (exportWithDarkMode) {
|
||||||
|
svgRoot.setAttribute("filter", THEME_FILTER);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const offsetX = -minX + padding;
|
||||||
|
const offsetY = -minY + padding;
|
||||||
|
|
||||||
|
const frameElements = getFrameLikeElements(elements);
|
||||||
|
|
||||||
let exportingFrameClipPath = "";
|
let exportingFrameClipPath = "";
|
||||||
const elementsMap = arrayToMap(elements);
|
const elementsMap = arrayToMap(elements);
|
||||||
const frameElements = getFrameLikeElements(elements);
|
|
||||||
for (const frame of frameElements) {
|
for (const frame of frameElements) {
|
||||||
const [x1, y1, x2, y2] = getElementAbsoluteCoords(frame, elementsMap);
|
const [x1, y1, x2, y2] = getElementAbsoluteCoords(frame, elementsMap);
|
||||||
const cx = (x2 - x1) / 2 - (frame.x - x1);
|
const cx = (x2 - x1) / 2 - (frame.x - x1);
|
||||||
@@ -760,33 +782,36 @@ export const exportToSvg = async ({
|
|||||||
</clipPath>`;
|
</clipPath>`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const fontFaces = !cfg?.skipInliningFonts
|
||||||
|
? await Fonts.generateFontFaceDeclarations(elements)
|
||||||
|
: [];
|
||||||
|
|
||||||
|
const delimiter = "\n "; // 6 spaces
|
||||||
|
|
||||||
svgRoot.innerHTML = `
|
svgRoot.innerHTML = `
|
||||||
${SVG_EXPORT_TAG}
|
${SVG_EXPORT_TAG}
|
||||||
${metadata}
|
${metadata}
|
||||||
<defs>
|
<defs>
|
||||||
<style class="style-fonts">${delimiter}${fontFaces.join(delimiter)}</style>
|
<style class="style-fonts">${delimiter}${fontFaces.join(delimiter)}
|
||||||
${exportContentClipPath}
|
</style>
|
||||||
${exportingFrameClipPath}
|
${exportingFrameClipPath}
|
||||||
</defs>
|
</defs>
|
||||||
`;
|
`;
|
||||||
|
|
||||||
// render background rect
|
// render background rect
|
||||||
if (appState.exportBackground && appState.viewBackgroundColor) {
|
if (data.appState.exportBackground && viewBackgroundColor) {
|
||||||
const rect = svgRoot.ownerDocument!.createElementNS(SVG_NS, "rect");
|
const rect = svgRoot.ownerDocument!.createElementNS(SVG_NS, "rect");
|
||||||
rect.setAttribute("x", "0");
|
rect.setAttribute("x", "0");
|
||||||
rect.setAttribute("y", "0");
|
rect.setAttribute("y", "0");
|
||||||
rect.setAttribute("width", `${exportWidth / exportScale}`);
|
rect.setAttribute("width", `${width}`);
|
||||||
rect.setAttribute("height", `${exportHeight / exportScale}`);
|
rect.setAttribute("height", `${height}`);
|
||||||
rect.setAttribute(
|
rect.setAttribute("fill", viewBackgroundColor);
|
||||||
"fill",
|
|
||||||
cfg.canvasBackgroundColor || appState.viewBackgroundColor,
|
|
||||||
);
|
|
||||||
svgRoot.appendChild(rect);
|
svgRoot.appendChild(rect);
|
||||||
}
|
}
|
||||||
|
|
||||||
const rsvg = rough.svg(svgRoot);
|
const rsvg = rough.svg(svgRoot);
|
||||||
|
|
||||||
// const renderEmbeddables = appState.embe ?? false;
|
const renderEmbeddables = cfg.renderEmbeddables ?? false;
|
||||||
|
|
||||||
renderSceneToSvg(
|
renderSceneToSvg(
|
||||||
elementsForRender,
|
elementsForRender,
|
||||||
@@ -798,18 +823,18 @@ export const exportToSvg = async ({
|
|||||||
offsetX,
|
offsetX,
|
||||||
offsetY,
|
offsetY,
|
||||||
isExporting: true,
|
isExporting: true,
|
||||||
exportWithDarkMode: cfg.theme === THEME.DARK,
|
exportWithDarkMode,
|
||||||
renderEmbeddables: false,
|
renderEmbeddables,
|
||||||
frameRendering,
|
frameRendering,
|
||||||
canvasBackgroundColor: appState.viewBackgroundColor,
|
canvasBackgroundColor: viewBackgroundColor,
|
||||||
embedsValidationStatus: false
|
embedsValidationStatus: renderEmbeddables
|
||||||
? new Map(
|
? new Map(
|
||||||
elementsForRender
|
elementsForRender
|
||||||
.filter((element) => isFrameLikeElement(element))
|
.filter((element) => isFrameLikeElement(element))
|
||||||
.map((element) => [element.id, true]),
|
.map((element) => [element.id, true]),
|
||||||
)
|
)
|
||||||
: new Map(),
|
: new Map(),
|
||||||
reuseImages: true,
|
reuseImages: cfg?.reuseImages ?? true,
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -829,7 +854,7 @@ export const getCanvasSize = (
|
|||||||
|
|
||||||
export { MIME_TYPES };
|
export { MIME_TYPES };
|
||||||
|
|
||||||
type ExportToBlobConfig = ExportSceneConfig & {
|
type ExportToBlobConfig = ExportToCanvasConfig & {
|
||||||
mimeType?: string;
|
mimeType?: string;
|
||||||
quality?: number;
|
quality?: number;
|
||||||
};
|
};
|
||||||
@@ -838,7 +863,7 @@ export const exportToBlob = async ({
|
|||||||
data,
|
data,
|
||||||
config,
|
config,
|
||||||
}: {
|
}: {
|
||||||
data: ExportSceneData;
|
data: ExportToCanvasData;
|
||||||
config?: ExportToBlobConfig;
|
config?: ExportToBlobConfig;
|
||||||
}): Promise<Blob> => {
|
}): Promise<Blob> => {
|
||||||
let { mimeType = MIME_TYPES.png, quality } = config || {};
|
let { mimeType = MIME_TYPES.png, quality } = config || {};
|
||||||
@@ -903,7 +928,7 @@ export const exportToClipboard = async ({
|
|||||||
data,
|
data,
|
||||||
config,
|
config,
|
||||||
}: {
|
}: {
|
||||||
data: ExportSceneData;
|
data: ExportToCanvasData;
|
||||||
} & (
|
} & (
|
||||||
| { type: "png"; config?: ExportToBlobConfig }
|
| { type: "png"; config?: ExportToBlobConfig }
|
||||||
| { type: "svg"; config?: ExportToSvgConfig }
|
| { type: "svg"; config?: ExportToSvgConfig }
|
||||||
|
@@ -6,7 +6,7 @@ exports[`Test <MermaidToExcalidraw/> > should open mermaid popup when active too
|
|||||||
B --> C{Let me think}
|
B --> C{Let me think}
|
||||||
C -->|One| D[Laptop]
|
C -->|One| D[Laptop]
|
||||||
C -->|Two| E[iPhone]
|
C -->|Two| E[iPhone]
|
||||||
C -->|Three| F[Car]</textarea><div class="ttd-dialog-panel-button-container invisible" style="display: flex; align-items: center;"><button type="button" class="excalidraw-button ttd-dialog-panel-button"><div class=""></div></button></div></div><div class="ttd-dialog-panel"><div class="ttd-dialog-panel__header"><label>Preview</label></div><div class="ttd-dialog-output-wrapper"><div style="opacity: 1;" class="ttd-dialog-output-canvas-container"><canvas width="300" height="0" dir="ltr"></canvas></div></div><div class="ttd-dialog-panel-button-container" style="display: flex; align-items: center;"><button type="button" class="excalidraw-button ttd-dialog-panel-button"><div class="">Insert<span><svg aria-hidden="true" focusable="false" role="img" viewBox="0 0 20 20" class="" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round"><g stroke-width="1.25"><path d="M4.16602 10H15.8327"></path><path d="M12.5 13.3333L15.8333 10"></path><path d="M12.5 6.66666L15.8333 9.99999"></path></g></svg></span></div></button><div class="ttd-dialog-submit-shortcut"><div class="ttd-dialog-submit-shortcut__key">Ctrl</div><div class="ttd-dialog-submit-shortcut__key">Enter</div></div></div></div></div></div></div></div></div></div></div>"
|
C -->|Three| F[Car]</textarea><div class="ttd-dialog-panel-button-container invisible" style="display: flex; align-items: center;"><button type="button" class="excalidraw-button ttd-dialog-panel-button"><div class=""></div></button></div></div><div class="ttd-dialog-panel"><div class="ttd-dialog-panel__header"><label>Preview</label></div><div class="ttd-dialog-output-wrapper"><div style="opacity: 1;" class="ttd-dialog-output-canvas-container"><canvas width="9" height="0" dir="ltr"></canvas></div></div><div class="ttd-dialog-panel-button-container" style="display: flex; align-items: center;"><button type="button" class="excalidraw-button ttd-dialog-panel-button"><div class="">Insert<span><svg aria-hidden="true" focusable="false" role="img" viewBox="0 0 20 20" class="" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round"><g stroke-width="1.25"><path d="M4.16602 10H15.8327"></path><path d="M12.5 13.3333L15.8333 10"></path><path d="M12.5 6.66666L15.8333 9.99999"></path></g></svg></span></div></button><div class="ttd-dialog-submit-shortcut"><div class="ttd-dialog-submit-shortcut__key">Ctrl</div><div class="ttd-dialog-submit-shortcut__key">Enter</div></div></div></div></div></div></div></div></div></div></div>"
|
||||||
`;
|
`;
|
||||||
|
|
||||||
exports[`Test <MermaidToExcalidraw/> > should show error in preview when mermaid library throws error 1`] = `
|
exports[`Test <MermaidToExcalidraw/> > should show error in preview when mermaid library throws error 1`] = `
|
||||||
|
@@ -6,8 +6,8 @@ exports[`export > exporting svg containing transformed images > svg export outpu
|
|||||||
|
|
||||||
<defs>
|
<defs>
|
||||||
<style class="style-fonts">
|
<style class="style-fonts">
|
||||||
</style>
|
|
||||||
|
</style>
|
||||||
|
|
||||||
</defs>
|
</defs>
|
||||||
<clipPath id="image-clipPath-id1" data-id="id1"><rect width="100" height="100" rx="25" ry="25"></rect></clipPath><g transform="translate(20.710678118654755 20.710678118654755) rotate(315 50 50)" clip-path="url(#image-clipPath-id1)" data-id="id1"><use href="#image-file_A" width="100" height="100" opacity="1"></use></g><clipPath id="image-clipPath-id2" data-id="id2"><rect width="50" height="50" rx="12.5" ry="12.5"></rect></clipPath><g transform="translate(120.71067811865476 20.710678118654755) rotate(45 25 25)" clip-path="url(#image-clipPath-id2)" data-id="id2"><use href="#image-file_A" width="50" height="50" opacity="1" transform="translate(25 25) scale(-1 1) translate(-25 -25)"></use></g><clipPath id="image-clipPath-id3" data-id="id3"><rect width="100" height="100" rx="25" ry="25"></rect></clipPath><g transform="translate(20.710678118654755 120.71067811865476) rotate(45 50 50)" clip-path="url(#image-clipPath-id3)" data-id="id3"><use href="#image-file_A" width="100" height="100" opacity="1" transform="translate(50 50) scale(1 -1) translate(-50 -50)"></use></g><clipPath id="image-clipPath-id4" data-id="id4"><rect width="50" height="50" rx="12.5" ry="12.5"></rect></clipPath><g transform="translate(120.71067811865476 120.71067811865476) rotate(315 25 25)" clip-path="url(#image-clipPath-id4)" data-id="id4"><use href="#image-file_A" width="50" height="50" opacity="1" transform="translate(25 25) scale(-1 -1) translate(-25 -25)"></use></g></svg>"
|
<clipPath id="image-clipPath-id1" data-id="id1"><rect width="100" height="100" rx="25" ry="25"></rect></clipPath><g transform="translate(20.710678118654755 20.710678118654755) rotate(315 50 50)" clip-path="url(#image-clipPath-id1)" data-id="id1"><use href="#image-file_A" width="100" height="100" opacity="1"></use></g><clipPath id="image-clipPath-id2" data-id="id2"><rect width="50" height="50" rx="12.5" ry="12.5"></rect></clipPath><g transform="translate(120.71067811865476 20.710678118654755) rotate(45 25 25)" clip-path="url(#image-clipPath-id2)" data-id="id2"><use href="#image-file_A" width="50" height="50" opacity="1" transform="translate(25 25) scale(-1 1) translate(-25 -25)"></use></g><clipPath id="image-clipPath-id3" data-id="id3"><rect width="100" height="100" rx="25" ry="25"></rect></clipPath><g transform="translate(20.710678118654755 120.71067811865476) rotate(45 50 50)" clip-path="url(#image-clipPath-id3)" data-id="id3"><use href="#image-file_A" width="100" height="100" opacity="1" transform="translate(50 50) scale(1 -1) translate(-50 -50)"></use></g><clipPath id="image-clipPath-id4" data-id="id4"><rect width="50" height="50" rx="12.5" ry="12.5"></rect></clipPath><g transform="translate(120.71067811865476 120.71067811865476) rotate(315 25 25)" clip-path="url(#image-clipPath-id4)" data-id="id4"><use href="#image-file_A" width="50" height="50" opacity="1" transform="translate(25 25) scale(-1 -1) translate(-25 -25)"></use></g></svg>"
|
||||||
|
@@ -24,7 +24,7 @@ exports[`<Excalidraw/> > <MainMenu/> > should render main menu with host menu it
|
|||||||
</button>
|
</button>
|
||||||
<a
|
<a
|
||||||
class="dropdown-menu-item dropdown-menu-item-base"
|
class="dropdown-menu-item dropdown-menu-item-base"
|
||||||
href="blog.excalidaw.com"
|
href="https://plus.excalidraw.com/blog"
|
||||||
rel="noreferrer"
|
rel="noreferrer"
|
||||||
target="_blank"
|
target="_blank"
|
||||||
>
|
>
|
||||||
|
File diff suppressed because one or more lines are too long
@@ -15,7 +15,6 @@ import { FONT_FAMILY, FRAME_STYLE } from "../../constants";
|
|||||||
import { prepareElementsForExport } from "../../data";
|
import { prepareElementsForExport } from "../../data";
|
||||||
import { diagramFactory } from "../fixtures/diagramFixture";
|
import { diagramFactory } from "../fixtures/diagramFixture";
|
||||||
import { vi } from "vitest";
|
import { vi } from "vitest";
|
||||||
import { isCloseTo } from "../../../math";
|
|
||||||
|
|
||||||
const DEFAULT_OPTIONS = {
|
const DEFAULT_OPTIONS = {
|
||||||
exportBackground: false,
|
exportBackground: false,
|
||||||
@@ -118,7 +117,9 @@ describe("exportToSvg", () => {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(svgElement.getAttribute("filter")).toMatchInlineSnapshot(`null`);
|
expect(svgElement.getAttribute("filter")).toMatchInlineSnapshot(
|
||||||
|
`"_themeFilter_1883f3"`,
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("with exportPadding", async () => {
|
it("with exportPadding", async () => {
|
||||||
@@ -148,12 +149,10 @@ describe("exportToSvg", () => {
|
|||||||
elements: ELEMENTS,
|
elements: ELEMENTS,
|
||||||
appState: {
|
appState: {
|
||||||
...DEFAULT_OPTIONS,
|
...DEFAULT_OPTIONS,
|
||||||
|
exportScale: SCALE,
|
||||||
},
|
},
|
||||||
files: null,
|
files: null,
|
||||||
},
|
},
|
||||||
config: {
|
|
||||||
scale: SCALE,
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(svgElement).toHaveAttribute(
|
expect(svgElement).toHaveAttribute(
|
||||||
@@ -607,622 +606,3 @@ describe("exportToBlob", async () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("updated API", () => {
|
|
||||||
// set up
|
|
||||||
// a random set of elements
|
|
||||||
const ELEMENT_HEIGHT = 100;
|
|
||||||
const ELEMENT_WIDTH = 100;
|
|
||||||
const POSITION = 1000;
|
|
||||||
const getRandomPos = () => {
|
|
||||||
const randomNum = () =>
|
|
||||||
Math.round((Math.random() < 0.5 ? 1 : -1) * Math.random() * POSITION);
|
|
||||||
return { x: randomNum(), y: randomNum() };
|
|
||||||
};
|
|
||||||
const ELEMENTS = [
|
|
||||||
{
|
|
||||||
...diamondFixture,
|
|
||||||
height: ELEMENT_HEIGHT,
|
|
||||||
width: ELEMENT_WIDTH,
|
|
||||||
index: "a0",
|
|
||||||
...getRandomPos(),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
...ellipseFixture,
|
|
||||||
height: ELEMENT_HEIGHT,
|
|
||||||
width: ELEMENT_WIDTH,
|
|
||||||
index: "a1",
|
|
||||||
...getRandomPos(),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
...textFixture,
|
|
||||||
height: ELEMENT_HEIGHT,
|
|
||||||
width: ELEMENT_WIDTH,
|
|
||||||
index: "a2",
|
|
||||||
...getRandomPos(),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
...textFixture,
|
|
||||||
fontFamily: FONT_FAMILY.Nunito, // test embedding external font
|
|
||||||
height: ELEMENT_HEIGHT,
|
|
||||||
width: ELEMENT_WIDTH,
|
|
||||||
index: "a3",
|
|
||||||
...getRandomPos(),
|
|
||||||
},
|
|
||||||
] as NonDeletedExcalidrawElement[];
|
|
||||||
|
|
||||||
// entire canvas
|
|
||||||
describe("exporting the entire canvas", () => {
|
|
||||||
const [, , canvasWidth, canvasHeight] = exportUtils.getCanvasSize(ELEMENTS);
|
|
||||||
|
|
||||||
it("fit = none, no padding", async () => {
|
|
||||||
const svgElement = await exportUtils.exportToSvg({
|
|
||||||
data: { elements: ELEMENTS, appState: DEFAULT_OPTIONS, files: null },
|
|
||||||
config: {
|
|
||||||
fit: "none",
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const canvas = await exportUtils.exportToCanvas({
|
|
||||||
data: { elements: ELEMENTS, appState: DEFAULT_OPTIONS, files: null },
|
|
||||||
});
|
|
||||||
|
|
||||||
expect(svgElement.getAttribute("width")).toBeCloseTo(canvas.width);
|
|
||||||
expect(svgElement.getAttribute("height")).toBeCloseTo(canvas.height);
|
|
||||||
expect(canvas.width).toBeCloseTo(canvasWidth, 1);
|
|
||||||
expect(canvas.height).toBeCloseTo(canvasHeight, 1);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("fit = contain, no padding", async () => {
|
|
||||||
const config: exportUtils.ExportSceneConfig = {
|
|
||||||
fit: "none",
|
|
||||||
};
|
|
||||||
|
|
||||||
const svgElement = await exportUtils.exportToSvg({
|
|
||||||
data: { elements: ELEMENTS, appState: DEFAULT_OPTIONS, files: null },
|
|
||||||
config,
|
|
||||||
});
|
|
||||||
|
|
||||||
const canvas = await exportUtils.exportToCanvas({
|
|
||||||
data: { elements: ELEMENTS, appState: DEFAULT_OPTIONS, files: null },
|
|
||||||
config: {
|
|
||||||
fit: "contain",
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
expect(svgElement.getAttribute("width")).toBeCloseTo(canvas.width);
|
|
||||||
expect(svgElement.getAttribute("height")).toBeCloseTo(canvas.height);
|
|
||||||
expect(isCloseTo(canvas.width, canvasWidth, 1)).toBe(true);
|
|
||||||
expect(isCloseTo(canvas.height, canvasHeight, 1)).toBe(true);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("fit = none, with padding", async () => {
|
|
||||||
const PADDING = Math.round(Math.random() * 100);
|
|
||||||
|
|
||||||
const config: exportUtils.ExportSceneConfig = {
|
|
||||||
fit: "none",
|
|
||||||
padding: PADDING,
|
|
||||||
};
|
|
||||||
const svgElement = await exportUtils.exportToSvg({
|
|
||||||
data: { elements: ELEMENTS, appState: DEFAULT_OPTIONS, files: null },
|
|
||||||
config,
|
|
||||||
});
|
|
||||||
|
|
||||||
const canvas = await exportUtils.exportToCanvas({
|
|
||||||
data: { elements: ELEMENTS, appState: DEFAULT_OPTIONS, files: null },
|
|
||||||
config,
|
|
||||||
});
|
|
||||||
|
|
||||||
expect(svgElement.getAttribute("width")).toBeCloseTo(canvas.width);
|
|
||||||
expect(svgElement.getAttribute("height")).toBeCloseTo(canvas.height);
|
|
||||||
expect(canvas.width).toBeCloseTo(canvasWidth + PADDING * 2);
|
|
||||||
expect(canvas.height).toBeCloseTo(canvasHeight + PADDING * 2);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("fit = contain, with padding", async () => {
|
|
||||||
const PADDING = Math.round(Math.random() * 100);
|
|
||||||
|
|
||||||
const config: exportUtils.ExportSceneConfig = {
|
|
||||||
fit: "contain",
|
|
||||||
padding: PADDING,
|
|
||||||
};
|
|
||||||
const svgElement = await exportUtils.exportToSvg({
|
|
||||||
data: { elements: ELEMENTS, appState: DEFAULT_OPTIONS, files: null },
|
|
||||||
config,
|
|
||||||
});
|
|
||||||
|
|
||||||
const canvas = await exportUtils.exportToCanvas({
|
|
||||||
data: { elements: ELEMENTS, appState: DEFAULT_OPTIONS, files: null },
|
|
||||||
config,
|
|
||||||
});
|
|
||||||
|
|
||||||
expect(
|
|
||||||
isCloseTo(
|
|
||||||
parseFloat(svgElement.getAttribute("width") ?? ""),
|
|
||||||
canvas.width,
|
|
||||||
1,
|
|
||||||
),
|
|
||||||
).toBe(true);
|
|
||||||
expect(
|
|
||||||
isCloseTo(
|
|
||||||
parseFloat(svgElement.getAttribute("height") ?? ""),
|
|
||||||
canvas.height,
|
|
||||||
1,
|
|
||||||
),
|
|
||||||
).toBe(true);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
// specified dimensions (w x h)
|
|
||||||
describe("exporting with specified dimensions", () => {
|
|
||||||
const dimension = {
|
|
||||||
width: 200,
|
|
||||||
height: 200,
|
|
||||||
};
|
|
||||||
|
|
||||||
it("fit = none, no padding", async () => {
|
|
||||||
const config: exportUtils.ExportSceneConfig = {
|
|
||||||
fit: "none",
|
|
||||||
...dimension,
|
|
||||||
};
|
|
||||||
|
|
||||||
const svgElement = await exportUtils.exportToSvg({
|
|
||||||
data: { elements: ELEMENTS, appState: DEFAULT_OPTIONS, files: null },
|
|
||||||
config,
|
|
||||||
});
|
|
||||||
|
|
||||||
const canvas = await exportUtils.exportToCanvas({
|
|
||||||
data: { elements: ELEMENTS, appState: DEFAULT_OPTIONS, files: null },
|
|
||||||
config,
|
|
||||||
});
|
|
||||||
|
|
||||||
expect(svgElement.getAttribute("width")).toBeCloseTo(canvas.width);
|
|
||||||
expect(svgElement.getAttribute("height")).toBeCloseTo(canvas.height);
|
|
||||||
expect(canvas.width).toBeCloseTo(dimension.width, 1);
|
|
||||||
expect(canvas.height).toBeCloseTo(dimension.height, 1);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("fit = contain, no padding", async () => {
|
|
||||||
const config: exportUtils.ExportSceneConfig = {
|
|
||||||
fit: "contain",
|
|
||||||
...dimension,
|
|
||||||
};
|
|
||||||
|
|
||||||
const svgElement = await exportUtils.exportToSvg({
|
|
||||||
data: { elements: ELEMENTS, appState: DEFAULT_OPTIONS, files: null },
|
|
||||||
config,
|
|
||||||
});
|
|
||||||
|
|
||||||
const canvas = await exportUtils.exportToCanvas({
|
|
||||||
data: { elements: ELEMENTS, appState: DEFAULT_OPTIONS, files: null },
|
|
||||||
config,
|
|
||||||
});
|
|
||||||
|
|
||||||
expect(svgElement.getAttribute("width")).toBeCloseTo(canvas.width);
|
|
||||||
expect(svgElement.getAttribute("height")).toBeCloseTo(canvas.height);
|
|
||||||
expect(canvas.width).toBeCloseTo(dimension.width, 1);
|
|
||||||
expect(canvas.height).toBeCloseTo(dimension.height, 1);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("fit = none, with padding", async () => {
|
|
||||||
const PADDING = Math.round(Math.random() * 100);
|
|
||||||
|
|
||||||
const config: exportUtils.ExportSceneConfig = {
|
|
||||||
fit: "none",
|
|
||||||
padding: PADDING,
|
|
||||||
...dimension,
|
|
||||||
};
|
|
||||||
|
|
||||||
const svgElement = await exportUtils.exportToSvg({
|
|
||||||
data: { elements: ELEMENTS, appState: DEFAULT_OPTIONS, files: null },
|
|
||||||
config,
|
|
||||||
});
|
|
||||||
|
|
||||||
const canvas = await exportUtils.exportToCanvas({
|
|
||||||
data: { elements: ELEMENTS, appState: DEFAULT_OPTIONS, files: null },
|
|
||||||
config,
|
|
||||||
});
|
|
||||||
|
|
||||||
expect(svgElement.getAttribute("width")).toBeCloseTo(canvas.width);
|
|
||||||
expect(svgElement.getAttribute("height")).toBeCloseTo(canvas.height);
|
|
||||||
expect(canvas.width).toBeCloseTo(dimension.width + PADDING * 2, 1);
|
|
||||||
expect(canvas.height).toBeCloseTo(dimension.height + PADDING * 2, 1);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("fit = contain, with padding", async () => {
|
|
||||||
const PADDING = Math.round(Math.random() * 100);
|
|
||||||
|
|
||||||
const config: exportUtils.ExportSceneConfig = {
|
|
||||||
fit: "contain",
|
|
||||||
padding: PADDING,
|
|
||||||
...dimension,
|
|
||||||
};
|
|
||||||
|
|
||||||
const svgElement = await exportUtils.exportToSvg({
|
|
||||||
data: { elements: ELEMENTS, appState: DEFAULT_OPTIONS, files: null },
|
|
||||||
config,
|
|
||||||
});
|
|
||||||
|
|
||||||
const canvas = await exportUtils.exportToCanvas({
|
|
||||||
data: { elements: ELEMENTS, appState: DEFAULT_OPTIONS, files: null },
|
|
||||||
config,
|
|
||||||
});
|
|
||||||
|
|
||||||
expect(svgElement.getAttribute("width")).toBeCloseTo(canvas.width);
|
|
||||||
expect(svgElement.getAttribute("height")).toBeCloseTo(canvas.height);
|
|
||||||
|
|
||||||
expect(isCloseTo(canvas.width, dimension.width, 1)).toBe(true);
|
|
||||||
expect(isCloseTo(canvas.height, dimension.height, 1)).toBe(true);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
// specified maxWH
|
|
||||||
describe("exporting with specified maxWidthOrHeight", () => {
|
|
||||||
const maxWH = 200;
|
|
||||||
|
|
||||||
it("fit = none, no padding", async () => {
|
|
||||||
const config: exportUtils.ExportSceneConfig = {
|
|
||||||
fit: "none",
|
|
||||||
maxWidthOrHeight: maxWH,
|
|
||||||
};
|
|
||||||
|
|
||||||
const svgElement = await exportUtils.exportToSvg({
|
|
||||||
data: { elements: ELEMENTS, appState: DEFAULT_OPTIONS, files: null },
|
|
||||||
config,
|
|
||||||
});
|
|
||||||
|
|
||||||
const canvas = await exportUtils.exportToCanvas({
|
|
||||||
data: { elements: ELEMENTS, appState: DEFAULT_OPTIONS, files: null },
|
|
||||||
config,
|
|
||||||
});
|
|
||||||
|
|
||||||
expect(
|
|
||||||
isCloseTo(
|
|
||||||
parseFloat(svgElement.getAttribute("width") ?? ""),
|
|
||||||
canvas.width,
|
|
||||||
1,
|
|
||||||
),
|
|
||||||
).toBe(true);
|
|
||||||
expect(
|
|
||||||
isCloseTo(
|
|
||||||
parseFloat(svgElement.getAttribute("height") ?? ""),
|
|
||||||
canvas.height,
|
|
||||||
1,
|
|
||||||
),
|
|
||||||
).toBe(true);
|
|
||||||
expect(canvas.width).toBeLessThanOrEqual(maxWH);
|
|
||||||
expect(canvas.height).toBeLessThanOrEqual(maxWH);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("fit = contain, no padding", async () => {
|
|
||||||
const config: exportUtils.ExportSceneConfig = {
|
|
||||||
fit: "contain",
|
|
||||||
maxWidthOrHeight: maxWH,
|
|
||||||
};
|
|
||||||
|
|
||||||
const svgElement = await exportUtils.exportToSvg({
|
|
||||||
data: { elements: ELEMENTS, appState: DEFAULT_OPTIONS, files: null },
|
|
||||||
config,
|
|
||||||
});
|
|
||||||
|
|
||||||
const canvas = await exportUtils.exportToCanvas({
|
|
||||||
data: { elements: ELEMENTS, appState: DEFAULT_OPTIONS, files: null },
|
|
||||||
config,
|
|
||||||
});
|
|
||||||
|
|
||||||
expect(
|
|
||||||
isCloseTo(
|
|
||||||
parseFloat(svgElement.getAttribute("width") ?? ""),
|
|
||||||
canvas.width,
|
|
||||||
1,
|
|
||||||
),
|
|
||||||
).toBe(true);
|
|
||||||
expect(
|
|
||||||
isCloseTo(
|
|
||||||
parseFloat(svgElement.getAttribute("height") ?? ""),
|
|
||||||
canvas.height,
|
|
||||||
1,
|
|
||||||
),
|
|
||||||
).toBe(true);
|
|
||||||
expect(canvas.width).toBeLessThanOrEqual(maxWH);
|
|
||||||
expect(canvas.height).toBeLessThanOrEqual(maxWH);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("fit = none, with padding", async () => {
|
|
||||||
const PADDING = Math.round(Math.random() * 100);
|
|
||||||
|
|
||||||
const config: exportUtils.ExportSceneConfig = {
|
|
||||||
fit: "none",
|
|
||||||
padding: PADDING,
|
|
||||||
maxWidthOrHeight: maxWH,
|
|
||||||
};
|
|
||||||
|
|
||||||
const svgElement = await exportUtils.exportToSvg({
|
|
||||||
data: { elements: ELEMENTS, appState: DEFAULT_OPTIONS, files: null },
|
|
||||||
config,
|
|
||||||
});
|
|
||||||
|
|
||||||
const canvas = await exportUtils.exportToCanvas({
|
|
||||||
data: { elements: ELEMENTS, appState: DEFAULT_OPTIONS, files: null },
|
|
||||||
config,
|
|
||||||
});
|
|
||||||
|
|
||||||
expect(
|
|
||||||
isCloseTo(
|
|
||||||
parseFloat(svgElement.getAttribute("width") ?? ""),
|
|
||||||
canvas.width,
|
|
||||||
1,
|
|
||||||
),
|
|
||||||
).toBe(true);
|
|
||||||
expect(
|
|
||||||
isCloseTo(
|
|
||||||
parseFloat(svgElement.getAttribute("height") ?? ""),
|
|
||||||
canvas.height,
|
|
||||||
1,
|
|
||||||
),
|
|
||||||
).toBe(true);
|
|
||||||
|
|
||||||
expect(canvas.width).toBeLessThanOrEqual(maxWH);
|
|
||||||
expect(canvas.height).toBeLessThanOrEqual(maxWH);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("fit = contain, with padding", async () => {
|
|
||||||
const PADDING = Math.round(Math.random() * 100);
|
|
||||||
|
|
||||||
const config: exportUtils.ExportSceneConfig = {
|
|
||||||
fit: "contain",
|
|
||||||
padding: PADDING,
|
|
||||||
maxWidthOrHeight: maxWH,
|
|
||||||
};
|
|
||||||
|
|
||||||
const svgElement = await exportUtils.exportToSvg({
|
|
||||||
data: { elements: ELEMENTS, appState: DEFAULT_OPTIONS, files: null },
|
|
||||||
config,
|
|
||||||
});
|
|
||||||
|
|
||||||
const canvas = await exportUtils.exportToCanvas({
|
|
||||||
data: { elements: ELEMENTS, appState: DEFAULT_OPTIONS, files: null },
|
|
||||||
config,
|
|
||||||
});
|
|
||||||
|
|
||||||
expect(
|
|
||||||
isCloseTo(
|
|
||||||
parseFloat(svgElement.getAttribute("width") ?? ""),
|
|
||||||
canvas.width,
|
|
||||||
1,
|
|
||||||
),
|
|
||||||
).toBe(true);
|
|
||||||
expect(
|
|
||||||
isCloseTo(
|
|
||||||
parseFloat(svgElement.getAttribute("height") ?? ""),
|
|
||||||
canvas.height,
|
|
||||||
1,
|
|
||||||
),
|
|
||||||
).toBe(true);
|
|
||||||
|
|
||||||
expect(canvas.width).toBeLessThanOrEqual(maxWH);
|
|
||||||
expect(canvas.height).toBeLessThanOrEqual(maxWH);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
// specified widthOrHeight
|
|
||||||
describe("exporting with specified widthOrHeight", () => {
|
|
||||||
const widthOrHeight = 200;
|
|
||||||
|
|
||||||
it("fit = none, no padding", async () => {
|
|
||||||
const config: exportUtils.ExportSceneConfig = {
|
|
||||||
fit: "none",
|
|
||||||
widthOrHeight,
|
|
||||||
};
|
|
||||||
|
|
||||||
const svgElement = await exportUtils.exportToSvg({
|
|
||||||
data: { elements: ELEMENTS, appState: DEFAULT_OPTIONS, files: null },
|
|
||||||
config,
|
|
||||||
});
|
|
||||||
|
|
||||||
const canvas = await exportUtils.exportToCanvas({
|
|
||||||
data: { elements: ELEMENTS, appState: DEFAULT_OPTIONS, files: null },
|
|
||||||
config,
|
|
||||||
});
|
|
||||||
|
|
||||||
expect(
|
|
||||||
isCloseTo(
|
|
||||||
parseFloat(svgElement.getAttribute("width") ?? ""),
|
|
||||||
canvas.width,
|
|
||||||
1,
|
|
||||||
),
|
|
||||||
).toBe(true);
|
|
||||||
expect(
|
|
||||||
isCloseTo(
|
|
||||||
parseFloat(svgElement.getAttribute("height") ?? ""),
|
|
||||||
canvas.height,
|
|
||||||
1,
|
|
||||||
),
|
|
||||||
).toBe(true);
|
|
||||||
expect(
|
|
||||||
isCloseTo(canvas.width, widthOrHeight, 1) ||
|
|
||||||
isCloseTo(canvas.height, widthOrHeight, 1),
|
|
||||||
).toBe(true);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("fit = contain, no padding", async () => {
|
|
||||||
const config: exportUtils.ExportSceneConfig = {
|
|
||||||
fit: "contain",
|
|
||||||
widthOrHeight,
|
|
||||||
};
|
|
||||||
|
|
||||||
const svgElement = await exportUtils.exportToSvg({
|
|
||||||
data: { elements: ELEMENTS, appState: DEFAULT_OPTIONS, files: null },
|
|
||||||
config,
|
|
||||||
});
|
|
||||||
|
|
||||||
const canvas = await exportUtils.exportToCanvas({
|
|
||||||
data: { elements: ELEMENTS, appState: DEFAULT_OPTIONS, files: null },
|
|
||||||
config,
|
|
||||||
});
|
|
||||||
|
|
||||||
expect(
|
|
||||||
isCloseTo(
|
|
||||||
parseFloat(svgElement.getAttribute("width") ?? ""),
|
|
||||||
canvas.width,
|
|
||||||
1,
|
|
||||||
),
|
|
||||||
).toBe(true);
|
|
||||||
expect(
|
|
||||||
isCloseTo(
|
|
||||||
parseFloat(svgElement.getAttribute("height") ?? ""),
|
|
||||||
canvas.height,
|
|
||||||
1,
|
|
||||||
),
|
|
||||||
).toBe(true);
|
|
||||||
|
|
||||||
expect(
|
|
||||||
isCloseTo(canvas.width, widthOrHeight, 1) ||
|
|
||||||
isCloseTo(canvas.height, widthOrHeight, 1),
|
|
||||||
).toBe(true);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("fit = none, with padding", async () => {
|
|
||||||
const PADDING = Math.round(Math.random() * 100);
|
|
||||||
|
|
||||||
const config: exportUtils.ExportSceneConfig = {
|
|
||||||
fit: "none",
|
|
||||||
padding: PADDING,
|
|
||||||
widthOrHeight,
|
|
||||||
};
|
|
||||||
|
|
||||||
const svgElement = await exportUtils.exportToSvg({
|
|
||||||
data: { elements: ELEMENTS, appState: DEFAULT_OPTIONS, files: null },
|
|
||||||
config,
|
|
||||||
});
|
|
||||||
|
|
||||||
const canvas = await exportUtils.exportToCanvas({
|
|
||||||
data: { elements: ELEMENTS, appState: DEFAULT_OPTIONS, files: null },
|
|
||||||
config,
|
|
||||||
});
|
|
||||||
|
|
||||||
expect(
|
|
||||||
isCloseTo(
|
|
||||||
parseFloat(svgElement.getAttribute("width") ?? ""),
|
|
||||||
canvas.width,
|
|
||||||
1,
|
|
||||||
),
|
|
||||||
).toBe(true);
|
|
||||||
expect(
|
|
||||||
isCloseTo(
|
|
||||||
parseFloat(svgElement.getAttribute("height") ?? ""),
|
|
||||||
canvas.height,
|
|
||||||
1,
|
|
||||||
),
|
|
||||||
).toBe(true);
|
|
||||||
expect(
|
|
||||||
isCloseTo(canvas.width, widthOrHeight, 1) ||
|
|
||||||
isCloseTo(canvas.height, widthOrHeight, 1),
|
|
||||||
).toBe(true);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("fit = contain, with padding", async () => {
|
|
||||||
const PADDING = Math.round(Math.random() * 100);
|
|
||||||
|
|
||||||
const config: exportUtils.ExportSceneConfig = {
|
|
||||||
fit: "contain",
|
|
||||||
padding: PADDING,
|
|
||||||
widthOrHeight,
|
|
||||||
};
|
|
||||||
|
|
||||||
const svgElement = await exportUtils.exportToSvg({
|
|
||||||
data: { elements: ELEMENTS, appState: DEFAULT_OPTIONS, files: null },
|
|
||||||
config,
|
|
||||||
});
|
|
||||||
|
|
||||||
const canvas = await exportUtils.exportToCanvas({
|
|
||||||
data: { elements: ELEMENTS, appState: DEFAULT_OPTIONS, files: null },
|
|
||||||
config,
|
|
||||||
});
|
|
||||||
|
|
||||||
expect(
|
|
||||||
isCloseTo(
|
|
||||||
parseFloat(svgElement.getAttribute("width") ?? ""),
|
|
||||||
canvas.width,
|
|
||||||
1,
|
|
||||||
),
|
|
||||||
).toBe(true);
|
|
||||||
expect(
|
|
||||||
isCloseTo(
|
|
||||||
parseFloat(svgElement.getAttribute("height") ?? ""),
|
|
||||||
canvas.height,
|
|
||||||
1,
|
|
||||||
),
|
|
||||||
).toBe(true);
|
|
||||||
expect(
|
|
||||||
isCloseTo(canvas.width, widthOrHeight, 1) ||
|
|
||||||
isCloseTo(canvas.height, widthOrHeight, 1),
|
|
||||||
).toBe(true);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
// specified position
|
|
||||||
describe("exporting with specified position", () => {
|
|
||||||
const [, , canvasWidth, canvasHeight] = exportUtils.getCanvasSize(ELEMENTS);
|
|
||||||
const position = { x: 100, y: 100 };
|
|
||||||
|
|
||||||
it("fit = none, no padding", async () => {
|
|
||||||
const config: exportUtils.ExportSceneConfig = {
|
|
||||||
fit: "none",
|
|
||||||
...position,
|
|
||||||
};
|
|
||||||
|
|
||||||
const svgElement = await exportUtils.exportToSvg({
|
|
||||||
data: { elements: ELEMENTS, appState: DEFAULT_OPTIONS, files: null },
|
|
||||||
config,
|
|
||||||
});
|
|
||||||
|
|
||||||
const canvas = await exportUtils.exportToCanvas({
|
|
||||||
data: { elements: ELEMENTS, appState: DEFAULT_OPTIONS, files: null },
|
|
||||||
config,
|
|
||||||
});
|
|
||||||
|
|
||||||
expect(
|
|
||||||
isCloseTo(
|
|
||||||
parseFloat(svgElement.getAttribute("width") ?? ""),
|
|
||||||
canvas.width,
|
|
||||||
1,
|
|
||||||
),
|
|
||||||
).toBe(true);
|
|
||||||
expect(
|
|
||||||
isCloseTo(
|
|
||||||
parseFloat(svgElement.getAttribute("height") ?? ""),
|
|
||||||
canvas.height,
|
|
||||||
1,
|
|
||||||
),
|
|
||||||
).toBe(true);
|
|
||||||
expect(canvas.width).toBeCloseTo(canvasWidth, 1);
|
|
||||||
expect(canvas.height).toBeCloseTo(canvasHeight, 1);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("fit = none, with padding", async () => {
|
|
||||||
const PADDING = Math.round(Math.random() * 100);
|
|
||||||
|
|
||||||
const config: exportUtils.ExportSceneConfig = {
|
|
||||||
fit: "none",
|
|
||||||
padding: PADDING,
|
|
||||||
...position,
|
|
||||||
};
|
|
||||||
|
|
||||||
const svgElement = await exportUtils.exportToSvg({
|
|
||||||
data: { elements: ELEMENTS, appState: DEFAULT_OPTIONS, files: null },
|
|
||||||
config,
|
|
||||||
});
|
|
||||||
|
|
||||||
const canvas = await exportUtils.exportToCanvas({
|
|
||||||
data: { elements: ELEMENTS, appState: DEFAULT_OPTIONS, files: null },
|
|
||||||
config,
|
|
||||||
});
|
|
||||||
|
|
||||||
expect(svgElement.getAttribute("width")).toBeCloseTo(canvas.width);
|
|
||||||
expect(svgElement.getAttribute("height")).toBeCloseTo(canvas.height);
|
|
||||||
expect(canvas.width).toBeCloseTo(canvasWidth + PADDING * 2);
|
|
||||||
expect(canvas.height).toBeCloseTo(canvasHeight + PADDING * 2);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
Reference in New Issue
Block a user