aboutsummaryrefslogtreecommitdiffstats
path: root/packages/excalidraw/components/canvases/InteractiveCanvas.tsx
diff options
context:
space:
mode:
authorkj_sh6042026-03-15 16:19:35 -0400
committerkj_sh6042026-03-15 16:19:35 -0400
commit6ec259a0e71174651bae95d4628138bf6fd68742 (patch)
tree5e33c6a5ec091ecabfcb257fdc7b6a88ed8754ac /packages/excalidraw/components/canvases/InteractiveCanvas.tsx
parent16c8578b15c727f22921f8a80a56ee4d4e7f2272 (diff)
refactor: packages/
Diffstat (limited to 'packages/excalidraw/components/canvases/InteractiveCanvas.tsx')
-rw-r--r--packages/excalidraw/components/canvases/InteractiveCanvas.tsx240
1 files changed, 240 insertions, 0 deletions
diff --git a/packages/excalidraw/components/canvases/InteractiveCanvas.tsx b/packages/excalidraw/components/canvases/InteractiveCanvas.tsx
new file mode 100644
index 0000000..03ae0f6
--- /dev/null
+++ b/packages/excalidraw/components/canvases/InteractiveCanvas.tsx
@@ -0,0 +1,240 @@
+import React, { useEffect, useRef } from "react";
+import { isShallowEqual, sceneCoordsToViewportCoords } from "../../utils";
+import { CURSOR_TYPE } from "../../constants";
+import { t } from "../../i18n";
+import type { DOMAttributes } from "react";
+import type { AppState, Device, InteractiveCanvasAppState } from "../../types";
+import type {
+ InteractiveCanvasRenderConfig,
+ RenderableElementsMap,
+ RenderInteractiveSceneCallback,
+} from "../../scene/types";
+import type {
+ NonDeletedExcalidrawElement,
+ NonDeletedSceneElementsMap,
+} from "../../element/types";
+import { isRenderThrottlingEnabled } from "../../reactUtils";
+import { renderInteractiveScene } from "../../renderer/interactiveScene";
+
+type InteractiveCanvasProps = {
+ containerRef: React.RefObject<HTMLDivElement | null>;
+ canvas: HTMLCanvasElement | null;
+ elementsMap: RenderableElementsMap;
+ visibleElements: readonly NonDeletedExcalidrawElement[];
+ selectedElements: readonly NonDeletedExcalidrawElement[];
+ allElementsMap: NonDeletedSceneElementsMap;
+ sceneNonce: number | undefined;
+ selectionNonce: number | undefined;
+ scale: number;
+ appState: InteractiveCanvasAppState;
+ device: Device;
+ renderInteractiveSceneCallback: (
+ data: RenderInteractiveSceneCallback,
+ ) => void;
+ handleCanvasRef: (canvas: HTMLCanvasElement | null) => void;
+ onContextMenu: Exclude<
+ DOMAttributes<HTMLCanvasElement | HTMLDivElement>["onContextMenu"],
+ undefined
+ >;
+ onPointerMove: Exclude<
+ DOMAttributes<HTMLCanvasElement>["onPointerMove"],
+ undefined
+ >;
+ onPointerUp: Exclude<
+ DOMAttributes<HTMLCanvasElement>["onPointerUp"],
+ undefined
+ >;
+ onPointerCancel: Exclude<
+ DOMAttributes<HTMLCanvasElement>["onPointerCancel"],
+ undefined
+ >;
+ onTouchMove: Exclude<
+ DOMAttributes<HTMLCanvasElement>["onTouchMove"],
+ undefined
+ >;
+ onPointerDown: Exclude<
+ DOMAttributes<HTMLCanvasElement>["onPointerDown"],
+ undefined
+ >;
+ onDoubleClick: Exclude<
+ DOMAttributes<HTMLCanvasElement>["onDoubleClick"],
+ undefined
+ >;
+};
+
+const InteractiveCanvas = (props: InteractiveCanvasProps) => {
+ const isComponentMounted = useRef(false);
+
+ useEffect(() => {
+ if (!isComponentMounted.current) {
+ isComponentMounted.current = true;
+ return;
+ }
+
+ const remotePointerButton: InteractiveCanvasRenderConfig["remotePointerButton"] =
+ new Map();
+ const remotePointerViewportCoords: InteractiveCanvasRenderConfig["remotePointerViewportCoords"] =
+ new Map();
+ const remoteSelectedElementIds: InteractiveCanvasRenderConfig["remoteSelectedElementIds"] =
+ new Map();
+ const remotePointerUsernames: InteractiveCanvasRenderConfig["remotePointerUsernames"] =
+ new Map();
+ const remotePointerUserStates: InteractiveCanvasRenderConfig["remotePointerUserStates"] =
+ new Map();
+
+ props.appState.collaborators.forEach((user, socketId) => {
+ if (user.selectedElementIds) {
+ for (const id of Object.keys(user.selectedElementIds)) {
+ if (!remoteSelectedElementIds.has(id)) {
+ remoteSelectedElementIds.set(id, []);
+ }
+ remoteSelectedElementIds.get(id)!.push(socketId);
+ }
+ }
+ if (!user.pointer || user.pointer.renderCursor === false) {
+ return;
+ }
+ if (user.username) {
+ remotePointerUsernames.set(socketId, user.username);
+ }
+ if (user.userState) {
+ remotePointerUserStates.set(socketId, user.userState);
+ }
+ remotePointerViewportCoords.set(
+ socketId,
+ sceneCoordsToViewportCoords(
+ {
+ sceneX: user.pointer.x,
+ sceneY: user.pointer.y,
+ },
+ props.appState,
+ ),
+ );
+ remotePointerButton.set(socketId, user.button);
+ });
+
+ const selectionColor =
+ (props.containerRef?.current &&
+ getComputedStyle(props.containerRef.current).getPropertyValue(
+ "--color-selection",
+ )) ||
+ "#6965db";
+
+ 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: false,
+ },
+ device: props.device,
+ callback: props.renderInteractiveSceneCallback,
+ },
+ isRenderThrottlingEnabled(),
+ );
+ });
+
+ return (
+ <canvas
+ className="excalidraw__canvas interactive"
+ style={{
+ width: props.appState.width,
+ height: props.appState.height,
+ cursor: props.appState.viewModeEnabled
+ ? CURSOR_TYPE.GRAB
+ : CURSOR_TYPE.AUTO,
+ }}
+ width={props.appState.width * props.scale}
+ height={props.appState.height * props.scale}
+ ref={props.handleCanvasRef}
+ onContextMenu={props.onContextMenu}
+ onPointerMove={props.onPointerMove}
+ onPointerUp={props.onPointerUp}
+ onPointerCancel={props.onPointerCancel}
+ onTouchMove={props.onTouchMove}
+ onPointerDown={props.onPointerDown}
+ onDoubleClick={
+ props.appState.viewModeEnabled ? undefined : props.onDoubleClick
+ }
+ >
+ {t("labels.drawingCanvas")}
+ </canvas>
+ );
+};
+
+const getRelevantAppStateProps = (
+ appState: AppState,
+): InteractiveCanvasAppState => ({
+ zoom: appState.zoom,
+ scrollX: appState.scrollX,
+ scrollY: appState.scrollY,
+ width: appState.width,
+ height: appState.height,
+ viewModeEnabled: appState.viewModeEnabled,
+ openDialog: appState.openDialog,
+ editingGroupId: appState.editingGroupId,
+ editingLinearElement: appState.editingLinearElement,
+ selectedElementIds: appState.selectedElementIds,
+ frameToHighlight: appState.frameToHighlight,
+ offsetLeft: appState.offsetLeft,
+ offsetTop: appState.offsetTop,
+ theme: appState.theme,
+ pendingImageElementId: appState.pendingImageElementId,
+ selectionElement: appState.selectionElement,
+ selectedGroupIds: appState.selectedGroupIds,
+ selectedLinearElement: appState.selectedLinearElement,
+ multiElement: appState.multiElement,
+ isBindingEnabled: appState.isBindingEnabled,
+ suggestedBindings: appState.suggestedBindings,
+ isRotating: appState.isRotating,
+ elementsToHighlight: appState.elementsToHighlight,
+ collaborators: appState.collaborators, // Necessary for collab. sessions
+ activeEmbeddable: appState.activeEmbeddable,
+ snapLines: appState.snapLines,
+ zenModeEnabled: appState.zenModeEnabled,
+ editingTextElement: appState.editingTextElement,
+ isCropping: appState.isCropping,
+ croppingElementId: appState.croppingElementId,
+ searchMatches: appState.searchMatches,
+});
+
+const areEqual = (
+ prevProps: InteractiveCanvasProps,
+ nextProps: InteractiveCanvasProps,
+) => {
+ // This could be further optimised if needed, as we don't have to render interactive canvas on each scene mutation
+ if (
+ prevProps.selectionNonce !== nextProps.selectionNonce ||
+ prevProps.sceneNonce !== nextProps.sceneNonce ||
+ prevProps.scale !== nextProps.scale ||
+ // we need to memoize on elementsMap because they may have renewed
+ // even if sceneNonce didn't change (e.g. we filter elements out based
+ // on appState)
+ prevProps.elementsMap !== nextProps.elementsMap ||
+ prevProps.visibleElements !== nextProps.visibleElements ||
+ prevProps.selectedElements !== nextProps.selectedElements
+ ) {
+ return false;
+ }
+
+ // Comparing the interactive appState for changes in case of some edge cases
+ return isShallowEqual(
+ // asserting AppState because we're being passed the whole AppState
+ // but resolve to only the InteractiveCanvas-relevant props
+ getRelevantAppStateProps(prevProps.appState as AppState),
+ getRelevantAppStateProps(nextProps.appState as AppState),
+ );
+};
+
+export default React.memo(InteractiveCanvas, areEqual);