Files
excalidraw/packages/excalidraw/renderer/animation.ts
T
Márk Tolmács b42b1a193d fix(editor): excessive battery usage (#11377)
* fix: Excessive battery usage

* chore: Refactor Eraser, Lasso and Laser pointer to use AnimationController

* fix: Last laser trail element is not removed from SVG
---------

Signed-off-by: Mark Tolmacs <mark@lazycat.hu>
Co-authored-by: dwelle <5153846+dwelle@users.noreply.github.com>
2026-05-24 16:12:17 +02:00

127 lines
3.0 KiB
TypeScript

import { isRenderThrottlingEnabled } from "../reactUtils";
export type Animation<R extends object> = (params: {
deltaTime: number;
state?: R;
}) => R | null | undefined;
export class AnimationController {
private static scheduledFrame:
| { id: ReturnType<typeof requestAnimationFrame>; type: "raf" }
| { id: ReturnType<typeof setTimeout>; type: "timeout" }
| null = null;
private static animations = new Map<
string,
{
animation: Animation<any>;
lastTime: number;
state: any;
}
>();
static start<R extends object>(key: string, animation: Animation<R>) {
if (AnimationController.animations.has(key)) {
return;
}
const initialState = animation({
deltaTime: 0,
state: undefined,
});
if (initialState) {
AnimationController.animations.set(key, {
animation,
lastTime: 0,
state: initialState,
});
AnimationController.scheduleNextFrame();
}
}
private static scheduleNextFrame() {
if (AnimationController.scheduledFrame) {
return;
}
if (isRenderThrottlingEnabled()) {
AnimationController.scheduledFrame = {
id: requestAnimationFrame(AnimationController.tick),
type: "raf",
};
} else {
AnimationController.scheduledFrame = {
id: setTimeout(AnimationController.tick, 0),
type: "timeout",
};
}
}
private static cancelScheduledFrame() {
if (!AnimationController.scheduledFrame) {
return;
}
if (AnimationController.scheduledFrame.type === "raf") {
cancelAnimationFrame(AnimationController.scheduledFrame.id);
} else {
clearTimeout(AnimationController.scheduledFrame.id);
}
AnimationController.scheduledFrame = null;
}
private static cancelScheduledFrameIfIdle() {
if (AnimationController.animations.size > 0) {
return false;
}
AnimationController.cancelScheduledFrame();
return true;
}
private static tick() {
AnimationController.scheduledFrame = null;
if (AnimationController.animations.size > 0) {
for (const [key, animation] of AnimationController.animations) {
const now = performance.now();
const deltaTime =
animation.lastTime === 0 ? 0 : now - animation.lastTime;
const state = animation.animation({
deltaTime,
state: animation.state,
});
if (!state) {
AnimationController.animations.delete(key);
if (AnimationController.cancelScheduledFrameIfIdle()) {
return;
}
} else {
animation.lastTime = now;
animation.state = state;
}
}
if (AnimationController.cancelScheduledFrameIfIdle()) {
return;
}
AnimationController.scheduleNextFrame();
}
}
static running(key: string) {
return AnimationController.animations.has(key);
}
static cancel(key: string) {
AnimationController.animations.delete(key);
AnimationController.cancelScheduledFrameIfIdle();
}
}