feat: throttle pointermove events per framerate (#4727)

This commit is contained in:
David Luzar 2022-02-06 17:45:37 +01:00 committed by GitHub
parent 96de887cc8
commit 8aff076782
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 75 additions and 6 deletions

View File

@ -212,6 +212,7 @@ import {
tupleToCoors,
viewportCoordsToSceneCoords,
withBatchedUpdates,
withBatchedUpdatesThrottled,
} from "../utils";
import ContextMenu, { ContextMenuOption } from "./ContextMenu";
import LayerUI from "./LayerUI";
@ -2921,7 +2922,7 @@ class App extends React.Component<AppProps, AppState> {
setCursor(this.canvas, CURSOR_TYPE.GRABBING);
let { clientX: lastX, clientY: lastY } = event;
const onPointerMove = withBatchedUpdates((event: PointerEvent) => {
const onPointerMove = withBatchedUpdatesThrottled((event: PointerEvent) => {
const deltaX = lastX - event.clientX;
const deltaY = lastY - event.clientY;
lastX = event.clientX;
@ -2984,6 +2985,7 @@ class App extends React.Component<AppProps, AppState> {
window.removeEventListener(EVENT.POINTER_MOVE, onPointerMove);
window.removeEventListener(EVENT.POINTER_UP, teardown);
window.removeEventListener(EVENT.BLUR, teardown);
onPointerMove.flush();
}),
);
window.addEventListener(EVENT.BLUR, teardown);
@ -3086,7 +3088,7 @@ class App extends React.Component<AppProps, AppState> {
isDraggingScrollBar = true;
pointerDownState.lastCoords.x = event.clientX;
pointerDownState.lastCoords.y = event.clientY;
const onPointerMove = withBatchedUpdates((event: PointerEvent) => {
const onPointerMove = withBatchedUpdatesThrottled((event: PointerEvent) => {
const target = event.target;
if (!(target instanceof HTMLElement)) {
return;
@ -3105,6 +3107,7 @@ class App extends React.Component<AppProps, AppState> {
this.savePointer(event.clientX, event.clientY, "up");
window.removeEventListener(EVENT.POINTER_MOVE, onPointerMove);
window.removeEventListener(EVENT.POINTER_UP, onPointerUp);
onPointerMove.flush();
});
lastPointerUp = onPointerUp;
@ -3640,8 +3643,8 @@ class App extends React.Component<AppProps, AppState> {
private onPointerMoveFromPointerDownHandler(
pointerDownState: PointerDownState,
): (event: PointerEvent) => void {
return withBatchedUpdates((event: PointerEvent) => {
) {
return withBatchedUpdatesThrottled((event: PointerEvent) => {
// We need to initialize dragOffsetXY only after we've updated
// `state.selectedElementIds` on pointerDown. Doing it here in pointerMove
// event handler should hopefully ensure we're already working with
@ -4062,6 +4065,10 @@ class App extends React.Component<AppProps, AppState> {
lastPointerUp = null;
if (pointerDownState.eventListeners.onMove) {
pointerDownState.eventListeners.onMove.flush();
}
window.removeEventListener(
EVENT.POINTER_MOVE,
pointerDownState.eventListeners.onMove!,

View File

@ -20,7 +20,7 @@ import { LinearElementEditor } from "./element/linearElementEditor";
import { SuggestedBinding } from "./element/binding";
import { ImportedDataState } from "./data/types";
import type App from "./components/App";
import type { ResolvablePromise } from "./utils";
import type { ResolvablePromise, throttleRAF } from "./utils";
import { Spreadsheet } from "./charts";
import { Language } from "./i18n";
import { ClipboardData } from "./clipboard";
@ -367,7 +367,7 @@ export type PointerDownState = Readonly<{
// We need to have these in the state so that we can unsubscribe them
eventListeners: {
// It's defined on the initial pointer down event
onMove: null | ((event: PointerEvent) => void);
onMove: null | ReturnType<typeof throttleRAF>;
// It's defined on the initial pointer down event
onUp: null | ((event: PointerEvent) => void);
// It's defined on the initial pointer down event

View File

@ -119,6 +119,53 @@ export const debounce = <T extends any[]>(
return ret;
};
// throttle callback to execute once per animation frame
export const throttleRAF = <T extends any[]>(fn: (...args: T) => void) => {
let handle: number | null = null;
let lastArgs: T | null = null;
let callback: ((...args: T) => void) | null = null;
const ret = (...args: T) => {
if (process.env.NODE_ENV === "test") {
fn(...args);
return;
}
lastArgs = args;
callback = fn;
if (handle === null) {
handle = window.requestAnimationFrame(() => {
handle = null;
lastArgs = null;
callback = null;
fn(...args);
});
}
};
ret.flush = () => {
if (handle !== null) {
cancelAnimationFrame(handle);
handle = null;
}
if (lastArgs) {
const _lastArgs = lastArgs;
const _callback = callback;
lastArgs = null;
callback = null;
if (_callback !== null) {
_callback(..._lastArgs);
}
}
};
ret.cancel = () => {
lastArgs = null;
callback = null;
if (handle !== null) {
cancelAnimationFrame(handle);
handle = null;
}
};
return ret;
};
// https://github.com/lodash/lodash/blob/es/chunk.js
export const chunk = <T extends any>(
array: readonly T[],
@ -356,6 +403,21 @@ export const withBatchedUpdates = <
unstable_batchedUpdates(func as TFunction, event);
}) as TFunction;
/**
* barches React state updates and throttles the calls to a single call per
* animation frame
*/
export const withBatchedUpdatesThrottled = <
TFunction extends ((event: any) => void) | (() => void),
>(
func: Parameters<TFunction>["length"] extends 0 | 1 ? TFunction : never,
) => {
// @ts-ignore
return throttleRAF<Parameters<TFunction>>(((event) => {
unstable_batchedUpdates(func, event);
}) as TFunction);
};
//https://stackoverflow.com/a/9462382/8418
export const nFormatter = (num: number, digits: number): string => {
const si = [