Skip to content

Commit

Permalink
feat: 선후관계의 mount, unmount 애니메이션 더 자연스럽도록 위치 변화를 더 명확하게 지정
Browse files Browse the repository at this point in the history
  • Loading branch information
n-ryu committed Dec 12, 2022
1 parent a2fd13a commit bb851d5
Show file tree
Hide file tree
Showing 3 changed files with 158 additions and 68 deletions.
72 changes: 5 additions & 67 deletions client/src/container/diagram/Diagram.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,14 @@ import { ReactElement, useEffect, useState, useRef, useCallback, memo, useMemo }
import { useAtom } from 'jotai';
import { todoList } from '@util/GlobalState';
import styled from 'styled-components';
import { getDiagramData, getTodoBlockProps, getVerticeProps, TodoBlockProps, VertexProps } from '@util/diagram.util';
import { PRIMARY_COLORS } from '@util/Constants';
import TodoBlock from '@components/diagram/TodoBlock';
import TodoVertex from '@components/diagram/TodoVertex';
import TodoBlockPopUp from '@components/diagram/TodoBlockPopUp';
import TodoVertexPopUp from '@components/diagram/TodoVertexPopUp';
import NewTodoVertex from '@components/diagram/NewTodoVertex';
import { toast } from 'react-toastify';
import { useDiagramAnimation } from '@hooks/useDiagramAnimation';

const { offWhite, green } = PRIMARY_COLORS;

Expand Down Expand Up @@ -81,57 +81,6 @@ const defaultNewVertexData: NewVertexData = {
y2: NaN,
};

interface AnimationData<T> {
aniState: string;
props: T;
timeout?: ReturnType<typeof setTimeout>;
}

function getMapWithAniState<T>(
prev: Map<string, AnimationData<T>>,
next: Map<string, T>,
setter: React.Dispatch<React.SetStateAction<Map<string, AnimationData<T>>>>,
): Map<string, AnimationData<T>> {
const result = new Map([...prev]);
const prevMounted = new Map([...prev].filter((el) => el[1].aniState !== 'unmount'));

const mountArr = [...next].filter((el) => !prevMounted.has(el[0]));
mountArr.forEach((el) => {
clearTimeout(prev.get(el[0])?.timeout);
const timeout = setTimeout(() => {
setter((prev) => {
const newState = new Map([...prev]);
const target = newState.get(el[0]);
if (target !== undefined && target.aniState === 'mount')
newState.set(el[0], { props: target.props, aniState: 'idle' });
return newState;
});
}, 0);
result.set(el[0], { aniState: 'mount', timeout, props: el[1] });
});

const updateArr = [...next].filter((el) => prevMounted.has(el[0]));
updateArr.forEach((el) => {
const target = prevMounted.get(el[0]);
if (target !== undefined) result.set(el[0], { aniState: target.aniState, props: el[1] });
});

const unmountArr = [...prevMounted].filter((el) => !next.has(el[0]));
unmountArr.forEach((el) => {
clearTimeout(prev.get(el[0])?.timeout);
const timeout = setTimeout(() => {
setter((prev) => {
const newState = new Map([...prev]);
if (newState.get(el[0])?.aniState === 'unmount') newState.delete(el[0]);
return newState;
});
}, 500);
result.set(el[0], { aniState: 'unmount', timeout, props: el[1].props });
});

return result;
}

const TodoBlockWrapper = styled.div<{ aniState: string }>`
opacity: ${(props) => (props.aniState === 'idle' ? 1 : 0)};
transition: opacity 0.5s;
Expand All @@ -141,25 +90,13 @@ const MemoTodoBlockWrapper = memo(TodoBlockWrapper);

const Diagram = ({ showDone }: { showDone: boolean }): ReactElement => {
const [todoListAtom, setTodoListAtom] = useAtom(todoList);
const [diagramData, setDiagramData] = useState<Map<string, AnimationData<TodoBlockProps>>>(new Map());
const [diagramVertice, setDiagramVertice] = useState<Map<string, AnimationData<VertexProps>>>(new Map());
const { todoBlockData, vertexData } = useDiagramAnimation(todoListAtom, showDone);
const [offset, setOffset] = useState<{ x: number; y: number }>({ x: 100, y: 100 });
const [clickData, setClickData] = useState<ClickData>(defaultClickData);
const [newVertexData, setNewVertexData] = useState<NewVertexData>(defaultNewVertexData);
const [isWheelDown, setIsWheelDown] = useState<boolean>(false);
const domRef = useRef<HTMLDivElement>(null);

useEffect(() => {
getDiagramData(todoListAtom, showDone)
.then((value) => {
setDiagramData((prev) => getMapWithAniState(prev, getTodoBlockProps(value), setDiagramData));
setDiagramVertice((prev) => getMapWithAniState(prev, getVerticeProps(value), setDiagramVertice));
})
.catch((err) => {
throw err;
});
}, [todoListAtom, showDone]);

useEffect(() => {
if (clickData.type === 'Todo' && newVertexData.from !== '') {
const from = newVertexData.from;
Expand Down Expand Up @@ -298,14 +235,15 @@ const Diagram = ({ showDone }: { showDone: boolean }): ReactElement => {
<HorizontalBaseLine style={horizontalLineStyle as React.CSSProperties} />
<VerticalBaseLine style={verticalLineStyle as React.CSSProperties} />
<Wrapper style={diagramStyle as React.CSSProperties} ref={domRef}>
{[...diagramVertice].map((el) => {
{[...vertexData].map((el) => {
console.log(el);
return (
<MemoTodoBlockWrapper key={el[0]} aniState={el[1].aniState}>
<TodoVertex {...el[1].props} getOnClick={getOnClick} />
</MemoTodoBlockWrapper>
);
})}
{[...diagramData].map((el) => {
{[...todoBlockData].map((el) => {
return (
<MemoTodoBlockWrapper key={el[0]} aniState={el[1].aniState}>
<TodoBlock {...el[1].props} getOnClick={getOnClick} />
Expand Down
132 changes: 132 additions & 0 deletions client/src/hooks/useDiagramAnimation.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
import { TodoList } from '@todo/todoList';
import { useEffect, useState } from 'react';
import {
getDiagramData,
getTodoBlockProps,
getVerticeProps,
TodoBlockProps,
VertexProps,
DiagramTodo,
getVertexFromPosition,
getVertexToPosition,
} from '@util/diagram.util';

interface AniData<T> {
aniState: string;
props: T;
timeout?: ReturnType<typeof setTimeout>;
}

type TodoBlockAniData = AniData<TodoBlockProps>;
type VertexAniData = AniData<VertexProps>;

const getUpdatedAniStates = (
prev: Map<string, DiagramTodo>,
next: Map<string, DiagramTodo>,
prevTodoBlockData: Map<string, TodoBlockAniData>,
prevVertexData: Map<string, VertexAniData>,
setTodoBlockData: React.Dispatch<React.SetStateAction<Map<string, TodoBlockAniData>>>,
setVertexData: React.Dispatch<React.SetStateAction<Map<string, VertexAniData>>>,
): void => {
const mountTodo = [...getTodoBlockProps(next)].filter(([key]) => !prev.has(key));
const idleTodo = [...getTodoBlockProps(next)].filter(([key]) => prev.has(key));
const unmountTodo = [...getTodoBlockProps(prev)].filter(([key]) => !next.has(key));
const resultTodoBlockData = new Map([...prevTodoBlockData]);
idleTodo.forEach(([key, value]) => {
const target = resultTodoBlockData.get(key);
if (target === undefined) return;
resultTodoBlockData.set(key, { ...target, props: value });
});
mountTodo.forEach(([key, value]) => {
const target = resultTodoBlockData.get(key);
if (target !== undefined) clearTimeout(target.timeout);
const timeout = setTimeout(() => {
setTodoBlockData((prev) => {
const newState = new Map([...prev]);
newState.set(key, { aniState: 'idle', props: value });
return newState;
});
}, 0);
resultTodoBlockData.set(key, { aniState: 'mount', props: value, timeout });
});
unmountTodo.forEach(([key, value]) => {
const target = resultTodoBlockData.get(key);
if (target !== undefined) clearTimeout(target.timeout);
const timeout = setTimeout(() => {
setTodoBlockData((prev) => {
const newState = new Map([...prev]);
newState.delete(key);
return newState;
});
}, 500);
resultTodoBlockData.set(key, { aniState: 'unmount', props: value, timeout });
});

const prevVertexMap = getVerticeProps(prev);
const nextVertexMap = getVerticeProps(next);
const mountVertex = [...nextVertexMap].filter(([key]) => !prevVertexMap.has(key));
const idleVertex = [...nextVertexMap].filter(([key]) => prevVertexMap.has(key));
const unmountVertex = [...prevVertexMap].filter(([key]) => !nextVertexMap.has(key));
const resultVertexData = new Map([...prevVertexData]);
idleVertex.forEach(([key, value]) => {
const target = resultVertexData.get(key);
if (target === undefined) return;
resultVertexData.set(key, { ...target, props: value });
});
mountVertex.forEach(([key, value]) => {
const target = resultVertexData.get(key);
if (target !== undefined) clearTimeout(target.timeout);
const timeout = setTimeout(() => {
setVertexData((prev) => {
const newState = new Map([...prev]);
newState.set(key, { aniState: 'idle', props: value });
return newState;
});
}, 0);
let props = { ...value };
const [from, to] = key.split('+');
if (prev.has(from)) props = { ...props, ...getVertexFromPosition(prev, from) };
if (prev.has(to)) props = { ...props, ...getVertexToPosition(prev, to) };
resultVertexData.set(key, { aniState: 'mount', props, timeout });
});
unmountVertex.forEach(([key, value]) => {
const target = resultVertexData.get(key);
if (target !== undefined) clearTimeout(target.timeout);
const timeout = setTimeout(() => {
setVertexData((prev) => {
const newState = new Map([...prev]);
newState.delete(key);
return newState;
});
}, 500);
let props = { ...value };
const [from, to] = key.split('+');
if (next.has(from)) props = { ...props, ...getVertexFromPosition(next, from) };
if (next.has(to)) props = { ...props, ...getVertexToPosition(next, to) };
resultVertexData.set(key, { aniState: 'unmount', props, timeout });
});
setTodoBlockData(() => resultTodoBlockData);
setVertexData(() => resultVertexData);
};

export const useDiagramAnimation = (
todoList: TodoList,
showDone: boolean,
): { todoBlockData: Map<string, TodoBlockAniData>; vertexData: Map<string, VertexAniData> } => {
const [, setDiagramData] = useState<Map<string, DiagramTodo>>(new Map());
const [todoBlockData, setTodoBlockData] = useState<Map<string, TodoBlockAniData>>(new Map());
const [vertexData, setVertexData] = useState<Map<string, VertexAniData>>(new Map());
useEffect(() => {
getDiagramData(todoList, showDone)
.then((newData) => {
setDiagramData((prev) => {
getUpdatedAniStates(prev, newData, todoBlockData, vertexData, setTodoBlockData, setVertexData);
return newData;
});
})
.catch((err) => {
throw err;
});
}, [todoList, showDone]);
return { todoBlockData, vertexData };
};
22 changes: 21 additions & 1 deletion client/src/util/diagram.util.ts
Original file line number Diff line number Diff line change
Expand Up @@ -147,6 +147,26 @@ export const getVertexDimension = (
};
};

export const getVertexFromPosition = (todoList: Map<string, DiagramTodo>, id: string): { x1: number; y1: number } => {
const from = todoList.get(id);
if (from === undefined) throw new Error('ERROR: 선후관계가 잘못된 레퍼런스를 참조하고 있습니다.');
const fromPos = calculatePosition(from.order as number, from.depth as number);
return {
x1: fromPos.x + BLOCK.x / 2,
y1: fromPos.y + BLOCK.y,
};
};

export const getVertexToPosition = (todoList: Map<string, DiagramTodo>, id: string): { x2: number; y2: number } => {
const to = todoList.get(id);
if (to === undefined) throw new Error('ERROR: 선후관계가 잘못된 레퍼런스를 참조하고 있습니다.');
const toPos = calculatePosition(to.order as number, to.depth as number);
return {
x2: toPos.x + BLOCK.x / 2,
y2: toPos.y,
};
};

export const validateVertex = (todoList: Map<string, DiagramTodo>, vertex: Vertex): 'NORMAL' | 'WARNING' | 'ERROR' => {
const from = todoList.get(vertex.from);
const to = todoList.get(vertex.to);
Expand Down Expand Up @@ -193,7 +213,7 @@ export const getTodoBlockProps = (todoList: Map<string, DiagramTodo>): Map<strin
);
};

const BOX_OFFSET = 15;
const BOX_OFFSET = 50;

export const getPathValue = (
x1: number,
Expand Down

0 comments on commit bb851d5

Please sign in to comment.