feat: render frames on export (#7210)

This commit is contained in:
David Luzar 2023-11-09 17:00:21 +01:00 committed by GitHub
parent a9a6f8eafb
commit 864c0b3ea8
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
30 changed files with 989 additions and 332 deletions

View file

@ -20,7 +20,13 @@ import type { Drawable } from "roughjs/bin/core";
import type { RoughSVG } from "roughjs/bin/svg";
import { StaticCanvasRenderConfig } from "../scene/types";
import { distance, getFontString, getFontFamilyString, isRTL } from "../utils";
import {
distance,
getFontString,
getFontFamilyString,
isRTL,
isTestEnv,
} from "../utils";
import { getCornerRadius, isPathALoop, isRightAngle } from "../math";
import rough from "roughjs/bin/rough";
import {
@ -589,11 +595,7 @@ export const renderElement = (
) => {
switch (element.type) {
case "frame": {
if (
!renderConfig.isExporting &&
appState.frameRendering.enabled &&
appState.frameRendering.outline
) {
if (appState.frameRendering.enabled && appState.frameRendering.outline) {
context.save();
context.translate(
element.x + appState.scrollX,
@ -601,7 +603,7 @@ export const renderElement = (
);
context.fillStyle = "rgba(0, 0, 200, 0.04)";
context.lineWidth = 2 / appState.zoom.value;
context.lineWidth = FRAME_STYLE.strokeWidth / appState.zoom.value;
context.strokeStyle = FRAME_STYLE.strokeColor;
if (FRAME_STYLE.radius && context.roundRect) {
@ -841,10 +843,13 @@ const maybeWrapNodesInFrameClipPath = (
element: NonDeletedExcalidrawElement,
root: SVGElement,
nodes: SVGElement[],
exportedFrameId?: string | null,
frameRendering: AppState["frameRendering"],
) => {
if (!frameRendering.enabled || !frameRendering.clip) {
return null;
}
const frame = getContainingFrame(element);
if (frame && frame.id === exportedFrameId) {
if (frame) {
const g = root.ownerDocument!.createElementNS(SVG_NS, "g");
g.setAttributeNS(SVG_NS, "clip-path", `url(#${frame.id})`);
nodes.forEach((node) => g.appendChild(node));
@ -861,9 +866,11 @@ export const renderElementToSvg = (
files: BinaryFiles,
offsetX: number,
offsetY: number,
exportWithDarkMode?: boolean,
exportingFrameId?: string | null,
renderEmbeddables?: boolean,
renderConfig: {
exportWithDarkMode: boolean;
renderEmbeddables: boolean;
frameRendering: AppState["frameRendering"];
},
) => {
const offset = { x: offsetX, y: offsetY };
const [x1, y1, x2, y2] = getElementAbsoluteCoords(element);
@ -897,6 +904,13 @@ export const renderElementToSvg = (
root = anchorTag;
}
const addToRoot = (node: SVGElement, element: ExcalidrawElement) => {
if (isTestEnv()) {
node.setAttribute("data-id", element.id);
}
root.appendChild(node);
};
const opacity =
((getContainingFrame(element)?.opacity ?? 100) * element.opacity) / 10000;
@ -931,10 +945,10 @@ export const renderElementToSvg = (
element,
root,
[node],
exportingFrameId,
renderConfig.frameRendering,
);
g ? root.appendChild(g) : root.appendChild(node);
addToRoot(g || node, element);
break;
}
case "embeddable": {
@ -957,7 +971,7 @@ export const renderElementToSvg = (
offsetY || 0
}) rotate(${degree} ${cx} ${cy})`,
);
root.appendChild(node);
addToRoot(node, element);
const label: ExcalidrawElement =
createPlaceholderEmbeddableLabel(element);
@ -968,9 +982,7 @@ export const renderElementToSvg = (
files,
label.x + offset.x - element.x,
label.y + offset.y - element.y,
exportWithDarkMode,
exportingFrameId,
renderEmbeddables,
renderConfig,
);
// render embeddable element + iframe
@ -999,7 +1011,10 @@ export const renderElementToSvg = (
// if rendering embeddables explicitly disabled or
// embedding documents via srcdoc (which doesn't seem to work for SVGs)
// replace with a link instead
if (renderEmbeddables === false || embedLink?.type === "document") {
if (
renderConfig.renderEmbeddables === false ||
embedLink?.type === "document"
) {
const anchorTag = svgRoot.ownerDocument!.createElementNS(SVG_NS, "a");
anchorTag.setAttribute("href", normalizeLink(element.link || ""));
anchorTag.setAttribute("target", "_blank");
@ -1033,8 +1048,7 @@ export const renderElementToSvg = (
embeddableNode.appendChild(foreignObject);
}
root.appendChild(embeddableNode);
addToRoot(embeddableNode, element);
break;
}
case "line":
@ -1119,12 +1133,13 @@ export const renderElementToSvg = (
element,
root,
[group, maskPath],
exportingFrameId,
renderConfig.frameRendering,
);
if (g) {
addToRoot(g, element);
root.appendChild(g);
} else {
root.appendChild(group);
addToRoot(group, element);
root.append(maskPath);
}
break;
@ -1158,10 +1173,10 @@ export const renderElementToSvg = (
element,
root,
[node],
exportingFrameId,
renderConfig.frameRendering,
);
g ? root.appendChild(g) : root.appendChild(node);
addToRoot(g || node, element);
break;
}
case "image": {
@ -1191,7 +1206,10 @@ export const renderElementToSvg = (
use.setAttribute("href", `#${symbolId}`);
// in dark theme, revert the image color filter
if (exportWithDarkMode && fileData.mimeType !== MIME_TYPES.svg) {
if (
renderConfig.exportWithDarkMode &&
fileData.mimeType !== MIME_TYPES.svg
) {
use.setAttribute("filter", IMAGE_INVERT_FILTER);
}
@ -1227,14 +1245,39 @@ export const renderElementToSvg = (
element,
root,
[g],
exportingFrameId,
renderConfig.frameRendering,
);
clipG ? root.appendChild(clipG) : root.appendChild(g);
addToRoot(clipG || g, element);
}
break;
}
// frames are not rendered and only acts as a container
case "frame": {
if (
renderConfig.frameRendering.enabled &&
renderConfig.frameRendering.outline
) {
const rect = document.createElementNS(SVG_NS, "rect");
rect.setAttribute(
"transform",
`translate(${offsetX || 0} ${
offsetY || 0
}) rotate(${degree} ${cx} ${cy})`,
);
rect.setAttribute("width", `${element.width}px`);
rect.setAttribute("height", `${element.height}px`);
// Rounded corners
rect.setAttribute("rx", FRAME_STYLE.radius.toString());
rect.setAttribute("ry", FRAME_STYLE.radius.toString());
rect.setAttribute("fill", "none");
rect.setAttribute("stroke", FRAME_STYLE.strokeColor);
rect.setAttribute("stroke-width", FRAME_STYLE.strokeWidth.toString());
addToRoot(rect, element);
}
break;
}
default: {
@ -1288,10 +1331,10 @@ export const renderElementToSvg = (
element,
root,
[node],
exportingFrameId,
renderConfig.frameRendering,
);
g ? root.appendChild(g) : root.appendChild(node);
addToRoot(g || node, element);
} else {
// @ts-ignore
throw new Error(`Unimplemented type ${element.type}`);

View file

@ -60,7 +60,7 @@ import {
TransformHandles,
TransformHandleType,
} from "../element/transformHandles";
import { throttleRAF, isOnlyExportingSingleFrame } from "../utils";
import { throttleRAF } from "../utils";
import { UserIdleState } from "../types";
import { FRAME_STYLE, THEME_FILTER } from "../constants";
import {
@ -74,7 +74,7 @@ import {
isLinearElement,
} from "../element/typeChecks";
import {
isEmbeddableOrFrameLabel,
isEmbeddableOrLabel,
createPlaceholderEmbeddableLabel,
} from "../element/embeddable";
import {
@ -369,7 +369,7 @@ const frameClip = (
) => {
context.translate(frame.x + appState.scrollX, frame.y + appState.scrollY);
context.beginPath();
if (context.roundRect && !renderConfig.isExporting) {
if (context.roundRect) {
context.roundRect(
0,
0,
@ -963,20 +963,15 @@ const _renderStaticScene = ({
// Paint visible elements
visibleElements
.filter((el) => !isEmbeddableOrFrameLabel(el))
.filter((el) => !isEmbeddableOrLabel(el))
.forEach((element) => {
try {
// - when exporting the whole canvas, we DO NOT apply clipping
// - when we are exporting a particular frame, apply clipping
// if the containing frame is not selected, apply clipping
const frameId = element.frameId || appState.frameToHighlight?.id;
if (
frameId &&
((renderConfig.isExporting && isOnlyExportingSingleFrame(elements)) ||
(!renderConfig.isExporting &&
appState.frameRendering.enabled &&
appState.frameRendering.clip))
appState.frameRendering.enabled &&
appState.frameRendering.clip
) {
context.save();
@ -1001,7 +996,7 @@ const _renderStaticScene = ({
// render embeddables on top
visibleElements
.filter((el) => isEmbeddableOrFrameLabel(el))
.filter((el) => isEmbeddableOrLabel(el))
.forEach((element) => {
try {
const render = () => {
@ -1027,10 +1022,8 @@ const _renderStaticScene = ({
if (
frameId &&
((renderConfig.isExporting && isOnlyExportingSingleFrame(elements)) ||
(!renderConfig.isExporting &&
appState.frameRendering.enabled &&
appState.frameRendering.clip))
appState.frameRendering.enabled &&
appState.frameRendering.clip
) {
context.save();
@ -1298,7 +1291,7 @@ const renderFrameHighlight = (
const height = y2 - y1;
context.strokeStyle = "rgb(0,118,255)";
context.lineWidth = (FRAME_STYLE.strokeWidth * 2) / appState.zoom.value;
context.lineWidth = FRAME_STYLE.strokeWidth / appState.zoom.value;
context.save();
context.translate(appState.scrollX, appState.scrollY);
@ -1454,24 +1447,29 @@ export const renderSceneToSvg = (
{
offsetX = 0,
offsetY = 0,
exportWithDarkMode = false,
exportingFrameId = null,
exportWithDarkMode,
renderEmbeddables,
frameRendering,
}: {
offsetX?: number;
offsetY?: number;
exportWithDarkMode?: boolean;
exportingFrameId?: string | null;
renderEmbeddables?: boolean;
} = {},
exportWithDarkMode: boolean;
renderEmbeddables: boolean;
frameRendering: AppState["frameRendering"];
},
) => {
if (!svgRoot) {
return;
}
const renderConfig = {
exportWithDarkMode,
renderEmbeddables,
frameRendering,
};
// render elements
elements
.filter((el) => !isEmbeddableOrFrameLabel(el))
.filter((el) => !isEmbeddableOrLabel(el))
.forEach((element) => {
if (!element.isDeleted) {
try {
@ -1482,9 +1480,7 @@ export const renderSceneToSvg = (
files,
element.x + offsetX,
element.y + offsetY,
exportWithDarkMode,
exportingFrameId,
renderEmbeddables,
renderConfig,
);
} catch (error: any) {
console.error(error);
@ -1505,9 +1501,7 @@ export const renderSceneToSvg = (
files,
element.x + offsetX,
element.y + offsetY,
exportWithDarkMode,
exportingFrameId,
renderEmbeddables,
renderConfig,
);
} catch (error: any) {
console.error(error);