feat: throttle pointermove
events per framerate (#4727)
This commit is contained in:
parent
96de887cc8
commit
8aff076782
@ -212,6 +212,7 @@ import {
|
|||||||
tupleToCoors,
|
tupleToCoors,
|
||||||
viewportCoordsToSceneCoords,
|
viewportCoordsToSceneCoords,
|
||||||
withBatchedUpdates,
|
withBatchedUpdates,
|
||||||
|
withBatchedUpdatesThrottled,
|
||||||
} from "../utils";
|
} from "../utils";
|
||||||
import ContextMenu, { ContextMenuOption } from "./ContextMenu";
|
import ContextMenu, { ContextMenuOption } from "./ContextMenu";
|
||||||
import LayerUI from "./LayerUI";
|
import LayerUI from "./LayerUI";
|
||||||
@ -2921,7 +2922,7 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
|
|
||||||
setCursor(this.canvas, CURSOR_TYPE.GRABBING);
|
setCursor(this.canvas, CURSOR_TYPE.GRABBING);
|
||||||
let { clientX: lastX, clientY: lastY } = event;
|
let { clientX: lastX, clientY: lastY } = event;
|
||||||
const onPointerMove = withBatchedUpdates((event: PointerEvent) => {
|
const onPointerMove = withBatchedUpdatesThrottled((event: PointerEvent) => {
|
||||||
const deltaX = lastX - event.clientX;
|
const deltaX = lastX - event.clientX;
|
||||||
const deltaY = lastY - event.clientY;
|
const deltaY = lastY - event.clientY;
|
||||||
lastX = event.clientX;
|
lastX = event.clientX;
|
||||||
@ -2984,6 +2985,7 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
window.removeEventListener(EVENT.POINTER_MOVE, onPointerMove);
|
window.removeEventListener(EVENT.POINTER_MOVE, onPointerMove);
|
||||||
window.removeEventListener(EVENT.POINTER_UP, teardown);
|
window.removeEventListener(EVENT.POINTER_UP, teardown);
|
||||||
window.removeEventListener(EVENT.BLUR, teardown);
|
window.removeEventListener(EVENT.BLUR, teardown);
|
||||||
|
onPointerMove.flush();
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
window.addEventListener(EVENT.BLUR, teardown);
|
window.addEventListener(EVENT.BLUR, teardown);
|
||||||
@ -3086,7 +3088,7 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
isDraggingScrollBar = true;
|
isDraggingScrollBar = true;
|
||||||
pointerDownState.lastCoords.x = event.clientX;
|
pointerDownState.lastCoords.x = event.clientX;
|
||||||
pointerDownState.lastCoords.y = event.clientY;
|
pointerDownState.lastCoords.y = event.clientY;
|
||||||
const onPointerMove = withBatchedUpdates((event: PointerEvent) => {
|
const onPointerMove = withBatchedUpdatesThrottled((event: PointerEvent) => {
|
||||||
const target = event.target;
|
const target = event.target;
|
||||||
if (!(target instanceof HTMLElement)) {
|
if (!(target instanceof HTMLElement)) {
|
||||||
return;
|
return;
|
||||||
@ -3105,6 +3107,7 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
this.savePointer(event.clientX, event.clientY, "up");
|
this.savePointer(event.clientX, event.clientY, "up");
|
||||||
window.removeEventListener(EVENT.POINTER_MOVE, onPointerMove);
|
window.removeEventListener(EVENT.POINTER_MOVE, onPointerMove);
|
||||||
window.removeEventListener(EVENT.POINTER_UP, onPointerUp);
|
window.removeEventListener(EVENT.POINTER_UP, onPointerUp);
|
||||||
|
onPointerMove.flush();
|
||||||
});
|
});
|
||||||
|
|
||||||
lastPointerUp = onPointerUp;
|
lastPointerUp = onPointerUp;
|
||||||
@ -3640,8 +3643,8 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
|
|
||||||
private onPointerMoveFromPointerDownHandler(
|
private onPointerMoveFromPointerDownHandler(
|
||||||
pointerDownState: PointerDownState,
|
pointerDownState: PointerDownState,
|
||||||
): (event: PointerEvent) => void {
|
) {
|
||||||
return withBatchedUpdates((event: PointerEvent) => {
|
return withBatchedUpdatesThrottled((event: PointerEvent) => {
|
||||||
// We need to initialize dragOffsetXY only after we've updated
|
// We need to initialize dragOffsetXY only after we've updated
|
||||||
// `state.selectedElementIds` on pointerDown. Doing it here in pointerMove
|
// `state.selectedElementIds` on pointerDown. Doing it here in pointerMove
|
||||||
// event handler should hopefully ensure we're already working with
|
// event handler should hopefully ensure we're already working with
|
||||||
@ -4062,6 +4065,10 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
|
|
||||||
lastPointerUp = null;
|
lastPointerUp = null;
|
||||||
|
|
||||||
|
if (pointerDownState.eventListeners.onMove) {
|
||||||
|
pointerDownState.eventListeners.onMove.flush();
|
||||||
|
}
|
||||||
|
|
||||||
window.removeEventListener(
|
window.removeEventListener(
|
||||||
EVENT.POINTER_MOVE,
|
EVENT.POINTER_MOVE,
|
||||||
pointerDownState.eventListeners.onMove!,
|
pointerDownState.eventListeners.onMove!,
|
||||||
|
@ -20,7 +20,7 @@ import { LinearElementEditor } from "./element/linearElementEditor";
|
|||||||
import { SuggestedBinding } from "./element/binding";
|
import { SuggestedBinding } from "./element/binding";
|
||||||
import { ImportedDataState } from "./data/types";
|
import { ImportedDataState } from "./data/types";
|
||||||
import type App from "./components/App";
|
import type App from "./components/App";
|
||||||
import type { ResolvablePromise } from "./utils";
|
import type { ResolvablePromise, throttleRAF } from "./utils";
|
||||||
import { Spreadsheet } from "./charts";
|
import { Spreadsheet } from "./charts";
|
||||||
import { Language } from "./i18n";
|
import { Language } from "./i18n";
|
||||||
import { ClipboardData } from "./clipboard";
|
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
|
// We need to have these in the state so that we can unsubscribe them
|
||||||
eventListeners: {
|
eventListeners: {
|
||||||
// It's defined on the initial pointer down event
|
// 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
|
// It's defined on the initial pointer down event
|
||||||
onUp: null | ((event: PointerEvent) => void);
|
onUp: null | ((event: PointerEvent) => void);
|
||||||
// It's defined on the initial pointer down event
|
// It's defined on the initial pointer down event
|
||||||
|
62
src/utils.ts
62
src/utils.ts
@ -119,6 +119,53 @@ export const debounce = <T extends any[]>(
|
|||||||
return ret;
|
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
|
// https://github.com/lodash/lodash/blob/es/chunk.js
|
||||||
export const chunk = <T extends any>(
|
export const chunk = <T extends any>(
|
||||||
array: readonly T[],
|
array: readonly T[],
|
||||||
@ -356,6 +403,21 @@ export const withBatchedUpdates = <
|
|||||||
unstable_batchedUpdates(func as TFunction, event);
|
unstable_batchedUpdates(func as TFunction, event);
|
||||||
}) as TFunction;
|
}) 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
|
//https://stackoverflow.com/a/9462382/8418
|
||||||
export const nFormatter = (num: number, digits: number): string => {
|
export const nFormatter = (num: number, digits: number): string => {
|
||||||
const si = [
|
const si = [
|
||||||
|
Loading…
x
Reference in New Issue
Block a user