import React, { useEffect, useRef } from "react"; import { isShallowEqual } from "@excalidraw/common"; import type { NonDeletedExcalidrawElement, NonDeletedSceneElementsMap, } from "@excalidraw/element/types"; import { isRenderThrottlingEnabled } from "../../reactUtils"; import { renderStaticScene } from "../../renderer/staticScene"; import type { RenderableElementsMap, StaticCanvasRenderConfig, } from "../../scene/types"; import type { AppState, StaticCanvasAppState } from "../../types"; import type { RoughCanvas } from "roughjs/bin/canvas"; type StaticCanvasProps = { canvas: HTMLCanvasElement; rc: RoughCanvas; elementsMap: RenderableElementsMap; allElementsMap: NonDeletedSceneElementsMap; visibleElements: readonly NonDeletedExcalidrawElement[]; canvasNonce: string; selectionNonce: number | undefined; scale: number; appState: StaticCanvasAppState; renderConfig: StaticCanvasRenderConfig; }; const StaticCanvas = (props: StaticCanvasProps) => { const wrapperRef = useRef(null); const isComponentMounted = useRef(false); useEffect(() => { props.canvas.style.width = `${props.appState.width}px`; props.canvas.style.height = `${props.appState.height}px`; props.canvas.width = props.appState.width * props.scale; props.canvas.height = props.appState.height * props.scale; }, [props.appState.height, props.appState.width, props.canvas, props.scale]); useEffect(() => { const wrapper = wrapperRef.current; if (!wrapper) { return; } const canvas = props.canvas; if (!isComponentMounted.current) { isComponentMounted.current = true; wrapper.replaceChildren(canvas); canvas.classList.add("excalidraw__canvas", "static"); } renderStaticScene( { canvas, rc: props.rc, scale: props.scale, elementsMap: props.elementsMap, allElementsMap: props.allElementsMap, visibleElements: props.visibleElements, appState: props.appState, renderConfig: props.renderConfig, }, isRenderThrottlingEnabled(), ); }); return
; }; const getRelevantAppStateProps = (appState: AppState): StaticCanvasAppState => { const relevantAppStateProps = { zoom: appState.zoom, scrollX: appState.scrollX, scrollY: appState.scrollY, width: appState.width, height: appState.height, viewModeEnabled: appState.viewModeEnabled, openDialog: appState.openDialog, hoveredElementIds: appState.hoveredElementIds, offsetLeft: appState.offsetLeft, offsetTop: appState.offsetTop, theme: appState.theme, shouldCacheIgnoreZoom: appState.shouldCacheIgnoreZoom, viewBackgroundColor: appState.viewBackgroundColor, exportScale: appState.exportScale, selectedElementsAreBeingDragged: appState.selectedElementsAreBeingDragged, gridSize: appState.gridSize, gridStep: appState.gridStep, frameRendering: appState.frameRendering, selectedElementIds: appState.selectedElementIds, frameToHighlight: appState.frameToHighlight, editingGroupId: appState.editingGroupId, currentHoveredFontFamily: appState.currentHoveredFontFamily, croppingElementId: appState.croppingElementId, suggestedBinding: appState.suggestedBinding, }; return relevantAppStateProps; }; const areEqual = ( prevProps: StaticCanvasProps, nextProps: StaticCanvasProps, ) => { if ( prevProps.canvasNonce !== nextProps.canvasNonce || prevProps.scale !== nextProps.scale || // we need to memoize on elementsMap because they may have renewed // even if canvasNonce didn't change (e.g. we filter elements out based // on appState) prevProps.elementsMap !== nextProps.elementsMap || prevProps.visibleElements !== nextProps.visibleElements ) { return false; } return ( isShallowEqual( // asserting AppState because we're being passed the whole AppState // but resolve to only the StaticCanvas-relevant props getRelevantAppStateProps(prevProps.appState as AppState), getRelevantAppStateProps(nextProps.appState as AppState), ) && isShallowEqual(prevProps.renderConfig, nextProps.renderConfig) ); }; export default React.memo(StaticCanvas, areEqual);