Alternate between two elements in an infinite loop in framer-motion - framer-motion

I have an animation containing one svg element that rotates and then changes color during rotation.
What I want is that, at the second rotation it fades out and a new svg image takes it's place and then the same happens in reverse.
So, Imagine I have SvgA and SvgB, I want to make SvgA rotate, then wait, rotate again and when the rotation finishes SvgB should be at it's place to do exactly the same on an infinite loop.
Animations can happen infinitely using a transition, but that only applies to a single component. I think I could use AnimatePresence and AnimateSharedLayout to swap between the components, but I am unsure about how to "toggle" between them on an infinite loop.
Maybe I can use variants + animation controls and an infinite asynchronous loop to run them as long as the component exists, but I am not sure how to orchestrate it.
This is the animation that I have so far:
// primary and secondary are defined colors
const animation = {
scale: [1, 2, 2, 1, 1],
rotate: [0, 0, 360, 360, 0],
color: [primary, primary, secondary, secondary, primary],
};
const transition = {
duration: 2,
ease: "easeInOut",
times: [0, 0.2, 0.5, 0.8, 1],
loop: Infinity,
repeatDelay: 1,
};
<motion.div animate={animation} transition={transition}>
<IconA className={css.icon} />
</motion.div>

<motion.div transition={ default: {repeat: Infinity} } />
This will make sure your element will loop through the animation for ever

Related

Nextjs x Framer Motion inertial scrolling with page transition

I've been working on an inertial scrolling implementation within Nextjs using Framer Motion but running into a bit of trouble when animation page transitions using animate presence. I'm hooking into the page scroll progress via useScroll and animating the Y transform with spring physics. The issue, however, is when resetting the window scroll position when navigating between pages. After the component has unmounted, I call window.scrollTo(0,0) via the AnimatePresence onExitComplete callback, but because I'm transforming the page position via spring physics, the scroll reset doesn't complete immediately. So when new pages are mounted, the page transform to reset the scroll is still running.
Additionally, I've included scroll={false} in my Next Link components to prevent the default scroll to top functionality so I can handle this manually via the AnimatePresence component as mentioned above. But this doesn't seem to be working.
There's a bit of code involved so I created a minimal reproduction repo here.
I also notice that passing scrollY to the y property of the variant object prevents the unwanted scrolling behavior on page transition. But passing something like scrollY - 128 breaks the behavior:
const variants = {
hidden: { opacity: 0, x: 0, y: 64 },
enter: { opacity: 1, x: 0, y: 0 },
exit: { opacity: 0, x: 0, y: scrollY },
};
y value in exit variant wasn't being updated to match latest scrollY value. Solution:
useEffect(() => {
return scrollY.onChange((latest) => {
variants.exit.y = -scrollY.current - 128;
});
});

Framer Motion Does not Loop

I'm trying to loop an animation with the Framer Motion library after a small delay. The animation itself will play when it's mounted, on the webpage refresh, however does not repeat. Iv'e looked through the docs and this seems to be similar syntax.
const AnimateTrial = {
initial: {
opacity: 0,
x: -100,
y: -35,
scale: 0.9,
},
animate: (i) => {
const delay = 5 + i * 0.5;
return {
opacity: 1,
x: -10,
y: -35,
transition: {
opacity: { delay, duration: 1.5 },
x: { delay, duration: 1.5},
repeatType: "Infinity",
repeatDelay: 5,
},
};
}
I have also tried passing:
transition={{ repeat: Infinity, repeatDelay: 5 }}
But am yet to have it work. Any ideas!
I can confirm the remainder of the animation works, but it does not loop.
Having never laid eyes upon a transition object nested inside an animate function, I had to take this to code sandbox.
I'm new to Framer Motion myself but there are multiple things that strike me as odd and how I would fix them:
Setting a duration on several properties within transition. I don't believe this is a supported feature, so I'm moving it for this answer as both your opacity and x properties share the same duration.
transition: {
duration: 1.5,
opacity: delay,
x: delay,
repeatType: "Infinity",
repeatDelay: 5
}
Setting a repeatType without a repeat will not loop the animation. Setting repeat: Infinite will (and you can drop the repeatType).
transition: {
duration: 1.5,
opacity: delay,
x: delay,
repeat: Infinity,
repeatDelay: 5
}
Note that we still do not have a repeating animation.
Using animate as a function. I'm not sure, but I think the entire function might only get evaluated once and immediately discarded. I believe that the second time Framer Motion looks at the delay value it fails, thus not animating that specific property. (I am also not sure what you are trying to achieve with using delay inside the x and opacity properties.)
If we change either the opacity or x to use 0 as a value rather than delay, you can see that it will behave as expected - whereas the unchanged property only animates once. Changing them both will display the looping behavior you are looking for:
transition: {
duration: 1.5,
opacity: 0,
x: 0,
repeat: Infinity,
repeatDelay: 5
}
Thus, we've reached our final code:
const AnimateTrial = {
initial: {
opacity: 0,
x: -100,
y: -35,
scale: 0.9
},
animate: (i) => {
const delay = 5 + i * 0.5; // (now unused)
return {
opacity: 1,
x: -10,
y: -35,
transition: {
duration: 1.5,
opacity: 0,
x: 0,
repeat: Infinity,
repeatDelay: 5
}
};
}
};
Here's a live demo showcasing the working animation loop. (I changed repeatDelay to 1 for testing speed purposes).

Framer Motion text gets distorted when animating with shared layouts

I am in a Next.js enviornment and have wrapped my _app.js with .
Inside a page I have a basic routing set up to jump from page 1 to page 2.
On each page I have a motion h1 which looks like. So there are two components with matching ID's.
const stats = {
visible: {
opacity: 1,
},
hidden: {
opacity: 1,
},
exit: {
opacity: 0,
y: 50,
},
}
<motion.h1
initial="hidden"
animate="visible"
variants={stats}
layout
className="text-3xl text-gray-800 font-bold"
layoutId={`product-title-${data.title}`}
>
{data.title}
</motion.h1>
When I navigate pages the elements animate from their counter parts previous position.. but the text gets all distorted when animating.
How do I fix the distorted text?
You can try giving the value of "position" to your layout prop, instead of true.
layout="position"
As referred in the framer motion documentation
If layout is set to "position", the size of the component will change instantly and only its position will animate.
Since you are animating only position and opacity, it could solve your issue.

CSS transform with SVG and anime.js

What I'm doing
I'm using anime.js to animate some circles along paths in an svg. I create paths using Vue as I'd like the circle on the top to end up on the bottom, so it can't just be a static SVG.
What I expect
I expect the circles to start at one end of the svg path, and finish at the other end.
What's actually happening
I have a demo here:
https://codepen.io/EightArmsHQ/pen/paVxpd
When I click animate, the circles are jumping to a different position, then animating almost correctly, before finishing in the wrong place.
This is the code I am using for anime.js:
for(var c = 0; c < this.coins.length; c++){
var path = anime.path('#path-'+ c);
anime({
targets: document.getElementById(this.coins[c].ref),
translateX: path('x'),
translateY: path('y'),
rotate: path('angle'),
duration: 1000,
delay : this.coins.length * 100 - (100 * c),
easing: 'easeInOutQuad',
rotation: 0,
});
}
I know its almost definitely to do with transform-origin and the rotation transform that is applied to the circles, but I can't get my head around it.
I found after some experimenting the issue was that I was altering the transformX and transformY properties, which, while it sort of worked and would work better for DOM elements, I should have been using cx and cy attributes. I also removed the rotation as well.
for(var c = 0; c < this.coins.length; c++){
var path = anime.path('#path-'+ c);
anime({
targets: document.getElementById(this.coins[c].ref),
cx: path('x'),
cy: path('y'),
duration: 1000,
delay : this.coins.length * 100 - (100 * c),
easing: 'easeInOutQuad',
rotation: 0,
});
}
Here is a demo:
https://codepen.io/EightArmsHQ/pen/aqKXrQ?editors=1010

Use Web Animations API to expand div height 0 --> 'auto'

I'm trying to get my head around the web animations standard and their polyfill, as I saw it work nicely in the Angular animations library (you set an animation end value to '*' and this becomes 100% of the div size, but that uses a special Angular Animations DSL).
I thought I would start with something simple, so all I want to do is expand a div from 0 height to 'auto'. i know there are thousands of other ways to do this, but I was trying to use web-animations-js with this code
The code below (which resembles an MDN example) causes the div to expand directly to 'auto' but after a 1 second delay, whereas I want it smoothly to expand.
let formDiv = document.querySelector("#new-present-form");
formDiv.animate([
{ height: 0},
{ height: 'auto'}
], {
easing: 'ease-in-out',
duration: 1000,
fill: 'forwards' // ensures menu stays open at end of animation
})
By contrast this
formDiv.animate({
height: [0, '100%'],
easing: 'ease-in-out'
}, {
duration: 1000,
fill: 'forwards' // ensures menu stays open at end of animation
})
causes the div to expand immediately, but again with no smooth transition.
This does give a smooth transition, but requires a carefully chosen value to replace '300px' and is precisely what I want to avoid.
formDiv.animate([
{ "height": '0px', offset: 0},
{ "height": '300px', offset: 1}
], {
duration: 1000,
iterations: 1,
fill: 'forwards' // ensure form stays open at end of animation
})
Unfortunately, it is not directly possible to do this with Web Animations or CSS animations/transitions yet. That is because CSS does not have a means to represent an intermediate state between an 'auto' length and a fixed length. The proposal to fix this involves allowing 'auto' inside calc(), e.g. calc(auto + 36px). You can follow the progress of this development on the CSS Transitions Github issue.
In the interim, many people have been able to work around this by animating max-height instead. For example, if you expect your 'auto' height to be somewhere between 300px and 600px, then you could leave height as 'auto', and animate max-height from '0' to '700px'. It's not perfect, but for a short animation it's often close enough.
Alternatively, one could set the height to auto, get the used value of the height using getComputedStyle, and, supposing it returned 375px, create an animation from, height: '0' to height: '375px'. If you do not specify a fill on the animation then when it completes, the computed value of the height will switch from height: 375px to height: auto (which should make no visual difference but mean that the element's height responds to future changes in the content size).
Regarding the error about partial keyframes, that is a short-term issue where both Firefox and Chrome have not shipped support for omitting the first or last keyframe. Both Firefox and Chrome have implemented this feature and it should ship this year, however, it still won't fix this issue until auto is permitted in calc().
Update 22 May with (completely untested) code samples as requested:
// Option 1: Use max-height
formDiv.animate(
{ maxHeight: ['0', '700px'] },
{ easing: 'ease-in-out', duration: 1000 }
);
// Option 2: Use used height
//
// (Beware, flushes layout initially so will probably not be very performant.)
formDiv.style.height = 'auto';
const finalHeight = getComputedStyle(formDiv).height;
formDiv.style.height = '0';
formDiv.animate(
{ height: ['0', finalHeight] },
{ easing: 'ease-in-out', duration: 1000 }
);
Of course if you don't actually have anything below the div you might be able to
get away with a transform animation which will definitely be the most
performant.
// Option 3: Use scale-y transform
formDiv.style.transformOrigin = '50% 0%';
formDiv.animate(
{ transform: ['scaleY(0)', 'scaleY(1)'] },
{ easing: 'ease-in-out', duration: 1000 }
);
If height is not mandatory you can try giving padding to the div.
#keyframes example {
from{padding:0px;}
to{padding: 50px;}
}
Vote up if it was help full.

Resources