From a30e46b7560027794a6bf8709b6909adc621388d Mon Sep 17 00:00:00 2001 From: Ryan Di Date: Fri, 10 Nov 2023 19:24:32 +0800 Subject: [PATCH] batch clipping --- src/frame.ts | 3 +- src/renderer/renderScene.ts | 139 ++++++++++++++++++++---------------- 2 files changed, 78 insertions(+), 64 deletions(-) diff --git a/src/frame.ts b/src/frame.ts index eda384f65..3cbaf5439 100644 --- a/src/frame.ts +++ b/src/frame.ts @@ -593,8 +593,9 @@ export const isElementInFrame = ( element: ExcalidrawElement, allElements: ExcalidrawElementsIncludingDeleted, appState: StaticCanvasAppState, + targetFrame?: ExcalidrawFrameElement, ) => { - const frame = getTargetFrame(element, appState); + const frame = targetFrame ?? getTargetFrame(element, appState); const _element = isTextElement(element) ? getContainerElement(element) || element : element; diff --git a/src/renderer/renderScene.ts b/src/renderer/renderScene.ts index 330747024..524d33844 100644 --- a/src/renderer/renderScene.ts +++ b/src/renderer/renderScene.ts @@ -357,6 +357,36 @@ const renderLinearElementPointHighlight = ( context.restore(); }; +const getContiguousElements = ( + elements: readonly ExcalidrawElement[], + appState: StaticCanvasAppState, +) => { + const contiguousElementsArray: ExcalidrawElement[][] = []; + + let previousFrameId: string | null | undefined = null; + const contiguousElements: ExcalidrawElement[] = []; + for (const element of elements) { + const frameId = element.frameId || appState.frameToHighlight?.id; + + if (previousFrameId !== frameId) { + if (contiguousElements.length > 0) { + contiguousElementsArray.push([...contiguousElements]); + contiguousElements.length = 0; + } + + previousFrameId = frameId; + } + + contiguousElements.push(element); + } + + if (contiguousElements.length > 0) { + contiguousElementsArray.push([...contiguousElements]); + } + + return contiguousElementsArray; +}; + const frameClip = ( frame: ExcalidrawFrameElement, context: CanvasRenderingContext2D, @@ -941,86 +971,69 @@ const _renderStaticScene = ({ ); } - // Paint visible elements - visibleElements - .filter((el) => !isEmbeddableOrLabel(el)) - .forEach((element) => { + // Paint visible elements with embeddables on top + const visibleNonEmbeddableOrLabelElements = visibleElements.filter( + (el) => !isEmbeddableOrLabel(el), + ); + + const visibleEmbeddableOrLabelElements = visibleElements.filter((el) => + isEmbeddableOrLabel(el), + ); + + const contiguousElementsArray = [ + ...getContiguousElements(visibleNonEmbeddableOrLabelElements, appState), + ...getContiguousElements(visibleEmbeddableOrLabelElements, appState), + ]; + + const renderContiguousElements = ( + contiguousElements: ExcalidrawElement[], + ) => { + for (const element of contiguousElements) { try { - const frameId = element.frameId || appState.frameToHighlight?.id; + renderElement(element, rc, context, renderConfig, appState); if ( - frameId && - appState.frameRendering.enabled && - appState.frameRendering.clip + isEmbeddableElement(element) && + (isExporting || !element.validated) && + element.width && + element.height ) { - context.save(); - - const frame = getTargetFrame(element, appState); - - // TODO do we need to check isElementInFrame here? - if (frame && isElementInFrame(element, elements, appState)) { - frameClip(frame, context, renderConfig, appState); - } - renderElement(element, rc, context, renderConfig, appState); - context.restore(); - } else { - renderElement(element, rc, context, renderConfig, appState); + const label = createPlaceholderEmbeddableLabel(element); + renderElement(label, rc, context, renderConfig, appState); } + if (!isExporting) { renderLinkIcon(element, context, appState); } } catch (error: any) { console.error(error); } - }); + } + }; - // render embeddables on top - visibleElements - .filter((el) => isEmbeddableOrLabel(el)) - .forEach((element) => { - try { - const render = () => { - renderElement(element, rc, context, renderConfig, appState); + for (const contiguousElements of contiguousElementsArray) { + const firstElement = contiguousElements[0]; - if ( - isEmbeddableElement(element) && - (isExporting || !element.validated) && - element.width && - element.height - ) { - const label = createPlaceholderEmbeddableLabel(element); - renderElement(label, rc, context, renderConfig, appState); - } - if (!isExporting) { - renderLinkIcon(element, context, appState); - } - }; - // - 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 (firstElement) { + context.save(); + const frameId = firstElement.frameId || appState.frameToHighlight?.id; - if ( - frameId && - appState.frameRendering.enabled && - appState.frameRendering.clip - ) { - context.save(); + if ( + frameId && + appState.frameRendering.enabled && + appState.frameRendering.clip + ) { + const frame = getTargetFrame(firstElement, appState); - const frame = getTargetFrame(element, appState); - - if (frame && isElementInFrame(element, elements, appState)) { - frameClip(frame, context, renderConfig, appState); - } - render(); - context.restore(); - } else { - render(); + if (frame && isElementInFrame(firstElement, elements, appState)) { + frameClip(frame, context, renderConfig, appState); } - } catch (error: any) { - console.error(error); } - }); + + renderContiguousElements(contiguousElements); + context.restore(); + } + } }; /** throttled to animation framerate */