Animating React Components
April 19, 2020
Let's make a react component that can be used to animate anything directly inside MDX files:
The above Animation uses a component called AnimationFrame which wraps the Plot component from last post:
<AnimationFrame autostart>
{({ time }) => {
const t = time.fromStart / 250;
return (
<Plot
functions={[(x) => Math.sin(3 * x) * Math.sin(t / 2), (x) => Math.sin(2 * x) * Math.sin(t / 3)]}
range={{ x: [0, Math.PI], y: [-1, 1] }}
height={200}
strokeWidth={4}
hideXAxis={true}
/>
);
}}
</AnimationFrame>
AnimationFrame component
The AnimationFrame component is relatively straightforward as it essentially just uses a custom hook called useFrame:
export default function AnimationFrame(props) {
const [time, setTime] = useState({
fromStart: 0,
fromFirstStart: 0,
progress: 0,
delta: null,
});
const frame = useFrame(setTime, props.autostart);
return props.children({ ...frame, time });
}
useFrame
The more interesting useFrame hook:
export default function useFrame(callback, autostart = false) {
const requestRef = useRef<number>();
const previousTimeRef = useRef<number>();
const startTimeRef = useRef<number>();
const stopTimeRef = useRef<number>(0);
const maxTimeRef = useRef<number>();
const [isRunning, setIsRunning] = useState(autostart);
const animate = time => {
if (!startTimeRef.current) {
startTimeRef.current = time;
}
if (previousTimeRef.current != undefined) {
const delta = time - previousTimeRef.current
const fromStart = time - startTimeRef.current;
callback({
time,
delta,
fromStart,
fromFirstStart: fromStart + stopTimeRef.current,
stopTime: stopTimeRef.current,
progress: maxTimeRef.current ? fromStart / maxTimeRef.current : 0
});
if (maxTimeRef.current && fromStart >= maxTimeRef.current) {
stop();
maxTimeRef.current = null;
return;
}
}
previousTimeRef.current = time;
requestRef.current = requestAnimationFrame(animate);
}
function start(maxTime?) {
if (isRunning) {
stop();
}
if (maxTime) {
maxTimeRef.current = maxTime;
}
requestRef.current = requestAnimationFrame(animate);
setIsRunning(true);
}
function stop() {
if (previousTimeRef.current && startTimeRef.current) {
stopTimeRef.current = previousTimeRef.current - startTimeRef.current + stopTimeRef.current;
}
cancelAnimationFrame(requestRef.current);
startTimeRef.current = 0;
requestRef.current = null;
setIsRunning(false)
}
function toggle() {
if (!requestRef.current) {
start()
} else {
stop();
}
}
React.useEffect(() => {
autostart && start();
return () => stop()
}, []); // Make sure the effect runs only once
return { start, stop, toggle, isRunning };
}
In its essence, it takes a callback that is repeatedly invoked inside requestAnimationFrame. When start is called, the animation begins.
Perspectives on time
The callback is passed different perspectives on time:
- time: total time elapsed since document creation (default requestAnimation param)
- delta: time elapsed since last frame e.g. 17ms for 60fps
- fromStart: time elapsed since animation was started
- fromFirstStart: time elapsed since animation was started the first time
- stopTime: the last time the animation was stopped
fromStart and fromFirstStart should be the most interesting for most use cases:
time.fromStart: 0s
time.fromFirstStart: 0s
time.delta: 0ms = Infinityfps
progress
We can turn the continuus animation to a timed one by passing a maxTime to the start method. If doing so, the callback param progress will contain a number between 0 and 1.
0%
React Animation libs
Of course, there are several libs for animating in the react world:
- https://reactcommunity.org/react-transition-group/
- https://www.react-spring.io/
- https://github.com/nearform/react-animation
But they are mainly focused on one time animations rather than on infinite time flow. Only react spring would allow never ending animations using a spring tension of 0, but still the values are sprining between number bounds that are only indirectly controllable via spring constants.
d3-transition
When using d3, we could use d3-transition, but this is only possible in combination with d3-selection, which I try to avoid, as I want to control the DOM from React as direct as possible. Also, like with the libs above, it is not really elegant to do infinite animations
TBD
- check out https://github.com/d3/d3-timer