mirror of
https://github.com/invoke-ai/InvokeAI
synced 2024-08-30 20:32:17 +00:00
feat(ui): use stable objects for animation/native element styles
This commit is contained in:
committed by
Kent Keirsey
parent
f5194f9e2d
commit
ca4b8e65c1
@ -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"
|
||||
|
@ -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}
|
||||
|
@ -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>
|
||||
);
|
||||
|
@ -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
|
||||
|
@ -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);
|
||||
|
@ -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',
|
||||
};
|
||||
|
@ -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}
|
||||
/>
|
||||
))}
|
||||
</>
|
||||
|
@ -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
|
||||
|
Reference in New Issue
Block a user