fix:Refactored and fixed highlight animation

This commit is contained in:
Mark Tolmacs
2025-09-17 18:37:56 +02:00
parent f0494ced4c
commit e0dd29aa36
3 changed files with 98 additions and 25 deletions

View File

@@ -5,8 +5,10 @@ import {
isShallowEqual,
sceneCoordsToViewportCoords,
} from "@excalidraw/common";
import { AnimationController } from "@excalidraw/excalidraw/renderer/animation";
import type {
ExcalidrawBindableElement,
NonDeletedExcalidrawElement,
NonDeletedSceneElementsMap,
} from "@excalidraw/element/types";
@@ -17,6 +19,7 @@ import { renderInteractiveScene } from "../../renderer/interactiveScene";
import type {
InteractiveCanvasRenderConfig,
InteractiveSceneRenderAnimationState,
RenderableElementsMap,
RenderInteractiveSceneCallback,
} from "../../scene/types";
@@ -78,6 +81,7 @@ type InteractiveCanvasProps = {
const InteractiveCanvas = (props: InteractiveCanvasProps) => {
const isComponentMounted = useRef(false);
const lastSuggestedBinding = useRef<ExcalidrawBindableElement | null>(null);
useEffect(() => {
if (!isComponentMounted.current) {
@@ -156,9 +160,69 @@ const InteractiveCanvas = (props: InteractiveCanvasProps) => {
},
device: props.device,
callback: props.renderInteractiveSceneCallback,
deltaTime: 0,
},
isRenderThrottlingEnabled(),
);
if (lastSuggestedBinding.current !== props.appState.suggestedBinding) {
lastSuggestedBinding.current = props.appState.suggestedBinding;
if (props.appState.suggestedBinding) {
AnimationController.cancel("bindingHighlight");
AnimationController.start<InteractiveSceneRenderAnimationState>(
"bindingHighlight",
({ deltaTime, state }) => {
if (
lastSuggestedBinding.current !== props.appState.suggestedBinding
) {
return null;
}
const nextAnimationState = renderInteractiveScene(
{
canvas: props.canvas,
elementsMap: props.elementsMap,
visibleElements: props.visibleElements,
selectedElements: props.selectedElements,
allElementsMap: props.allElementsMap,
scale: window.devicePixelRatio,
appState: props.appState,
renderConfig: {
remotePointerViewportCoords,
remotePointerButton,
remoteSelectedElementIds,
remotePointerUsernames,
remotePointerUserStates,
selectionColor,
renderScrollbars: props.renderScrollbars,
// NOTE not memoized on so we don't rerender on cursor move
lastViewportPosition: props.app.lastViewportPosition,
},
device: props.device,
callback: props.renderInteractiveSceneCallback,
animationState: state,
deltaTime,
},
false,
).animationState;
if (nextAnimationState) {
for (const key in nextAnimationState) {
if (
nextAnimationState[
key as keyof InteractiveSceneRenderAnimationState
] !== undefined
) {
return nextAnimationState;
}
}
return undefined;
}
},
);
}
}
});
return (

View File

@@ -87,8 +87,6 @@ import {
strokeRectWithRotation,
} from "./helpers";
import { AnimationController } from "./animation";
import type {
InteractiveCanvasRenderConfig,
InteractiveSceneRenderConfig,
@@ -198,7 +196,8 @@ const renderBindingHighlightForBindableElement = (
deltaTime: number,
state?: { runtime: number },
) => {
const remainingTime = BIND_MODE_TIMEOUT - (state?.runtime ?? 0);
const remainingTime =
BIND_MODE_TIMEOUT - (state?.runtime ?? BIND_MODE_TIMEOUT);
const opacity = clamp((1 / BIND_MODE_TIMEOUT) * remainingTime, 0.0001, 1);
const offset = element.strokeWidth / 2;
@@ -206,7 +205,6 @@ const renderBindingHighlightForBindableElement = (
case "magicframe":
case "frame":
context.save();
context.clearRect(0, 0, context.canvas.width, context.canvas.height);
context.translate(
element.x + appState.scrollX,
@@ -243,7 +241,6 @@ const renderBindingHighlightForBindableElement = (
const cx = center[0] + appState.scrollX;
const cy = center[1] + appState.scrollY;
context.clearRect(0, 0, context.canvas.width, context.canvas.height);
context.translate(cx, cy);
context.rotate(element.angle as Radians);
context.translate(-cx, -cy);
@@ -401,7 +398,7 @@ const renderBindingHighlightForBindableElement = (
}
if ((state?.runtime ?? 0) > BIND_MODE_TIMEOUT) {
return null;
return;
}
return {
@@ -860,7 +857,14 @@ const _renderInteractiveScene = ({
appState,
renderConfig,
device,
}: InteractiveSceneRenderConfig) => {
animationState,
deltaTime,
}: InteractiveSceneRenderConfig): {
scrollBars?: ReturnType<typeof getScrollBars>;
atLeastOneVisibleElement: boolean;
elementsMap: RenderableElementsMap;
animationState?: typeof animationState;
} => {
if (canvas === null) {
return { atLeastOneVisibleElement: false, elementsMap };
}
@@ -869,6 +873,7 @@ const _renderInteractiveScene = ({
canvas,
scale,
);
let nextAnimationState = animationState;
const context = bootstrapCanvas({
canvas,
@@ -939,25 +944,22 @@ const _renderInteractiveScene = ({
}
if (appState.isBindingEnabled && appState.suggestedBinding) {
AnimationController.start<{ runtime: number }>(
"bindingHighlight",
({ deltaTime, state }) => {
if (!appState.suggestedBinding) {
return null; // Stop the animation
}
return renderBindingHighlightForBindableElement(
context,
appState.suggestedBinding,
allElementsMap,
appState,
deltaTime,
state,
);
},
);
nextAnimationState = {
...animationState,
bindingHighlight: renderBindingHighlightForBindableElement(
context,
appState.suggestedBinding,
allElementsMap,
appState,
deltaTime,
animationState?.bindingHighlight,
),
};
} else {
AnimationController.cancel("bindingHighlight");
nextAnimationState = {
...animationState,
bindingHighlight: undefined,
};
}
if (appState.frameToHighlight) {
@@ -1329,6 +1331,7 @@ const _renderInteractiveScene = ({
scrollBars,
atLeastOneVisibleElement: visibleElements.length > 0,
elementsMap,
animationState: nextAnimationState,
};
};

View File

@@ -89,6 +89,10 @@ export type StaticSceneRenderConfig = {
renderConfig: StaticCanvasRenderConfig;
};
export type InteractiveSceneRenderAnimationState = {
bindingHighlight: { runtime: number } | undefined;
};
export type InteractiveSceneRenderConfig = {
canvas: HTMLCanvasElement | null;
elementsMap: RenderableElementsMap;
@@ -100,6 +104,8 @@ export type InteractiveSceneRenderConfig = {
renderConfig: InteractiveCanvasRenderConfig;
device: Device;
callback: (data: RenderInteractiveSceneCallback) => void;
animationState?: InteractiveSceneRenderAnimationState;
deltaTime: number;
};
export type NewElementSceneRenderConfig = {