diff --git a/src/components/App.tsx b/src/components/App.tsx index 363091ef..0fe8319e 100644 --- a/src/components/App.tsx +++ b/src/components/App.tsx @@ -166,7 +166,7 @@ import { isAndroid, } from "../keys"; import { distance2d, getGridPoint, isPathALoop } from "../math"; -import { renderSceneThrottled } from "../renderer/renderScene"; +import { renderScene } from "../renderer/renderScene"; import { invalidateShapeForElement } from "../renderer/renderElement"; import { calculateScrollCenter, @@ -286,6 +286,10 @@ let currentScrollBars: ScrollBars = { horizontal: null, vertical: null }; let touchTimeout = 0; let invalidateContextMenu = false; +// remove this hack when we can sync render & resizeObserver (state update) +// to rAF. See #5439 +let THROTTLE_NEXT_RENDER = true; + let lastPointerUp: ((event: any) => void) | null = null; const gesture: Gesture = { pointers: new Map(), @@ -858,6 +862,7 @@ class App extends React.Component { if ("ResizeObserver" in window && this.excalidrawContainerRef?.current) { this.resizeObserver = new ResizeObserver(() => { + THROTTLE_NEXT_RENDER = false; // recompute device dimensions state // --------------------------------------------------------------------- this.refreshDeviceState(this.excalidrawContainerRef.current!); @@ -1221,7 +1226,7 @@ class App extends React.Component { ); }); - renderSceneThrottled( + renderScene( renderingElements, this.state, this.state.selectionElement, @@ -1259,8 +1264,13 @@ class App extends React.Component { this.scheduleImageRefresh(); }, + THROTTLE_NEXT_RENDER && window.EXCALIDRAW_THROTTLE_RENDER === true, ); + if (!THROTTLE_NEXT_RENDER) { + THROTTLE_NEXT_RENDER = true; + } + this.history.record(this.state, this.scene.getElementsIncludingDeleted()); // Do not notify consumers if we're still loading the scene. Among other diff --git a/src/excalidraw-app/index.tsx b/src/excalidraw-app/index.tsx index 8f35772e..1666fe4b 100644 --- a/src/excalidraw-app/index.tsx +++ b/src/excalidraw-app/index.tsx @@ -83,6 +83,8 @@ import { jotaiStore, useAtomWithInitialValue } from "../jotai"; import { reconcileElements } from "./collab/reconciliation"; import { parseLibraryTokensFromUrl, useHandleLibrary } from "../data/library"; +window.EXCALIDRAW_THROTTLE_RENDER = true; + const isExcalidrawPlusSignedUser = document.cookie.includes( COOKIES.AUTH_STATE_COOKIE, ); diff --git a/src/global.d.ts b/src/global.d.ts index 3bff2b5d..266957cf 100644 --- a/src/global.d.ts +++ b/src/global.d.ts @@ -14,6 +14,7 @@ interface Window { __EXCALIDRAW_SHA__: string | undefined; EXCALIDRAW_ASSET_PATH: string | undefined; EXCALIDRAW_EXPORT_SOURCE: string; + EXCALIDRAW_THROTTLE_RENDER: boolean | undefined; gtag: Function; } diff --git a/src/renderer/renderScene.ts b/src/renderer/renderScene.ts index 9df35407..912f0341 100644 --- a/src/renderer/renderScene.ts +++ b/src/renderer/renderScene.ts @@ -181,7 +181,7 @@ const renderLinearPointHandles = ( context.restore(); }; -export const renderScene = ( +export const _renderScene = ( elements: readonly NonDeletedExcalidrawElement[], appState: AppState, selectionElement: NonDeletedExcalidrawElement | null, @@ -572,8 +572,7 @@ export const renderScene = ( return { atLeastOneVisibleElement: visibleElements.length > 0, scrollBars }; }; -/** renderScene throttled to animation framerate */ -export const renderSceneThrottled = throttleRAF( +const renderSceneThrottled = throttleRAF( ( elements: readonly NonDeletedExcalidrawElement[], appState: AppState, @@ -582,9 +581,9 @@ export const renderSceneThrottled = throttleRAF( rc: RoughCanvas, canvas: HTMLCanvasElement, renderConfig: RenderConfig, - callback?: (data: ReturnType) => void, + callback?: (data: ReturnType) => void, ) => { - const ret = renderScene( + const ret = _renderScene( elements, appState, selectionElement, @@ -598,6 +597,46 @@ export const renderSceneThrottled = throttleRAF( { trailing: true }, ); +/** renderScene throttled to animation framerate */ +export const renderScene = ( + elements: readonly NonDeletedExcalidrawElement[], + appState: AppState, + selectionElement: NonDeletedExcalidrawElement | null, + scale: number, + rc: RoughCanvas, + canvas: HTMLCanvasElement, + renderConfig: RenderConfig, + callback?: (data: ReturnType) => void, + /** Whether to throttle rendering. Defaults to false. + * When throttling, no value is returned. Use the callback instead. */ + throttle?: T, +): T extends true ? void : ReturnType => { + if (throttle) { + renderSceneThrottled( + elements, + appState, + selectionElement, + scale, + rc, + canvas, + renderConfig, + callback, + ); + return undefined as T extends true ? void : ReturnType; + } + const ret = _renderScene( + elements, + appState, + selectionElement, + scale, + rc, + canvas, + renderConfig, + ); + callback?.(ret); + return ret as T extends true ? void : ReturnType; +}; + const renderTransformHandles = ( context: CanvasRenderingContext2D, renderConfig: RenderConfig, diff --git a/src/tests/contextmenu.test.tsx b/src/tests/contextmenu.test.tsx index b30d28d4..3e1c7447 100644 --- a/src/tests/contextmenu.test.tsx +++ b/src/tests/contextmenu.test.tsx @@ -39,7 +39,7 @@ const mouse = new Pointer("mouse"); // Unmount ReactDOM from root ReactDOM.unmountComponentAtNode(document.getElementById("root")!); -const renderScene = jest.spyOn(Renderer, "renderSceneThrottled"); +const renderScene = jest.spyOn(Renderer, "renderScene"); beforeEach(() => { localStorage.clear(); renderScene.mockClear(); diff --git a/src/tests/dragCreate.test.tsx b/src/tests/dragCreate.test.tsx index a5a514cb..b39601eb 100644 --- a/src/tests/dragCreate.test.tsx +++ b/src/tests/dragCreate.test.tsx @@ -14,7 +14,7 @@ import { reseed } from "../random"; // Unmount ReactDOM from root ReactDOM.unmountComponentAtNode(document.getElementById("root")!); -const renderScene = jest.spyOn(Renderer, "renderSceneThrottled"); +const renderScene = jest.spyOn(Renderer, "renderScene"); beforeEach(() => { localStorage.clear(); renderScene.mockClear(); diff --git a/src/tests/move.test.tsx b/src/tests/move.test.tsx index 398fd6bd..312b3eec 100644 --- a/src/tests/move.test.tsx +++ b/src/tests/move.test.tsx @@ -16,7 +16,7 @@ import { KEYS } from "../keys"; // Unmount ReactDOM from root ReactDOM.unmountComponentAtNode(document.getElementById("root")!); -const renderScene = jest.spyOn(Renderer, "renderSceneThrottled"); +const renderScene = jest.spyOn(Renderer, "renderScene"); beforeEach(() => { localStorage.clear(); renderScene.mockClear(); diff --git a/src/tests/multiPointCreate.test.tsx b/src/tests/multiPointCreate.test.tsx index 054ffd79..7437a1d4 100644 --- a/src/tests/multiPointCreate.test.tsx +++ b/src/tests/multiPointCreate.test.tsx @@ -14,7 +14,7 @@ import { reseed } from "../random"; // Unmount ReactDOM from root ReactDOM.unmountComponentAtNode(document.getElementById("root")!); -const renderScene = jest.spyOn(Renderer, "renderSceneThrottled"); +const renderScene = jest.spyOn(Renderer, "renderScene"); beforeEach(() => { localStorage.clear(); renderScene.mockClear(); diff --git a/src/tests/regressionTests.test.tsx b/src/tests/regressionTests.test.tsx index 9967281a..3fbd6513 100644 --- a/src/tests/regressionTests.test.tsx +++ b/src/tests/regressionTests.test.tsx @@ -20,7 +20,7 @@ import { t } from "../i18n"; const { h } = window; -const renderScene = jest.spyOn(Renderer, "renderSceneThrottled"); +const renderScene = jest.spyOn(Renderer, "renderScene"); const mouse = new Pointer("mouse"); const finger1 = new Pointer("touch", 1); diff --git a/src/tests/resize.test.tsx b/src/tests/resize.test.tsx index 8bec75a4..4e553825 100644 --- a/src/tests/resize.test.tsx +++ b/src/tests/resize.test.tsx @@ -18,7 +18,7 @@ const mouse = new Pointer("mouse"); // Unmount ReactDOM from root ReactDOM.unmountComponentAtNode(document.getElementById("root")!); -const renderScene = jest.spyOn(Renderer, "renderSceneThrottled"); +const renderScene = jest.spyOn(Renderer, "renderScene"); beforeEach(() => { localStorage.clear(); renderScene.mockClear(); diff --git a/src/tests/selection.test.tsx b/src/tests/selection.test.tsx index bb9b6819..e2bcd1db 100644 --- a/src/tests/selection.test.tsx +++ b/src/tests/selection.test.tsx @@ -16,7 +16,7 @@ import { Keyboard, Pointer } from "./helpers/ui"; // Unmount ReactDOM from root ReactDOM.unmountComponentAtNode(document.getElementById("root")!); -const renderScene = jest.spyOn(Renderer, "renderSceneThrottled"); +const renderScene = jest.spyOn(Renderer, "renderScene"); beforeEach(() => { localStorage.clear(); renderScene.mockClear();