mirror of
https://github.com/excalidraw/excalidraw.git
synced 2025-11-15 10:15:03 +01:00
feat: Animation support (#10042)
This commit is contained in:
@@ -1813,6 +1813,7 @@ class App extends React.Component<AppProps, AppState> {
|
||||
/>
|
||||
)}
|
||||
<InteractiveCanvas
|
||||
app={this}
|
||||
containerRef={this.excalidrawContainerRef}
|
||||
canvas={this.interactiveCanvas}
|
||||
elementsMap={elementsMap}
|
||||
|
||||
@@ -6,6 +6,15 @@ import {
|
||||
sceneCoordsToViewportCoords,
|
||||
type EditorInterface,
|
||||
} from "@excalidraw/common";
|
||||
import { AnimationController } from "@excalidraw/excalidraw/renderer/animation";
|
||||
|
||||
import type {
|
||||
InteractiveCanvasRenderConfig,
|
||||
InteractiveSceneRenderAnimationState,
|
||||
InteractiveSceneRenderConfig,
|
||||
RenderableElementsMap,
|
||||
RenderInteractiveSceneCallback,
|
||||
} from "@excalidraw/excalidraw/scene/types";
|
||||
|
||||
import type {
|
||||
NonDeletedExcalidrawElement,
|
||||
@@ -13,15 +22,13 @@ import type {
|
||||
} from "@excalidraw/element/types";
|
||||
|
||||
import { t } from "../../i18n";
|
||||
import { isRenderThrottlingEnabled } from "../../reactUtils";
|
||||
import { renderInteractiveScene } from "../../renderer/interactiveScene";
|
||||
|
||||
import type {
|
||||
InteractiveCanvasRenderConfig,
|
||||
RenderableElementsMap,
|
||||
RenderInteractiveSceneCallback,
|
||||
} from "../../scene/types";
|
||||
import type { AppState, InteractiveCanvasAppState } from "../../types";
|
||||
AppClassProperties,
|
||||
AppState,
|
||||
InteractiveCanvasAppState,
|
||||
} from "../../types";
|
||||
import type { DOMAttributes } from "react";
|
||||
|
||||
type InteractiveCanvasProps = {
|
||||
@@ -37,6 +44,7 @@ type InteractiveCanvasProps = {
|
||||
appState: InteractiveCanvasAppState;
|
||||
renderScrollbars: boolean;
|
||||
editorInterface: EditorInterface;
|
||||
app: AppClassProperties;
|
||||
renderInteractiveSceneCallback: (
|
||||
data: RenderInteractiveSceneCallback,
|
||||
) => void;
|
||||
@@ -71,8 +79,11 @@ type InteractiveCanvasProps = {
|
||||
>;
|
||||
};
|
||||
|
||||
export const INTERACTIVE_SCENE_ANIMATION_KEY = "animateInteractiveScene";
|
||||
|
||||
const InteractiveCanvas = (props: InteractiveCanvasProps) => {
|
||||
const isComponentMounted = useRef(false);
|
||||
const rendererParams = useRef(null as InteractiveSceneRenderConfig | null);
|
||||
|
||||
useEffect(() => {
|
||||
if (!isComponentMounted.current) {
|
||||
@@ -129,8 +140,8 @@ const InteractiveCanvas = (props: InteractiveCanvasProps) => {
|
||||
)) ||
|
||||
"#6965db";
|
||||
|
||||
renderInteractiveScene(
|
||||
{
|
||||
rendererParams.current = {
|
||||
app: props.app,
|
||||
canvas: props.canvas,
|
||||
elementsMap: props.elementsMap,
|
||||
visibleElements: props.visibleElements,
|
||||
@@ -149,9 +160,38 @@ const InteractiveCanvas = (props: InteractiveCanvasProps) => {
|
||||
},
|
||||
editorInterface: props.editorInterface,
|
||||
callback: props.renderInteractiveSceneCallback,
|
||||
animationState: {
|
||||
bindingHighlight: undefined,
|
||||
},
|
||||
deltaTime: 0,
|
||||
};
|
||||
|
||||
if (!AnimationController.running(INTERACTIVE_SCENE_ANIMATION_KEY)) {
|
||||
AnimationController.start<InteractiveSceneRenderAnimationState>(
|
||||
INTERACTIVE_SCENE_ANIMATION_KEY,
|
||||
({ deltaTime, state }) => {
|
||||
const nextAnimationState = renderInteractiveScene({
|
||||
...rendererParams.current!,
|
||||
deltaTime,
|
||||
animationState: state,
|
||||
}).animationState;
|
||||
|
||||
if (nextAnimationState) {
|
||||
for (const key in nextAnimationState) {
|
||||
if (
|
||||
nextAnimationState[
|
||||
key as keyof InteractiveSceneRenderAnimationState
|
||||
] !== undefined
|
||||
) {
|
||||
return nextAnimationState;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return undefined;
|
||||
},
|
||||
isRenderThrottlingEnabled(),
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
return (
|
||||
|
||||
84
packages/excalidraw/renderer/animation.ts
Normal file
84
packages/excalidraw/renderer/animation.ts
Normal file
@@ -0,0 +1,84 @@
|
||||
import { isRenderThrottlingEnabled } from "../reactUtils";
|
||||
|
||||
export type Animation<R extends object> = (params: {
|
||||
deltaTime: number;
|
||||
state?: R;
|
||||
}) => R | null | undefined;
|
||||
|
||||
export class AnimationController {
|
||||
private static isRunning = false;
|
||||
private static animations = new Map<
|
||||
string,
|
||||
{
|
||||
animation: Animation<any>;
|
||||
lastTime: number;
|
||||
state: any;
|
||||
}
|
||||
>();
|
||||
|
||||
static start<R extends object>(key: string, animation: Animation<R>) {
|
||||
const initialState = animation({
|
||||
deltaTime: 0,
|
||||
state: undefined,
|
||||
});
|
||||
|
||||
if (initialState) {
|
||||
AnimationController.animations.set(key, {
|
||||
animation,
|
||||
lastTime: 0,
|
||||
state: initialState,
|
||||
});
|
||||
|
||||
if (!AnimationController.isRunning) {
|
||||
AnimationController.isRunning = true;
|
||||
|
||||
if (isRenderThrottlingEnabled()) {
|
||||
requestAnimationFrame(AnimationController.tick);
|
||||
} else {
|
||||
setTimeout(AnimationController.tick, 0);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static tick() {
|
||||
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.animations.size === 0) {
|
||||
AnimationController.isRunning = false;
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
animation.lastTime = now;
|
||||
animation.state = state;
|
||||
}
|
||||
}
|
||||
|
||||
if (isRenderThrottlingEnabled()) {
|
||||
requestAnimationFrame(AnimationController.tick);
|
||||
} else {
|
||||
setTimeout(AnimationController.tick, 0);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static running(key: string) {
|
||||
return AnimationController.animations.has(key);
|
||||
}
|
||||
|
||||
static cancel(key: string) {
|
||||
AnimationController.animations.delete(key);
|
||||
}
|
||||
}
|
||||
@@ -13,7 +13,6 @@ import {
|
||||
FRAME_STYLE,
|
||||
invariant,
|
||||
THEME,
|
||||
throttleRAF,
|
||||
} from "@excalidraw/common";
|
||||
|
||||
import { FIXED_BINDING_DISTANCE, maxBindingGap } from "@excalidraw/element";
|
||||
@@ -735,7 +734,14 @@ const _renderInteractiveScene = ({
|
||||
appState,
|
||||
renderConfig,
|
||||
editorInterface,
|
||||
}: InteractiveSceneRenderConfig) => {
|
||||
animationState,
|
||||
deltaTime,
|
||||
}: InteractiveSceneRenderConfig): {
|
||||
scrollBars?: ReturnType<typeof getScrollBars>;
|
||||
atLeastOneVisibleElement: boolean;
|
||||
elementsMap: RenderableElementsMap;
|
||||
animationState?: typeof animationState;
|
||||
} => {
|
||||
if (canvas === null) {
|
||||
return { atLeastOneVisibleElement: false, elementsMap };
|
||||
}
|
||||
@@ -745,6 +751,8 @@ const _renderInteractiveScene = ({
|
||||
scale,
|
||||
);
|
||||
|
||||
const nextAnimationState = animationState;
|
||||
|
||||
const context = bootstrapCanvas({
|
||||
canvas,
|
||||
scale,
|
||||
@@ -1198,34 +1206,20 @@ const _renderInteractiveScene = ({
|
||||
scrollBars,
|
||||
atLeastOneVisibleElement: visibleElements.length > 0,
|
||||
elementsMap,
|
||||
animationState: nextAnimationState,
|
||||
};
|
||||
};
|
||||
|
||||
/** throttled to animation framerate */
|
||||
export const renderInteractiveSceneThrottled = throttleRAF(
|
||||
(config: InteractiveSceneRenderConfig) => {
|
||||
const ret = _renderInteractiveScene(config);
|
||||
config.callback?.(ret);
|
||||
},
|
||||
{ trailing: true },
|
||||
);
|
||||
|
||||
/**
|
||||
* Interactive scene is the ui-canvas where we render bounding boxes, selections
|
||||
* and other ui stuff.
|
||||
*/
|
||||
export const renderInteractiveScene = <
|
||||
U extends typeof _renderInteractiveScene,
|
||||
T extends boolean = false,
|
||||
>(
|
||||
renderConfig: InteractiveSceneRenderConfig,
|
||||
throttle?: T,
|
||||
): T extends true ? void : ReturnType<U> => {
|
||||
if (throttle) {
|
||||
renderInteractiveSceneThrottled(renderConfig);
|
||||
return undefined as T extends true ? void : ReturnType<U>;
|
||||
}
|
||||
): ReturnType<U> => {
|
||||
const ret = _renderInteractiveScene(renderConfig);
|
||||
renderConfig.callback(ret);
|
||||
return ret as T extends true ? void : ReturnType<U>;
|
||||
return ret as ReturnType<U>;
|
||||
};
|
||||
|
||||
@@ -10,7 +10,6 @@ import type {
|
||||
|
||||
import type { Scene } from "@excalidraw/element";
|
||||
|
||||
import { renderInteractiveSceneThrottled } from "../renderer/interactiveScene";
|
||||
import { renderStaticSceneThrottled } from "../renderer/staticScene";
|
||||
|
||||
import type { RenderableElementsMap } from "./types";
|
||||
@@ -150,7 +149,6 @@ export class Renderer {
|
||||
// NOTE Doesn't destroy everything (scene, rc, etc.) because it may not be
|
||||
// safe to break TS contract here (for upstream cases)
|
||||
public destroy() {
|
||||
renderInteractiveSceneThrottled.cancel();
|
||||
renderStaticSceneThrottled.cancel();
|
||||
this.getRenderableElements.clear();
|
||||
}
|
||||
|
||||
@@ -87,7 +87,12 @@ export type StaticSceneRenderConfig = {
|
||||
renderConfig: StaticCanvasRenderConfig;
|
||||
};
|
||||
|
||||
export type InteractiveSceneRenderAnimationState = {
|
||||
bindingHighlight: { runtime: number } | undefined;
|
||||
};
|
||||
|
||||
export type InteractiveSceneRenderConfig = {
|
||||
app: AppClassProperties;
|
||||
canvas: HTMLCanvasElement | null;
|
||||
elementsMap: RenderableElementsMap;
|
||||
visibleElements: readonly NonDeletedExcalidrawElement[];
|
||||
@@ -98,6 +103,8 @@ export type InteractiveSceneRenderConfig = {
|
||||
renderConfig: InteractiveCanvasRenderConfig;
|
||||
editorInterface: EditorInterface;
|
||||
callback: (data: RenderInteractiveSceneCallback) => void;
|
||||
animationState?: InteractiveSceneRenderAnimationState;
|
||||
deltaTime: number;
|
||||
};
|
||||
|
||||
export type NewElementSceneRenderConfig = {
|
||||
|
||||
Reference in New Issue
Block a user