feat(ui): use stable objects for animation/native element styles

This commit is contained in:
psychedelicious
2023-12-29 13:27:39 +11:00
committed by Kent Keirsey
parent f5194f9e2d
commit ca4b8e65c1
33 changed files with 292 additions and 183 deletions

View File

@ -4,7 +4,9 @@ import { Flex } from '@chakra-ui/react';
import { useAppSelector } from 'app/store/storeHooks';
import { IAINoContentFallback } from 'common/components/IAIImageFallback';
import TopPanel from 'features/nodes/components/flow/panels/TopPanel/TopPanel';
import type { AnimationProps } from 'framer-motion';
import { AnimatePresence, motion } from 'framer-motion';
import type { CSSProperties } from 'react';
import { memo } from 'react';
import { useTranslation } from 'react-i18next';
import { MdDeviceHub } from 'react-icons/md';
@ -14,6 +16,28 @@ import { Flow } from './flow/Flow';
import BottomLeftPanel from './flow/panels/BottomLeftPanel/BottomLeftPanel';
import MinimapPanel from './flow/panels/MinimapPanel/MinimapPanel';
const isReadyMotionStyles: CSSProperties = {
position: 'relative',
width: '100%',
height: '100%',
};
const notIsReadyMotionStyles: CSSProperties = {
position: 'absolute',
width: '100%',
height: '100%',
};
const initial: AnimationProps['initial'] = {
opacity: 0,
};
const animate: AnimationProps['animate'] = {
opacity: 1,
transition: { duration: 0.2 },
};
const exit: AnimationProps['exit'] = {
opacity: 0,
transition: { duration: 0.2 },
};
const NodeEditor = () => {
const isReady = useAppSelector((state) => state.nodes.isReady);
const { t } = useTranslation();
@ -30,18 +54,10 @@ const NodeEditor = () => {
<AnimatePresence>
{isReady && (
<motion.div
initial={{
opacity: 0,
}}
animate={{
opacity: 1,
transition: { duration: 0.2 },
}}
exit={{
opacity: 0,
transition: { duration: 0.2 },
}}
style={{ position: 'relative', width: '100%', height: '100%' }}
initial={initial}
animate={animate}
exit={exit}
style={isReadyMotionStyles}
>
<Flow />
<AddNodePopover />
@ -54,18 +70,10 @@ const NodeEditor = () => {
<AnimatePresence>
{!isReady && (
<motion.div
initial={{
opacity: 0,
}}
animate={{
opacity: 1,
transition: { duration: 0.2 },
}}
exit={{
opacity: 0,
transition: { duration: 0.2 },
}}
style={{ position: 'absolute', width: '100%', height: '100%' }}
initial={initial}
animate={animate}
exit={exit}
style={notIsReadyMotionStyles}
>
<Flex
layerStyle="first"

View File

@ -23,8 +23,8 @@ import {
} from 'features/nodes/store/nodesSlice';
import { $flow } from 'features/nodes/store/reactFlowInstance';
import { bumpGlobalMenuCloseTrigger } from 'features/ui/store/uiSlice';
import type { MouseEvent } from 'react';
import { useCallback, useRef } from 'react';
import type { CSSProperties, MouseEvent } from 'react';
import { useCallback, useMemo, useRef } from 'react';
import { useHotkeys } from 'react-hotkeys-hook';
import type {
OnConnect,
@ -75,6 +75,8 @@ const selector = createMemoizedSelector(stateSelector, ({ nodes }) => {
};
});
const snapGrid: [number, number] = [25, 25];
export const Flow = () => {
const dispatch = useAppDispatch();
const nodes = useAppSelector((state) => state.nodes.nodes);
@ -87,6 +89,13 @@ export const Flow = () => {
const [borderRadius] = useToken('radii', ['base']);
const flowStyles = useMemo<CSSProperties>(
() => ({
borderRadius,
}),
[borderRadius]
);
const onNodesChange: OnNodesChange = useCallback(
(changes) => {
dispatch(nodesChanged(changes));
@ -266,10 +275,10 @@ export const Flow = () => {
isValidConnection={isValidConnection}
minZoom={0.1}
snapToGrid={shouldSnapToGrid}
snapGrid={[25, 25]}
snapGrid={snapGrid}
connectionRadius={30}
proOptions={proOptions}
style={{ borderRadius }}
style={flowStyles}
onPaneClick={handlePaneClick}
deleteKeyCode={DELETE_KEYS}
selectionMode={selectionMode}

View File

@ -3,6 +3,7 @@ import { stateSelector } from 'app/store/store';
import { useAppSelector } from 'app/store/storeHooks';
import { colorTokenToCssVar } from 'common/util/colorTokenToCssVar';
import { getFieldColor } from 'features/nodes/components/flow/edges/util/getEdgeColor';
import type { CSSProperties } from 'react';
import { memo } from 'react';
import type { ConnectionLineComponentProps } from 'reactflow';
import { getBezierPath } from 'reactflow';
@ -27,6 +28,8 @@ const selector = createMemoizedSelector(stateSelector, ({ nodes }) => {
};
});
const pathStyles: CSSProperties = { opacity: 0.8 };
const CustomConnectionLine = ({
fromX,
fromY,
@ -56,7 +59,7 @@ const CustomConnectionLine = ({
strokeWidth={2}
className={className}
d={dAttr}
style={{ opacity: 0.8 }}
style={pathStyles}
/>
</g>
);

View File

@ -47,21 +47,20 @@ const InvocationCollapsedEdge = ({
const { base500 } = useChakraThemeTokens();
const edgeStyles = useMemo(
() => ({
strokeWidth: isSelected ? 3 : 2,
stroke: base500,
opacity: isSelected ? 0.8 : 0.5,
animation: shouldAnimate ? 'dashdraw 0.5s linear infinite' : undefined,
strokeDasharray: shouldAnimate ? 5 : 'none',
}),
[base500, isSelected, shouldAnimate]
);
return (
<>
<BaseEdge
path={edgePath}
markerEnd={markerEnd}
style={{
strokeWidth: isSelected ? 3 : 2,
stroke: base500,
opacity: isSelected ? 0.8 : 0.5,
animation: shouldAnimate
? 'dashdraw 0.5s linear infinite'
: undefined,
strokeDasharray: shouldAnimate ? 5 : 'none',
}}
/>
<BaseEdge path={edgePath} markerEnd={markerEnd} style={edgeStyles} />
{data?.count && data.count > 1 && (
<EdgeLabelRenderer>
<Flex

View File

@ -1,4 +1,5 @@
import { useAppSelector } from 'app/store/storeHooks';
import type { CSSProperties } from 'react';
import { memo, useMemo } from 'react';
import type { EdgeProps } from 'reactflow';
import { BaseEdge, getBezierPath } from 'reactflow';
@ -42,19 +43,18 @@ const InvocationDefaultEdge = ({
targetPosition,
});
return (
<BaseEdge
path={edgePath}
markerEnd={markerEnd}
style={{
strokeWidth: isSelected ? 3 : 2,
stroke,
opacity: isSelected ? 0.8 : 0.5,
animation: shouldAnimate ? 'dashdraw 0.5s linear infinite' : undefined,
strokeDasharray: shouldAnimate ? 5 : 'none',
}}
/>
const edgeStyles = useMemo<CSSProperties>(
() => ({
strokeWidth: isSelected ? 3 : 2,
stroke,
opacity: isSelected ? 0.8 : 0.5,
animation: shouldAnimate ? 'dashdraw 0.5s linear infinite' : undefined,
strokeDasharray: shouldAnimate ? 5 : 'none',
}),
[isSelected, shouldAnimate, stroke]
);
return <BaseEdge path={edgePath} markerEnd={markerEnd} style={edgeStyles} />;
};
export default memo(InvocationDefaultEdge);

View File

@ -7,8 +7,9 @@ import { InvText } from 'common/components/InvText/wrapper';
import NextPrevImageButtons from 'features/gallery/components/NextPrevImageButtons';
import NodeWrapper from 'features/nodes/components/flow/nodes/common/NodeWrapper';
import { DRAG_HANDLE_CLASSNAME } from 'features/nodes/types/constants';
import type { AnimationProps } from 'framer-motion';
import { motion } from 'framer-motion';
import type { PropsWithChildren } from 'react';
import type { CSSProperties, PropsWithChildren } from 'react';
import { memo, useCallback, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { useSelector } from 'react-redux';
@ -106,25 +107,10 @@ const Wrapper = (props: PropsWithChildren<{ nodeProps: NodeProps }>) => {
{isHovering && (
<motion.div
key="nextPrevButtons"
initial={{
opacity: 0,
}}
animate={{
opacity: 1,
transition: { duration: 0.1 },
}}
exit={{
opacity: 0,
transition: { duration: 0.1 },
}}
style={{
position: 'absolute',
top: 40,
left: -2,
right: -2,
bottom: 0,
pointerEvents: 'none',
}}
initial={initial}
animate={animate}
exit={exit}
style={styles}
>
<NextPrevImageButtons />
</motion.div>
@ -134,3 +120,23 @@ const Wrapper = (props: PropsWithChildren<{ nodeProps: NodeProps }>) => {
</NodeWrapper>
);
};
const initial: AnimationProps['initial'] = {
opacity: 0,
};
const animate: AnimationProps['animate'] = {
opacity: 1,
transition: { duration: 0.1 },
};
const exit: AnimationProps['exit'] = {
opacity: 0,
transition: { duration: 0.1 },
};
const styles: CSSProperties = {
position: 'absolute',
top: 40,
left: -2,
right: -2,
bottom: 0,
pointerEvents: 'none',
};

View File

@ -10,6 +10,8 @@ interface Props {
nodeId: string;
}
const hiddenHandleStyles: CSSProperties = { visibility: 'hidden' };
const InvocationNodeCollapsedHandles = ({ nodeId }: Props) => {
const data = useNodeData(nodeId);
const { base600 } = useChakraThemeTokens();
@ -26,6 +28,15 @@ const InvocationNodeCollapsedHandles = ({ nodeId }: Props) => {
[base600]
);
const collapsedTargetStyles: CSSProperties = useMemo(
() => ({ ...dummyHandleStyles, left: '-0.5rem' }),
[dummyHandleStyles]
);
const collapsedSourceStyles: CSSProperties = useMemo(
() => ({ ...dummyHandleStyles, right: '-0.5rem' }),
[dummyHandleStyles]
);
if (!isInvocationNodeData(data)) {
return null;
}
@ -37,7 +48,7 @@ const InvocationNodeCollapsedHandles = ({ nodeId }: Props) => {
id={`${data.id}-collapsed-target`}
isConnectable={false}
position={Position.Left}
style={{ ...dummyHandleStyles, left: '-0.5rem' }}
style={collapsedTargetStyles}
/>
{map(data.inputs, (input) => (
<Handle
@ -46,7 +57,7 @@ const InvocationNodeCollapsedHandles = ({ nodeId }: Props) => {
id={input.name}
isConnectable={false}
position={Position.Left}
style={{ visibility: 'hidden' }}
style={hiddenHandleStyles}
/>
))}
<Handle
@ -54,7 +65,7 @@ const InvocationNodeCollapsedHandles = ({ nodeId }: Props) => {
id={`${data.id}-collapsed-source`}
isConnectable={false}
position={Position.Right}
style={{ ...dummyHandleStyles, right: '-0.5rem' }}
style={collapsedSourceStyles}
/>
{map(data.outputs, (output) => (
<Handle
@ -63,7 +74,7 @@ const InvocationNodeCollapsedHandles = ({ nodeId }: Props) => {
id={output.name}
isConnectable={false}
position={Position.Right}
style={{ visibility: 'hidden' }}
style={hiddenHandleStyles}
/>
))}
</>

View File

@ -4,6 +4,7 @@ import { Flex } from '@chakra-ui/react';
import QueueControls from 'features/queue/components/QueueControls';
import ResizeHandle from 'features/ui/components/tabs/ResizeHandle';
import { usePanelStorage } from 'features/ui/hooks/usePanelStorage';
import type { CSSProperties } from 'react';
import { memo, useCallback, useRef, useState } from 'react';
import type { ImperativePanelGroupHandle } from 'react-resizable-panels';
import { Panel, PanelGroup } from 'react-resizable-panels';
@ -11,6 +12,8 @@ import { Panel, PanelGroup } from 'react-resizable-panels';
import InspectorPanel from './inspector/InspectorPanel';
import WorkflowPanel from './workflow/WorkflowPanel';
const panelGroupStyles: CSSProperties = { height: '100%', width: '100%' };
const NodeEditorPanelGroup = () => {
const [isTopPanelCollapsed, setIsTopPanelCollapsed] = useState(false);
const [isBottomPanelCollapsed, setIsBottomPanelCollapsed] = useState(false);
@ -31,7 +34,7 @@ const NodeEditorPanelGroup = () => {
id="workflow-panel-group"
autoSaveId="workflow-panel-group"
direction="vertical"
style={{ height: '100%', width: '100%' }}
style={panelGroupStyles}
storage={panelStorage}
>
<Panel