feat: throttle scene rendering to animation framerate (#5422)
This commit is contained in:
parent
c725f84334
commit
b6bb74d08d
@ -166,7 +166,7 @@ import {
|
|||||||
isAndroid,
|
isAndroid,
|
||||||
} from "../keys";
|
} from "../keys";
|
||||||
import { distance2d, getGridPoint, isPathALoop } from "../math";
|
import { distance2d, getGridPoint, isPathALoop } from "../math";
|
||||||
import { renderScene } from "../renderer";
|
import { renderSceneThrottled } from "../renderer/renderScene";
|
||||||
import { invalidateShapeForElement } from "../renderer/renderElement";
|
import { invalidateShapeForElement } from "../renderer/renderElement";
|
||||||
import {
|
import {
|
||||||
calculateScrollCenter,
|
calculateScrollCenter,
|
||||||
@ -1193,7 +1193,8 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
element.id !== this.state.editingElement.id
|
element.id !== this.state.editingElement.id
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
const { atLeastOneVisibleElement, scrollBars } = renderScene(
|
|
||||||
|
renderSceneThrottled(
|
||||||
renderingElements,
|
renderingElements,
|
||||||
this.state,
|
this.state,
|
||||||
this.state.selectionElement,
|
this.state.selectionElement,
|
||||||
@ -1216,8 +1217,7 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
isExporting: false,
|
isExporting: false,
|
||||||
renderScrollbars: !this.device.isMobile,
|
renderScrollbars: !this.device.isMobile,
|
||||||
},
|
},
|
||||||
);
|
({ atLeastOneVisibleElement, scrollBars }) => {
|
||||||
|
|
||||||
if (scrollBars) {
|
if (scrollBars) {
|
||||||
currentScrollBars = scrollBars;
|
currentScrollBars = scrollBars;
|
||||||
}
|
}
|
||||||
@ -1230,9 +1230,11 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
this.setState({ scrolledOutside });
|
this.setState({ scrolledOutside });
|
||||||
}
|
}
|
||||||
|
|
||||||
this.history.record(this.state, this.scene.getElementsIncludingDeleted());
|
|
||||||
|
|
||||||
this.scheduleImageRefresh();
|
this.scheduleImageRefresh();
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
this.history.record(this.state, this.scene.getElementsIncludingDeleted());
|
||||||
|
|
||||||
// Do not notify consumers if we're still loading the scene. Among other
|
// Do not notify consumers if we're still loading the scene. Among other
|
||||||
// potential issues, this fixes a case where the tab isn't focused during
|
// potential issues, this fixes a case where the tab isn't focused during
|
||||||
|
@ -1 +0,0 @@
|
|||||||
export { renderScene } from "./renderScene";
|
|
@ -47,7 +47,11 @@ import {
|
|||||||
TransformHandles,
|
TransformHandles,
|
||||||
TransformHandleType,
|
TransformHandleType,
|
||||||
} from "../element/transformHandles";
|
} from "../element/transformHandles";
|
||||||
import { viewportCoordsToSceneCoords, supportsEmoji } from "../utils";
|
import {
|
||||||
|
viewportCoordsToSceneCoords,
|
||||||
|
supportsEmoji,
|
||||||
|
throttleRAF,
|
||||||
|
} from "../utils";
|
||||||
import { UserIdleState } from "../types";
|
import { UserIdleState } from "../types";
|
||||||
import { THEME_FILTER } from "../constants";
|
import { THEME_FILTER } from "../constants";
|
||||||
import {
|
import {
|
||||||
@ -568,6 +572,32 @@ export const renderScene = (
|
|||||||
return { atLeastOneVisibleElement: visibleElements.length > 0, scrollBars };
|
return { atLeastOneVisibleElement: visibleElements.length > 0, scrollBars };
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/** renderScene throttled to animation framerate */
|
||||||
|
export const renderSceneThrottled = throttleRAF(
|
||||||
|
(
|
||||||
|
elements: readonly NonDeletedExcalidrawElement[],
|
||||||
|
appState: AppState,
|
||||||
|
selectionElement: NonDeletedExcalidrawElement | null,
|
||||||
|
scale: number,
|
||||||
|
rc: RoughCanvas,
|
||||||
|
canvas: HTMLCanvasElement,
|
||||||
|
renderConfig: RenderConfig,
|
||||||
|
callback?: (data: ReturnType<typeof renderScene>) => void,
|
||||||
|
) => {
|
||||||
|
const ret = renderScene(
|
||||||
|
elements,
|
||||||
|
appState,
|
||||||
|
selectionElement,
|
||||||
|
scale,
|
||||||
|
rc,
|
||||||
|
canvas,
|
||||||
|
renderConfig,
|
||||||
|
);
|
||||||
|
callback?.(ret);
|
||||||
|
},
|
||||||
|
{ trailing: true },
|
||||||
|
);
|
||||||
|
|
||||||
const renderTransformHandles = (
|
const renderTransformHandles = (
|
||||||
context: CanvasRenderingContext2D,
|
context: CanvasRenderingContext2D,
|
||||||
renderConfig: RenderConfig,
|
renderConfig: RenderConfig,
|
||||||
|
@ -39,7 +39,7 @@ const mouse = new Pointer("mouse");
|
|||||||
// Unmount ReactDOM from root
|
// Unmount ReactDOM from root
|
||||||
ReactDOM.unmountComponentAtNode(document.getElementById("root")!);
|
ReactDOM.unmountComponentAtNode(document.getElementById("root")!);
|
||||||
|
|
||||||
const renderScene = jest.spyOn(Renderer, "renderScene");
|
const renderScene = jest.spyOn(Renderer, "renderSceneThrottled");
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
localStorage.clear();
|
localStorage.clear();
|
||||||
renderScene.mockClear();
|
renderScene.mockClear();
|
||||||
|
@ -14,7 +14,7 @@ import { reseed } from "../random";
|
|||||||
// Unmount ReactDOM from root
|
// Unmount ReactDOM from root
|
||||||
ReactDOM.unmountComponentAtNode(document.getElementById("root")!);
|
ReactDOM.unmountComponentAtNode(document.getElementById("root")!);
|
||||||
|
|
||||||
const renderScene = jest.spyOn(Renderer, "renderScene");
|
const renderScene = jest.spyOn(Renderer, "renderSceneThrottled");
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
localStorage.clear();
|
localStorage.clear();
|
||||||
renderScene.mockClear();
|
renderScene.mockClear();
|
||||||
|
@ -16,7 +16,7 @@ import { KEYS } from "../keys";
|
|||||||
// Unmount ReactDOM from root
|
// Unmount ReactDOM from root
|
||||||
ReactDOM.unmountComponentAtNode(document.getElementById("root")!);
|
ReactDOM.unmountComponentAtNode(document.getElementById("root")!);
|
||||||
|
|
||||||
const renderScene = jest.spyOn(Renderer, "renderScene");
|
const renderScene = jest.spyOn(Renderer, "renderSceneThrottled");
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
localStorage.clear();
|
localStorage.clear();
|
||||||
renderScene.mockClear();
|
renderScene.mockClear();
|
||||||
|
@ -14,7 +14,7 @@ import { reseed } from "../random";
|
|||||||
// Unmount ReactDOM from root
|
// Unmount ReactDOM from root
|
||||||
ReactDOM.unmountComponentAtNode(document.getElementById("root")!);
|
ReactDOM.unmountComponentAtNode(document.getElementById("root")!);
|
||||||
|
|
||||||
const renderScene = jest.spyOn(Renderer, "renderScene");
|
const renderScene = jest.spyOn(Renderer, "renderSceneThrottled");
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
localStorage.clear();
|
localStorage.clear();
|
||||||
renderScene.mockClear();
|
renderScene.mockClear();
|
||||||
|
@ -20,7 +20,7 @@ import { t } from "../i18n";
|
|||||||
|
|
||||||
const { h } = window;
|
const { h } = window;
|
||||||
|
|
||||||
const renderScene = jest.spyOn(Renderer, "renderScene");
|
const renderScene = jest.spyOn(Renderer, "renderSceneThrottled");
|
||||||
|
|
||||||
const mouse = new Pointer("mouse");
|
const mouse = new Pointer("mouse");
|
||||||
const finger1 = new Pointer("touch", 1);
|
const finger1 = new Pointer("touch", 1);
|
||||||
|
@ -18,7 +18,7 @@ const mouse = new Pointer("mouse");
|
|||||||
// Unmount ReactDOM from root
|
// Unmount ReactDOM from root
|
||||||
ReactDOM.unmountComponentAtNode(document.getElementById("root")!);
|
ReactDOM.unmountComponentAtNode(document.getElementById("root")!);
|
||||||
|
|
||||||
const renderScene = jest.spyOn(Renderer, "renderScene");
|
const renderScene = jest.spyOn(Renderer, "renderSceneThrottled");
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
localStorage.clear();
|
localStorage.clear();
|
||||||
renderScene.mockClear();
|
renderScene.mockClear();
|
||||||
|
@ -16,7 +16,7 @@ import { Keyboard, Pointer } from "./helpers/ui";
|
|||||||
// Unmount ReactDOM from root
|
// Unmount ReactDOM from root
|
||||||
ReactDOM.unmountComponentAtNode(document.getElementById("root")!);
|
ReactDOM.unmountComponentAtNode(document.getElementById("root")!);
|
||||||
|
|
||||||
const renderScene = jest.spyOn(Renderer, "renderScene");
|
const renderScene = jest.spyOn(Renderer, "renderSceneThrottled");
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
localStorage.clear();
|
localStorage.clear();
|
||||||
renderScene.mockClear();
|
renderScene.mockClear();
|
||||||
|
59
src/utils.ts
59
src/utils.ts
@ -126,47 +126,54 @@ export const debounce = <T extends any[]>(
|
|||||||
};
|
};
|
||||||
|
|
||||||
// throttle callback to execute once per animation frame
|
// throttle callback to execute once per animation frame
|
||||||
export const throttleRAF = <T extends any[]>(fn: (...args: T) => void) => {
|
export const throttleRAF = <T extends any[]>(
|
||||||
let handle: number | null = null;
|
fn: (...args: T) => void,
|
||||||
|
opts?: { trailing?: boolean },
|
||||||
|
) => {
|
||||||
|
let timerId: number | null = null;
|
||||||
let lastArgs: T | null = null;
|
let lastArgs: T | null = null;
|
||||||
let callback: ((...args: T) => void) | null = null;
|
let lastArgsTrailing: T | null = null;
|
||||||
|
|
||||||
|
const scheduleFunc = (args: T) => {
|
||||||
|
timerId = window.requestAnimationFrame(() => {
|
||||||
|
timerId = null;
|
||||||
|
fn(...args);
|
||||||
|
lastArgs = null;
|
||||||
|
if (lastArgsTrailing) {
|
||||||
|
lastArgs = lastArgsTrailing;
|
||||||
|
lastArgsTrailing = null;
|
||||||
|
scheduleFunc(lastArgs);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
const ret = (...args: T) => {
|
const ret = (...args: T) => {
|
||||||
if (process.env.NODE_ENV === "test") {
|
if (process.env.NODE_ENV === "test") {
|
||||||
fn(...args);
|
fn(...args);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
lastArgs = args;
|
lastArgs = args;
|
||||||
callback = fn;
|
if (timerId === null) {
|
||||||
if (handle === null) {
|
scheduleFunc(lastArgs);
|
||||||
handle = window.requestAnimationFrame(() => {
|
} else if (opts?.trailing) {
|
||||||
handle = null;
|
lastArgsTrailing = args;
|
||||||
lastArgs = null;
|
|
||||||
callback = null;
|
|
||||||
fn(...args);
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
ret.flush = () => {
|
ret.flush = () => {
|
||||||
if (handle !== null) {
|
if (timerId !== null) {
|
||||||
cancelAnimationFrame(handle);
|
cancelAnimationFrame(timerId);
|
||||||
handle = null;
|
timerId = null;
|
||||||
}
|
}
|
||||||
if (lastArgs) {
|
if (lastArgs) {
|
||||||
const _lastArgs = lastArgs;
|
fn(...(lastArgsTrailing || lastArgs));
|
||||||
const _callback = callback;
|
lastArgs = lastArgsTrailing = null;
|
||||||
lastArgs = null;
|
|
||||||
callback = null;
|
|
||||||
if (_callback !== null) {
|
|
||||||
_callback(..._lastArgs);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
ret.cancel = () => {
|
ret.cancel = () => {
|
||||||
lastArgs = null;
|
lastArgs = lastArgsTrailing = null;
|
||||||
callback = null;
|
if (timerId !== null) {
|
||||||
if (handle !== null) {
|
cancelAnimationFrame(timerId);
|
||||||
cancelAnimationFrame(handle);
|
timerId = null;
|
||||||
handle = null;
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
return ret;
|
return ret;
|
||||||
|
Loading…
x
Reference in New Issue
Block a user