Framer Motion Wait to Calculate value before Starting animation - framer-motion

I define x and y values to hide from as state:
const [x, setX] = useState(0);
const [y, setY] = useState(0);
I have the following variant defined:
const transition = {
delay: 0,
duration: 0.5
};
const fromXYVariant = {
visible: {
y: 0,
x: 0,
transition
},
hidden: {
x,
y,
transition
}
};
and use it as
<AnimatePresence>
{!context.isGrouped(towerId2) ? (
<motion.div initial="hidden" animate="visible" exit="hidden" variants={fromXYVariant}>
<div>...</div>
</motion.div>
) : null}
</AnimatePresence>
I set my state variables using some complicated logic. However, I am running into an issue where, on the first "hidden animation" I use x and y as 0, because I haven't calculated them yet for my state.
My solution would be pausing the animation for a half a second or so while I do the math and then animate with the new values.
My question is, how do I pause framer motion from doing the animation while I do the math for the x and y not to be 0?

To pause framer motion from doing the animation you must use delay but instead of 0 as you are doing, you use 0.5 as you wished.
As you can see here:
With delay, you add a pause before the start of the animation (also in seconds). It will only delay the beginning of the initial animation; and not insert pauses between the repetitions.
And also seen in the framer motion documentation:
Delay the animation by this duration (in seconds). Defaults to 0.
const transition = {
delay: 0.2
}
There is also delayChildren:
When using variants, children animations will start after this duration (in seconds). You can add the transition property to both the Frame and the variant directly. Adding it to the variant generally offers more flexibility, as it allows you to customize the delay per visual state.
const container = {
hidden: { opacity: 0 },
show: {
opacity: 1,
transition: {
delayChildren: 0.5
}
}
}
const item = {
hidden: { opacity: 0 },
show: { opacity: 1 }
}
return (
<motion.ul
variants={container}
initial="hidden"
animate="show"
>
<motion.li variants={item} />
<motion.li variants={item} />
</motion.ul>
)

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.

Fixed Background in a Matter.js world

I am looking for a way to set a background fixed to the world. In my case, Render.lookAt targets a moving body and I need to display a fiexd grid behind it to enhance the body’s movements. Is that possible?
The best way to do this is to create a body inside the world with a background sprite.
You can do this like so:
var gridBackground = Bodies.rectangle(0, 0, 1, 1, {
isStatic: true,
isSensor: true,
render: {
sprite: {
texture: "Path/To/Image.png",
xScale: neededScale,
yScale: neededScale
}
}
});
World.add(world, gridBackground);
If your world is infinite, you can also use the grid as a background-image on the body/canvas in css and move it in the opposite direction the player is moving:
Matter.Events.on(engine, "beforeupdate", function() {
var x = player.position.x * -1;
var y = player.position.y * -1;
Matter.Render.lookAt(render, {
min: {y:y-FoV/2, x:x-FoV/2},
max: {y:y+FoV/2, x:x+FoV/2},
});
});

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