Animating transitions between path d attributes cause Complex Values errors.
In react-pose, we discussed the approach for this using custom interpolation via d3-interpolate. Reference: https://github.com/Popmotion/popmotion/issues/645
How would one convert something like the following to motion?
import posed from 'react-pose';
import { spring } from 'popmotion';
import { interpolate } from 'd3-interpolate';
// Handle complex transitions with d3-interpolate
export const d3Interpolate = ({ from, to }) =>
spring({
from: 0,
to: 1,
velocity: 10,
damping: 100
}).pipe(interpolate(from, to));
// Common Transition Maps
export const transition = {
d: d3Interpolate,
transform: d3Interpolate
};
// Simple Example of Path Transition
export const PosedPath = posed.path({
enter: {
d: ({ enterProps }) => enterProps.d
transition
},
exit: {
d: ({ exitProps }) => exitProps.d
}
You can see the pose implementation live in reaviz:
I think I might have a solution, would love to get feedback though...
import React, { useEffect } from 'react';
import { motion, useMotionValue, useSpring } from 'framer-motion';
import { interpolate } from 'd3-interpolate';
const variants = {
initial: ({ exitProps }) => exitProps,
animate: ({ enterProps }) => enterProps
};
export const MotionPath = ({ custom, ...rest }) => {
const d = useMotionValue(custom.exitProps.d);
const prevPath = useMotionValue(custom.exitProps.d);
const spring = useSpring(prevPath, {
from: 0,
to: 1,
velocity: 10,
damping: 50
});
useEffect(() => {
const interpolator = interpolate(prevPath.get(), custom.enterProps.d);
spring.onChange(v => d.set(interpolator(v)));
prevPath.set(custom.enterProps.d);
});
return (
<motion.path
{...rest}
initial="initial"
exit="initial"
animate="animate"
variants={variants}
custom={custom}
d={d}
/>
);
};
and then I use it like:
<MotionLine
{...extras}
pointerEvents="none"
stroke={stroke}
strokeWidth={strokeWidth}
fill="none"
transition={transition}
custom={{
enterProps,
exitProps
}}
/>
Here is the code I ended up with: https://github.com/jask-oss/reaviz/blob/master/src/common/Motion/MotionPath.tsx
Yeah I think that's the best approach for now, you can of course also use Popmotion to drive the animation. But I am thinking of better ways of accomplishing this in the meantime. I think this https://github.com/framer/motion/issues/353 would be a good start, potentially there's also room for attaching a custom interpolator to a MotionValue.
Custom interpolation using d3-interpolate-path was also something I needed. I got it working in a slightly different way (shown below) but it would be great to have the ability to pass it directly to components.
import { tween, ColdSubscription } from 'popmotion';
import { interpolatePath } from 'd3-interpolate-path';
const usePathMorph = (d: null | string) => {
const ref = useRef<SVGPathElement>(null);
const refTween = useRef<ColdSubscription>();
useEffect(() => {
if (ref.current) {
const dAttrVal = ref.current.getAttribute('d');
if (d && !dAttrVal) {
ref.current.setAttribute('d', d);
} else if (d && dAttrVal && d !== dAttrVal) {
if (refTween.current) {
refTween.current.stop();
}
refTween.current = tween()
.pipe(interpolatePath(dAttrVal, d))
.start((v: string) => {
if (ref.current) {
ref.current.setAttribute('d', v);
}
});
}
}
}, [d]);
return ref;
};
Then use it like.
const { d } = props;
const ref = usePathMorph(d);
<motion.path ref={ ref } />
There's probably an edge case in there somewhere.
Edit: Updated hook for interrupting tween