mirror of
https://github.com/excalidraw/excalidraw.git
synced 2025-09-22 00:41:09 +02:00
fix:Refactored and fixed highlight animation
This commit is contained in:
@@ -5,8 +5,10 @@ import {
|
|||||||
isShallowEqual,
|
isShallowEqual,
|
||||||
sceneCoordsToViewportCoords,
|
sceneCoordsToViewportCoords,
|
||||||
} from "@excalidraw/common";
|
} from "@excalidraw/common";
|
||||||
|
import { AnimationController } from "@excalidraw/excalidraw/renderer/animation";
|
||||||
|
|
||||||
import type {
|
import type {
|
||||||
|
ExcalidrawBindableElement,
|
||||||
NonDeletedExcalidrawElement,
|
NonDeletedExcalidrawElement,
|
||||||
NonDeletedSceneElementsMap,
|
NonDeletedSceneElementsMap,
|
||||||
} from "@excalidraw/element/types";
|
} from "@excalidraw/element/types";
|
||||||
@@ -17,6 +19,7 @@ import { renderInteractiveScene } from "../../renderer/interactiveScene";
|
|||||||
|
|
||||||
import type {
|
import type {
|
||||||
InteractiveCanvasRenderConfig,
|
InteractiveCanvasRenderConfig,
|
||||||
|
InteractiveSceneRenderAnimationState,
|
||||||
RenderableElementsMap,
|
RenderableElementsMap,
|
||||||
RenderInteractiveSceneCallback,
|
RenderInteractiveSceneCallback,
|
||||||
} from "../../scene/types";
|
} from "../../scene/types";
|
||||||
@@ -78,6 +81,7 @@ type InteractiveCanvasProps = {
|
|||||||
|
|
||||||
const InteractiveCanvas = (props: InteractiveCanvasProps) => {
|
const InteractiveCanvas = (props: InteractiveCanvasProps) => {
|
||||||
const isComponentMounted = useRef(false);
|
const isComponentMounted = useRef(false);
|
||||||
|
const lastSuggestedBinding = useRef<ExcalidrawBindableElement | null>(null);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!isComponentMounted.current) {
|
if (!isComponentMounted.current) {
|
||||||
@@ -156,9 +160,69 @@ const InteractiveCanvas = (props: InteractiveCanvasProps) => {
|
|||||||
},
|
},
|
||||||
device: props.device,
|
device: props.device,
|
||||||
callback: props.renderInteractiveSceneCallback,
|
callback: props.renderInteractiveSceneCallback,
|
||||||
|
deltaTime: 0,
|
||||||
},
|
},
|
||||||
isRenderThrottlingEnabled(),
|
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 (
|
return (
|
||||||
|
@@ -87,8 +87,6 @@ import {
|
|||||||
strokeRectWithRotation,
|
strokeRectWithRotation,
|
||||||
} from "./helpers";
|
} from "./helpers";
|
||||||
|
|
||||||
import { AnimationController } from "./animation";
|
|
||||||
|
|
||||||
import type {
|
import type {
|
||||||
InteractiveCanvasRenderConfig,
|
InteractiveCanvasRenderConfig,
|
||||||
InteractiveSceneRenderConfig,
|
InteractiveSceneRenderConfig,
|
||||||
@@ -198,7 +196,8 @@ const renderBindingHighlightForBindableElement = (
|
|||||||
deltaTime: number,
|
deltaTime: number,
|
||||||
state?: { runtime: 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 opacity = clamp((1 / BIND_MODE_TIMEOUT) * remainingTime, 0.0001, 1);
|
||||||
const offset = element.strokeWidth / 2;
|
const offset = element.strokeWidth / 2;
|
||||||
|
|
||||||
@@ -206,7 +205,6 @@ const renderBindingHighlightForBindableElement = (
|
|||||||
case "magicframe":
|
case "magicframe":
|
||||||
case "frame":
|
case "frame":
|
||||||
context.save();
|
context.save();
|
||||||
context.clearRect(0, 0, context.canvas.width, context.canvas.height);
|
|
||||||
|
|
||||||
context.translate(
|
context.translate(
|
||||||
element.x + appState.scrollX,
|
element.x + appState.scrollX,
|
||||||
@@ -243,7 +241,6 @@ const renderBindingHighlightForBindableElement = (
|
|||||||
const cx = center[0] + appState.scrollX;
|
const cx = center[0] + appState.scrollX;
|
||||||
const cy = center[1] + appState.scrollY;
|
const cy = center[1] + appState.scrollY;
|
||||||
|
|
||||||
context.clearRect(0, 0, context.canvas.width, context.canvas.height);
|
|
||||||
context.translate(cx, cy);
|
context.translate(cx, cy);
|
||||||
context.rotate(element.angle as Radians);
|
context.rotate(element.angle as Radians);
|
||||||
context.translate(-cx, -cy);
|
context.translate(-cx, -cy);
|
||||||
@@ -401,7 +398,7 @@ const renderBindingHighlightForBindableElement = (
|
|||||||
}
|
}
|
||||||
|
|
||||||
if ((state?.runtime ?? 0) > BIND_MODE_TIMEOUT) {
|
if ((state?.runtime ?? 0) > BIND_MODE_TIMEOUT) {
|
||||||
return null;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
@@ -860,7 +857,14 @@ const _renderInteractiveScene = ({
|
|||||||
appState,
|
appState,
|
||||||
renderConfig,
|
renderConfig,
|
||||||
device,
|
device,
|
||||||
}: InteractiveSceneRenderConfig) => {
|
animationState,
|
||||||
|
deltaTime,
|
||||||
|
}: InteractiveSceneRenderConfig): {
|
||||||
|
scrollBars?: ReturnType<typeof getScrollBars>;
|
||||||
|
atLeastOneVisibleElement: boolean;
|
||||||
|
elementsMap: RenderableElementsMap;
|
||||||
|
animationState?: typeof animationState;
|
||||||
|
} => {
|
||||||
if (canvas === null) {
|
if (canvas === null) {
|
||||||
return { atLeastOneVisibleElement: false, elementsMap };
|
return { atLeastOneVisibleElement: false, elementsMap };
|
||||||
}
|
}
|
||||||
@@ -869,6 +873,7 @@ const _renderInteractiveScene = ({
|
|||||||
canvas,
|
canvas,
|
||||||
scale,
|
scale,
|
||||||
);
|
);
|
||||||
|
let nextAnimationState = animationState;
|
||||||
|
|
||||||
const context = bootstrapCanvas({
|
const context = bootstrapCanvas({
|
||||||
canvas,
|
canvas,
|
||||||
@@ -939,25 +944,22 @@ const _renderInteractiveScene = ({
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (appState.isBindingEnabled && appState.suggestedBinding) {
|
if (appState.isBindingEnabled && appState.suggestedBinding) {
|
||||||
AnimationController.start<{ runtime: number }>(
|
nextAnimationState = {
|
||||||
"bindingHighlight",
|
...animationState,
|
||||||
({ deltaTime, state }) => {
|
bindingHighlight: renderBindingHighlightForBindableElement(
|
||||||
if (!appState.suggestedBinding) {
|
context,
|
||||||
return null; // Stop the animation
|
appState.suggestedBinding,
|
||||||
}
|
allElementsMap,
|
||||||
|
appState,
|
||||||
return renderBindingHighlightForBindableElement(
|
deltaTime,
|
||||||
context,
|
animationState?.bindingHighlight,
|
||||||
appState.suggestedBinding,
|
),
|
||||||
allElementsMap,
|
};
|
||||||
appState,
|
|
||||||
deltaTime,
|
|
||||||
state,
|
|
||||||
);
|
|
||||||
},
|
|
||||||
);
|
|
||||||
} else {
|
} else {
|
||||||
AnimationController.cancel("bindingHighlight");
|
nextAnimationState = {
|
||||||
|
...animationState,
|
||||||
|
bindingHighlight: undefined,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
if (appState.frameToHighlight) {
|
if (appState.frameToHighlight) {
|
||||||
@@ -1329,6 +1331,7 @@ const _renderInteractiveScene = ({
|
|||||||
scrollBars,
|
scrollBars,
|
||||||
atLeastOneVisibleElement: visibleElements.length > 0,
|
atLeastOneVisibleElement: visibleElements.length > 0,
|
||||||
elementsMap,
|
elementsMap,
|
||||||
|
animationState: nextAnimationState,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@@ -89,6 +89,10 @@ export type StaticSceneRenderConfig = {
|
|||||||
renderConfig: StaticCanvasRenderConfig;
|
renderConfig: StaticCanvasRenderConfig;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type InteractiveSceneRenderAnimationState = {
|
||||||
|
bindingHighlight: { runtime: number } | undefined;
|
||||||
|
};
|
||||||
|
|
||||||
export type InteractiveSceneRenderConfig = {
|
export type InteractiveSceneRenderConfig = {
|
||||||
canvas: HTMLCanvasElement | null;
|
canvas: HTMLCanvasElement | null;
|
||||||
elementsMap: RenderableElementsMap;
|
elementsMap: RenderableElementsMap;
|
||||||
@@ -100,6 +104,8 @@ export type InteractiveSceneRenderConfig = {
|
|||||||
renderConfig: InteractiveCanvasRenderConfig;
|
renderConfig: InteractiveCanvasRenderConfig;
|
||||||
device: Device;
|
device: Device;
|
||||||
callback: (data: RenderInteractiveSceneCallback) => void;
|
callback: (data: RenderInteractiveSceneCallback) => void;
|
||||||
|
animationState?: InteractiveSceneRenderAnimationState;
|
||||||
|
deltaTime: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type NewElementSceneRenderConfig = {
|
export type NewElementSceneRenderConfig = {
|
||||||
|
Reference in New Issue
Block a user