perf: tidy frame bg render pass

This commit is contained in:
Ryan Di
2026-03-18 19:36:41 +11:00
parent a7281de157
commit 2b3871856e
+128 -116
View File
@@ -26,6 +26,7 @@ import { getElementAbsoluteCoords } from "@excalidraw/element";
import type { import type {
ElementsMap, ElementsMap,
ExcalidrawFrameElement,
ExcalidrawFrameLikeElement, ExcalidrawFrameLikeElement,
NonDeletedExcalidrawElement, NonDeletedExcalidrawElement,
} from "@excalidraw/element/types"; } from "@excalidraw/element/types";
@@ -278,39 +279,15 @@ const _renderStaticScene = ({
} }
const groupsToBeAddedToFrame = new Set<string>(); const groupsToBeAddedToFrame = new Set<string>();
const renderedFrameBackgrounds = new Set<string>(); // iframe-like elements are rendered in a separate top-layer pass.
const nonIframeVisibleElements = visibleElements.filter(
const maybeRenderFrameBackground = ( (el) => !isIframeLikeElement(el),
element: NonDeletedExcalidrawElement | ExcalidrawFrameLikeElement, );
) => { // Frame background to render right before a given element id.
if ( const frameBackgroundByElementId = new Map<
!appState.frameRendering.enabled || NonDeletedExcalidrawElement["id"],
(!appState.frameRendering.outline && !renderConfig.exportingFrame) ExcalidrawFrameElement
) { >();
return;
}
const frame =
renderConfig.exportingFrame ||
(isFrameLikeElement(element)
? element
: getContainingFrame(element, elementsMap));
if (!isFrameElement(frame)) {
return;
}
if (!frame || renderedFrameBackgrounds.has(frame.id)) {
return;
}
renderFrameBackground(frame, context, appState, {
roundCorners:
!renderConfig.exportingFrame ||
frame.id !== renderConfig.exportingFrame.id,
});
renderedFrameBackgrounds.add(frame.id);
};
visibleElements.forEach((element) => { visibleElements.forEach((element) => {
if ( if (
@@ -332,100 +309,135 @@ const _renderStaticScene = ({
const inFrameGroupsMap = new Map<string, boolean>(); const inFrameGroupsMap = new Map<string, boolean>();
if (renderConfig.exportingFrame) { if (
maybeRenderFrameBackground(renderConfig.exportingFrame); appState.frameRendering.enabled &&
(appState.frameRendering.outline || renderConfig.exportingFrame)
) {
// Precompute where each frame background should be emitted to avoid
// re-resolving containing frames during the paint loop.
const renderedFrameBackgrounds = new Set<string>();
if (
renderConfig.exportingFrame &&
isFrameElement(renderConfig.exportingFrame)
) {
renderFrameBackground(renderConfig.exportingFrame, context, appState, {
roundCorners: false,
});
renderedFrameBackgrounds.add(renderConfig.exportingFrame.id);
}
const maybeQueueFrameBackground = (
element: NonDeletedExcalidrawElement | ExcalidrawFrameLikeElement,
) => {
const frame = isFrameLikeElement(element)
? element
: getContainingFrame(element, elementsMap);
if (!isFrameElement(frame) || renderedFrameBackgrounds.has(frame.id)) {
return;
}
frameBackgroundByElementId.set(element.id, frame);
renderedFrameBackgrounds.add(frame.id);
};
nonIframeVisibleElements.forEach((element) => {
maybeQueueFrameBackground(element);
});
} }
// Paint visible elements // Paint visible elements
visibleElements nonIframeVisibleElements.forEach((element) => {
.filter((el) => !isIframeLikeElement(el)) try {
.forEach((element) => { const frameBackground = frameBackgroundByElementId.get(element.id);
try { if (frameBackground) {
// TODO: optimize (currently we call this func for each element because renderFrameBackground(frameBackground, context, appState, {
// children come before their frames and we neeed to render the frame roundCorners:
// background at the bottom) !renderConfig.exportingFrame ||
maybeRenderFrameBackground(element); frameBackground.id !== renderConfig.exportingFrame.id,
const frameId = element.frameId || appState.frameToHighlight?.id; });
}
const frameId = element.frameId || appState.frameToHighlight?.id;
if (
isTextElement(element) &&
element.containerId &&
elementsMap.has(element.containerId)
) {
// will be rendered with the container
return;
}
context.save();
if (
frameId &&
appState.frameRendering.enabled &&
appState.frameRendering.clip
) {
const frame = getTargetFrame(element, elementsMap, appState);
if ( if (
isTextElement(element) && frame &&
element.containerId && shouldApplyFrameClip(
elementsMap.has(element.containerId)
) {
// will be rendered with the container
return;
}
context.save();
if (
frameId &&
appState.frameRendering.enabled &&
appState.frameRendering.clip
) {
const frame = getTargetFrame(element, elementsMap, appState);
if (
frame &&
shouldApplyFrameClip(
element,
frame,
appState,
elementsMap,
inFrameGroupsMap,
)
) {
frameClip(frame, context, renderConfig, appState);
}
renderElement(
element, element,
elementsMap, frame,
allElementsMap,
rc,
context,
renderConfig,
appState, appState,
);
} else {
renderElement(
element,
elementsMap, elementsMap,
allElementsMap, inFrameGroupsMap,
rc, )
context, ) {
renderConfig, frameClip(frame, context, renderConfig, appState);
appState,
);
} }
renderElement(
const boundTextElement = getBoundTextElement(element, elementsMap); element,
if (boundTextElement) { elementsMap,
renderElement( allElementsMap,
boundTextElement, rc,
elementsMap, context,
allElementsMap, renderConfig,
rc, appState,
context, );
renderConfig, } else {
appState, renderElement(
); element,
} elementsMap,
allElementsMap,
context.restore(); rc,
context,
if (!isExporting) { renderConfig,
renderLinkIcon(element, context, appState, elementsMap); appState,
}
} catch (error: any) {
console.error(
error,
element.id,
element.x,
element.y,
element.width,
element.height,
); );
} }
});
const boundTextElement = getBoundTextElement(element, elementsMap);
if (boundTextElement) {
renderElement(
boundTextElement,
elementsMap,
allElementsMap,
rc,
context,
renderConfig,
appState,
);
}
context.restore();
if (!isExporting) {
renderLinkIcon(element, context, appState, elementsMap);
}
} catch (error: any) {
console.error(
error,
element.id,
element.x,
element.y,
element.width,
element.height,
);
}
});
// render embeddables on top // render embeddables on top
visibleElements visibleElements