Skip to content

Commit

Permalink
Add improved Animation Updater
Browse files Browse the repository at this point in the history
In preparation for the zoom behavior, the animations are improved.
The AnimationUpdater is responsible for keeping the connections between
nodes responsive, i. e. the Edges and Arrows are updated when the
nodes change. Nodes change due to expanding their height, expanding
recursion or changing the width of the window. During those changes, the
animation updater changes state, so that the Edges and Arrows rerender.

Height: Whenever the nodes change height, the animationUpdater is
started. It updates the bounding rect of the nodes in the Aminmation
Updater's state variable until the stopAnimationUpdater is called.

Width: a ResizeObserver is added, to handle changes to nodes, when they
don't change height, but width.

Resolves: #61
Contributes: #71
  • Loading branch information
stephanzwicknagl committed Mar 12, 2024
1 parent 70cc086 commit 7c97bcd
Show file tree
Hide file tree
Showing 7 changed files with 161 additions and 55 deletions.
86 changes: 55 additions & 31 deletions frontend/src/lib/components/Arrows.react.js
@@ -1,47 +1,71 @@
import React from "react";
import { useHighlightedSymbol } from "../contexts/HighlightedSymbol";
import { useShownRecursion } from "../contexts/ShownRecursion";
import Xarrow from "react-xarrows";
import { useAnimationUpdater } from "../contexts/AnimationUpdater";
import PropTypes from 'prop-types'


function getRandomKey() {
return Math.random();
}

export function Arrows() {
const { highlightedSymbol } = useHighlightedSymbol();
const [shownRecursion, , ] = useShownRecursion();
// state to update Arrows after height animation of node
const {highlightedSymbol} = useHighlightedSymbol();
const [arrows, setArrows] = React.useState([]);
const [value, , ,] = useAnimationUpdater();

const calculateArrows = React.useCallback(() => {
return highlightedSymbol.map(arrow => {
const suffix1 = `_${document.getElementById(arrow.src+"_main")?"main":"sub"}`;
const suffix2 = `_${document.getElementById(arrow.tgt+"_main")?"main":"sub"}`;
return {"src": arrow.src + suffix1, "tgt": arrow.tgt + suffix2, "color": arrow.color};
}).filter(arrow => {
// filter false arrows that are not in the DOM
return document.getElementById(arrow.src) && document.getElementById(arrow.tgt)
}).map(arrow => {
return <Xarrow
key={arrow.src + "-" + arrow.tgt} start={arrow.src} end={arrow.tgt} startAnchor={"top"} endAnchor={"bottom"} color={arrow.color} strokeWidth={2} headSize={5} zIndex={10} />
})
return highlightedSymbol
.map((arrow) => {
const suffix1 = `_${
document.getElementById(arrow.src + '_main')
? 'main'
: 'sub'
}`;
const suffix2 = `_${
document.getElementById(arrow.tgt + '_main')
? 'main'
: 'sub'
}`;
return {
src: arrow.src + suffix1,
tgt: arrow.tgt + suffix2,
color: arrow.color,
};
})
.filter((arrow) => {
// filter false arrows that are not in the DOM
return (
document.getElementById(arrow.src) &&
document.getElementById(arrow.tgt)
);
})
.map((arrow) => {
return (
<Xarrow
key={getRandomKey()}
start={arrow.src}
end={arrow.tgt}
startAnchor={'auto'}
endAnchor={'auto'}
color={arrow.color}
strokeWidth={2}
headSize={5}
zIndex={10}
/>
);
});
}, [highlightedSymbol]);

const arrows = calculateArrows();


// React.useEffect(() => {
// // arrows = [];
// arrows = calculateArrows();
// onFullyLoaded(() => {arrows = calculateArrows()});
// }, [ highlightedSymbol, shownRecursion ])
React.useEffect(() => {
setArrows(calculateArrows());
}, [value, highlightedSymbol, calculateArrows]);

function onFullyLoaded(callback) {
setTimeout(function () {
requestAnimationFrame(callback)
})
}
return <div className="arrows_container">
{arrows.length > 0 ? arrows : null}
</div>
return (
<div className="arrows_container">
{arrows.length > 0 ? arrows : null}
</div>
);
}

Arrows.propTypes = {
Expand Down
2 changes: 1 addition & 1 deletion frontend/src/lib/components/Edges.react.js
Expand Up @@ -9,7 +9,7 @@ import { useEdges } from "../contexts/Edges";
export function Edges(props) {
const colorPalete = useColorPalette();
const { edges } = useEdges();
const [value, , ,] = useAnimationUpdater();
const [value, , , ,] = useAnimationUpdater();


return <div className="edge_container">
Expand Down
83 changes: 67 additions & 16 deletions frontend/src/lib/components/Node.react.js
Expand Up @@ -259,34 +259,77 @@ function useHighlightedNodeToCreateClassName(node) {
}

export function Node(props) {
const { node, showMini, isSubnode } = props;
const {node, showMini, isSubnode} = props;
const [isOverflowV, setIsOverflowV] = React.useState(false);
const colorPalette = useColorPalette();
const { dispatch: dispatchShownNodes } = useShownNodes();
const {dispatch: dispatchShownNodes} = useShownNodes();
const classNames = useHighlightedNodeToCreateClassName(node);
const [height, setHeight] = React.useState(minimumNodeHeight);
const [expandNode, setExpandNode] = React.useState(false);
// state updater to force other components to update
const [, , startAnimationUpdater, stopAnimationUpdater] = useAnimationUpdater();
const { setShownDetail } = useShownDetail();

// state updater to force otheuseAnimationUpdaterr components to update
const [
,
setValue,
startAnimationUpdater,
stopAnimationUpdater,
animationUpdater,
] = useAnimationUpdater();
const setValueRef = React.useRef(setValue);
const stopAnimationUpdaterRef = React.useRef(stopAnimationUpdater);
const {setShownDetail} = useShownDetail();

const dispatchShownNodesRef = React.useRef(dispatchShownNodes);
const nodeuuidRef = React.useRef(node.uuid);

const notifyClick = (node) => {
setShownDetail(node.uuid);
}
};
React.useEffect(() => {
const dispatch = dispatchShownNodesRef.current;
const nodeuuid = nodeuuidRef.current
dispatch(showNode(nodeuuid))
const nodeuuid = nodeuuidRef.current;
dispatch(showNode(nodeuuid));
return () => {
dispatch(hideNode(nodeuuid))
}
}, [])
dispatch(hideNode(nodeuuid));
};
}, []);

React.useEffect(() => {
const setValuec = setValueRef.current;
const nodeuuid = nodeuuidRef.current;
const stopAnimationUpdater = stopAnimationUpdaterRef.current;
setValuec((oldValue) => ({...oldValue, [nodeuuid]: null}));
return () => {
stopAnimationUpdater(nodeuuid);
setValuec((v) => {
const {[nodeuuid]: _, ...rest} = v;
return rest;
});
};
}, []);

const divID = `${node.uuid}_animate_height`;

React.useEffect(() => {
const resizeObserver = new ResizeObserver((entries) => {
for (let entry of entries) {
window.requestAnimationFrame(() => {
animationUpdater(node.uuid, 0);
});
}
});

const divElement = document.getElementById(node.uuid);
if (divElement) {
resizeObserver.observe(divElement);
}

return () => {
if (divElement) {
resizeObserver.unobserve(divElement);
}
};
}, [animationUpdater, node.uuid]);

return (
<div
className={classNames}
Expand All @@ -309,14 +352,22 @@ export function Node(props) {
className={'mini'}
/>
) : (
<div className={`set_too_high ${node.uuid.includes('loading') ? 'loading' : null}`}>
<div
className={`set_too_high ${
node.uuid.includes('loading') ? 'loading' : null
}`}
>
<AnimateHeight
id={divID}
duration={500}
height={height}
onHeightAnimationStart={startAnimationUpdater}
onHeightAnimationEnd={stopAnimationUpdater}
>
onHeightAnimationStart={() =>
startAnimationUpdater(node.uuid)
}
onHeightAnimationEnd={() =>
setTimeout(stopAnimationUpdater(node.uuid), 250)
}
>
<NodeContent
node={node}
setHeight={setHeight}
Expand Down
2 changes: 1 addition & 1 deletion frontend/src/lib/components/node.css
Expand Up @@ -9,7 +9,7 @@
.node_border {
border-radius: 10px 10px 10px 10px;
border: 2px solid;
margin: 15px 5px 15px 5px;
margin: 15px 3% 15px 3%;
position: relative;
height: max-content;
overflow: hidden;
Expand Down
39 changes: 35 additions & 4 deletions frontend/src/lib/contexts/AnimationUpdater.js
@@ -1,18 +1,49 @@
import React from "react";
import PropTypes from "prop-types";

const animationFPS = 24;
const millisecondsPerSecond = 1000;
const animationInterval = 1 / animationFPS * millisecondsPerSecond;
const defaultAnimationUpdater = () => { };

const AnimationUpdater = React.createContext(defaultAnimationUpdater);

export const useAnimationUpdater = () => React.useContext(AnimationUpdater);
export const AnimationUpdaterProvider = ({ children }) => {
const [value, setValue] = React.useState(0);
const startAnimationUpdater = () => setInterval(() => setValue(value => value + 1), 25);
const stopAnimationUpdater = () => clearInterval(startAnimationUpdater);
const [value, setValue] = React.useState(Object());
const intervalIds = {};

const animationUpdater = (nodeuuid, newHeight) => {
setValue((oldValue) => {
const element = document.getElementById(nodeuuid);
const rect = element
? element.getBoundingClientRect()
: {};
return {
...oldValue,
[nodeuuid]: rect,
};
});
};

const startAnimationUpdater = (nodeuuid, newHeight) => {
if (intervalIds[nodeuuid]) {
return;
}
intervalIds[nodeuuid] = setInterval(
() => animationUpdater(nodeuuid, newHeight),
animationInterval
);
};
const stopAnimationUpdater = (nodeuuid) => {
if (intervalIds[nodeuuid]) {
clearInterval(intervalIds[nodeuuid]);
delete intervalIds[nodeuuid];
}
};

return <AnimationUpdater.Provider
value={[value, setValue, startAnimationUpdater, stopAnimationUpdater]}>{children}</AnimationUpdater.Provider>
value={[value, setValue, startAnimationUpdater, stopAnimationUpdater, animationUpdater]}>{children}</AnimationUpdater.Provider>
}

AnimationUpdaterProvider.propTypes = {
Expand Down
2 changes: 1 addition & 1 deletion frontend/viasp_dash/viasp_dash.min.js

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion frontend/viasp_dash/viasp_dash.min.js.map

Large diffs are not rendered by default.

0 comments on commit 7c97bcd

Please sign in to comment.