-
-
Notifications
You must be signed in to change notification settings - Fork 3.7k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Transition animations for UI elements #15725
Comments
@mockersf - I think this could dovetail with some of the work you have been doing on interpolation curves. |
There is prior art for this in |
Yup, one of the goal of curves and animations is to be able to be used for this. It's actually already possible, but the API is not a lot friendly for now. I hope to have a better one for the 0.16. For the current state, you can look at the |
Here's a quick overview of what I have. Note that this was done early on in my Bevy journey, and I'm not sure I would do things the same way today:
So for example, In the current framework, the actual despawning of the entities is handled by a separate timer, which is inside However, I'd like to get rid of this. Although, one difficulty here is that the logical place to put the transition component is on the entity that is being animated, but that entity may not exist when the dialog is closed. Currently the bistable transition actually lives on the parent, which doesn't go away. (It's a Reaction, which, like Observers, are "owned" by the parent entity). You can see this in use here: let state = builder.create_bistable_transition(self.open, TRANSITION_DURATION); In this code, both 'state' and 'open' are signals. The // Animate the opacity of the dialog backdrop
backdrop.effect(
move |rcx| {
let state = state.get(rcx);
// Compute the target opacity from the state
match state {
BistableTransitionState::Entering
| BistableTransitionState::Entered
| BistableTransitionState::ExitStart => colors::U2.with_alpha(0.7),
BistableTransitionState::EnterStart
| BistableTransitionState::Exiting
| BistableTransitionState::Exited => colors::U2.with_alpha(0.0),
}
},
move |color, ent| {
// Interpolate the opacity target
AnimatedTransition::<AnimatedBackgroundColor>::start(
ent,
color,
TRANSITION_DURATION,
);
},
) Note that you don't need to worry about the reactive stuff. I only mention it here because I wanted to show how it ties together. |
If I were re-implementing this today, I might actually consider dividing each animation into two components: one which contains the target value, duration, and the interpolation type, and a different component which contains the current state, which might be a required component. This would allow changing the animation parameters by overwriting / re-inserting the component, while preserving the current state of the animation. This is somewhat tricky, though, because you might want to animate multiple parameters, which would mean a different state component for each parameter type. |
I recently made a few more tweaks to my animation framework, and I also wanted to give a clearer explanation about the dialog lifecycle. The basic constraint is that we don't want the UI nodes for popups (dialogs and menus) to be hanging around, invisible, when they are closed. This not only consumes memory for the nodes themselves, but any resources that they may be hanging on to. So we want to despawn the entities when they are not needed. But when you click the "close" button on a dialog, you don't want to delete the entities immediately - instead, you want to wait until the closing animation completes before despawning the hierarchy. A dialog typically has two animated elements: the popup itself, and a "backdrop" element which covers the entire screen and grays out the background. The backdrop covers the entire window (window-absolute coordinates), and fades in and out using an animated background color, while the dialog may have a variety of animated transitions: opacity, scale, position, and so on. (Note that when I say "dialog" I don't just mean the traditional dialog box, but include things like the inventory screen in Skyrim). Currently the way I handle this is to place animation components on the dialog and the background, but use a separate timer (whose duration is the same as the length of the closing animation) to despawn the dialog elements. This timer is the "bistable transition" that I mentioned earlier. The reason I don't use the animation components for the despawning is because those components are themselves despawned - that is, before the dialog opens, and after it is finished closing, those entities don't exist. This makes change-detection complicated. When the dialog first opens, we want the animations to smoothly transition from the "closed" state to the "open" state - however, we can't use a simply "interpolate from previous state" because there is no previous state - the entities don't exist. This is a problem in CSS too, often we need to insert an extra state in at the beginning to represent the "fully closed but opening" state. Instead, the way I set this up in Bevy is to have the animation "start" method take an optional "initial state" parameter: animation.transition_to(1.0, Some(0.0)); The initial state parameter is ignored if there is already a transition in progress - it simply continues from wherever the transition is currently at. However, if there is no animation in progress, then the initial value param is used to start a new transition. So for the "open" animation we transition from 0 to 1, and for the closing animation we transition from 1 to 0. (This is the |
I've also been looking at layout projection https://gist.github.com/taowen/e102cf5731e527cb9ac02574783c4119 which lets you animate UI layouts. E.g. animate from flex-start to flex-end. I don't quite understand how it works yet, but it's something I want to mention. |
This is also what I've being doing, in a bit of a different way with reflex etc, I +1 the idea of splitting the transition data and target value into two separate components. Thinking about my current code base it would make more sense |
What problem does this solve or what need does it fill?
Most animations in games are based on a timeline with keyframes: that is, you have some parameter which is being animated over a curve. Your options are to start, stop, or reverse the animation, but the control points are usually fixed.
However, a different kind of animation - one often seen in CSS - is an interpolation to a target value, often modulated by some mathematical easing function. (There are also physics-based animations, like mechanical spring simulations). These animations don't have a fixed timeline of keyframes, and in fact the target value can be changed in mid-animation.
These kinds of animations are very useful in UI work for things like popup menus, dialogs, sliding drawers, basically anything that has an "enter" and "exit" transition. "Interpolate to target" animations have an advantage over keyframe-based approaches because the animation is frequently interrupted: the popup menu may be closed before it has finished opening. It also works well for elements that have more than two states, such as a sidebar which might have "hidden", "collapsed" and "expanded" states. The problem with the keyframe approach is that restarting the animation track in mid-animation will cause an unsightly "pop".
A related requirement is that we need some way to poll when the animation is complete. Take for example a dialog or inventory screen: we don't want to keep around the entity hierarchy for the popup when it's not visible. But when the popup closes, we can't despawn the entities right away; we need to wait until the closing animation is complete. Typically this would be done by some conditional expression such as "if open || animation.running" where "running" means that the animation has not reached some quiescent state.
For UI work, it would be ideal to be able to do this by inserting a component into the Node entity, such that this component would continually modify the style and transform properties of the node.
(In my case, I would want to be able to rely on Bevy component change detection for the polling: in other words, I would like to be able to monitor the progress of the animation by looking for changed components. This would let me easily integrate it with my reactive framework.)
The kinds of properties we most frequently want to animate are:
What solution would you like?
I have prototyped a framework for this in bevy_reactor, however this was done over a year ago, and there have been a lot of developments in Bevy since then, and my code doesn't take advantage of any of those improvements. What I would like to see is a solution which integrates all of the various animation ideas that people have had in the last year.
Part of the motivation for this ticket is to start a discussion on what the API might look like.
What alternative(s) have you considered?
I already have a working solution but it's less than ideal.
The text was updated successfully, but these errors were encountered: