victory-core
Version:
132 lines (124 loc) • 3.92 kB
JavaScript
import React from "react";
import * as d3Ease from "victory-vendor/d3-ease";
import { victoryInterpolator } from "./util";
import TimerContext from "../victory-util/timer-context";
/**
* Single animation object to interpolate
*/
/**
* Animation styles to interpolate
*/
/** d3-ease changed the naming scheme for ease from "linear" -> "easeLinear" etc. */
const formatAnimationName = name => {
const capitalizedName = name.charAt(0).toUpperCase() + name.slice(1);
return `ease${capitalizedName}`;
};
const DEFAULT_DURATION = 1000;
export const VictoryAnimation = _ref => {
let {
duration = DEFAULT_DURATION,
easing = "quadInOut",
delay = 0,
data,
children,
onEnd
} = _ref;
const [state, setState] = React.useState({
data: Array.isArray(data) ? data[0] : data,
animationInfo: {
progress: 0,
animating: false
}
});
const timer = React.useContext(TimerContext).animationTimer;
const queue = React.useRef(Array.isArray(data) ? data.slice(1) : []);
const interpolator = React.useRef(null);
const loopID = React.useRef(undefined);
const ease = d3Ease[formatAnimationName(easing)];
React.useEffect(() => {
// Length check prevents us from triggering `onEnd` in `traverseQueue`.
if (queue.current.length) {
traverseQueue();
}
// Clean up the animation loop
return () => {
if (loopID.current) {
timer.unsubscribe(loopID.current);
} else {
timer.stop();
}
};
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
React.useEffect(() => {
// If the previous animation didn't finish, force it to complete before starting a new one
if (interpolator.current && state.animationInfo && state.animationInfo.progress < 1) {
setState({
data: interpolator.current(1),
animationInfo: {
progress: 1,
animating: false,
terminating: true
}
});
} else {
// Cancel existing loop if it exists
timer.unsubscribe(loopID.current);
// Set the tween queue to the new data
queue.current = Array.isArray(data) ? data : [data];
// Start traversing the tween queue
traverseQueue();
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [data]);
const traverseQueue = () => {
if (queue.current.length) {
const nextData = queue.current[0];
// Compare cached version to next props
interpolator.current = victoryInterpolator(state.data, nextData);
// Reset step to zero
if (delay) {
setTimeout(() => {
loopID.current = timer.subscribe(functionToBeRunEachFrame, duration);
}, delay);
} else {
loopID.current = timer.subscribe(functionToBeRunEachFrame, duration);
}
} else if (onEnd) {
onEnd();
}
};
const functionToBeRunEachFrame = elapsed => {
if (!interpolator.current) return;
// Step can generate imprecise values, sometimes greater than 1
// if this happens set the state to 1 and return, cancelling the timer
const step = duration ? elapsed / duration : 1;
if (step >= 1) {
setState({
data: interpolator.current(1),
animationInfo: {
progress: 1,
animating: false,
terminating: true
}
});
if (loopID.current) {
timer.unsubscribe(loopID.current);
}
queue.current.shift();
traverseQueue();
return;
}
// If we're not at the end of the timer, set the state by passing
// current step value that's transformed by the ease function to the
// interpolator, which is cached for performance whenever props are received
setState({
data: interpolator.current(ease(step)),
animationInfo: {
progress: step,
animating: step < 1
}
});
};
return children(state.data, state.animationInfo);
};