In Part 1 we covered the history of animating UI effects in Xojo. In Part 2 we dug in to linear interpolation. In Part 3 saw the addition of support for concurrent animations and color changes. In Part 4 we’ll cover easing, which can make the most drab User Interface interactions beautiful.
What is Easing?
To quote Paul Lewis:
Nothing in nature moves linearly from one point to another. In reality, things tend to accelerate or decelerate as they move. Our brains are wired to expect this kind of motion, so when animating, you should use this to your advantage. Natural motion makes your users feel more comfortable with your apps, which in turn leads to a better overall experience.https://developers.google.com/web/fundamentals/design-and-ux/animations/the-basics-of-easing
Based on that, we can define easing as a more natural-appearing change in value between a start and end point which is designed to make a user feel more comfortable with changes in the user interface.
Types of Easing Functions
Easing functions can be broken down in to three basic types:
In— An animation that starts slowly and increases in speed over time
Out— An animation that starts quickly and decreases in speed over time
InOut— A combination of InOut that begins and ends quickly with a slow transition in the middle.
What Does Easing Look Like?
This can be used to provide some really enthralling UI transitions that’ll make both you and your users much happier as it just feels more natural than making Element A of your UI disappear and Element B appear, or using a linear animation to swap the two elements.
What Does an Easing Function Look Like
Easing functions can be incredibly simple, as you saw in our lerp method in Part 2, or as complex as you can imagine. Our example of Elastic easing, while not the most complex, is pretty interesting mathematically (if you’re terrible at math like I am).
Private Function easeInElastic(startValue as Double, endValue as Double, alphaValue as Double) as Double Return ((endValue - startValue) * (0.04 * alphaValue / (alphaValue - 1) * Sin( 25 * (alphaValue - 1) ))) + startValue End Function
Basically what this method is doing, and my grasp of the math is admittedly pretty thin, is it’s taking the start and end value and creating a diminishing sine wave to simulate the change over time. I normally would say that this should be broken down in to multiple lines to make it easier to read, possibly by using no fewer than three variables, but we really want it to be quick.
If we did break it down, it would look more like this:
Private Function easeInElastic(startValue as Double, endValue as Double, alphaValue as Double) as Double dim changeValue as Double = endValue - startValue dim differenceValue as Double = (0.04 * alphaValue / (alphaValue - 1) * Sin( 25 * (alphaValue - 1) )) Return (changeValue * differenceValue) + startValue End Function
Which may be easier to read for some of us, but those variable declarations have a cost.
Many of the easing functions you’ll see in this part of the series are adapted for Xojo (with some heavy modification in most places) from Robert Penner’s Easing Functions (BSD License). There’s a note on the Animated Canvas project item if you wish to explore the sources.
What Types of Easing Functions Should I Use?
UI animations are, typically, something you want to happen fairly quickly. Given that, you likely won’t use
InOut easing functions very often as they can feel slower to your end user.
In functions can be off putting to users as the natural expectation of seeing the most movement at the end of an animation is antithetical to
In functions and may feel slow or confusing for some users.
Based on those theories, you’ll typically employ
Out functions in your UI animations, as they’re both quick and easier for our brains to understand in a UI setting. That said, there will be times when other types of easing functions are more suited to an individual task and you must use your best judgement and listen to your QA/users if they complain about it. Animation type and length are a delicate balance.
As I alluded to earlier, the timing of your chosen easing function is incredibly important. The following table should be a good starting point for appropriate animation lengths for different types of operations.
These are the values I typically start with, and alter them based on the responsiveness of the UI and the desired effect. Your mileage may vary.
While I won’t cover the addition of each of the example easing functions that I’ve provided in the example project, I will cover the functional changes to our existing codebase and the addition of the Elastic easing functions.
Our first step is to create a new property on our AnimatedCanvas class called AnimationEasing. In our demo window we’ll set this value based on selection in a PopupMenu to make it easy to switch between different easing functions and view their output. It’s going to public and computed so we can show it in the Inspector.
Private Property mAnimationEasing as Easings Public Property AnimationEasing as Easings Get Return mAnimationEasing End Get Set mAnimationEasing = value End Set End Property
Next we to create an enum for our various easing functions. It’s pretty straight-forward, and we’ll use it to tell our animation controller code what to do in the animation operations we define in our Dictionary objects.
Public Enum Easings Linear InQuad OutQuad InOutQuad InCubic OutCubic InOutCubic InElastic OutElastic InOutElastic End Enum
We are, again, modifying our Dictionary object definition. This time we’re passing along the desired easing effect. Our new definition looks like this:
new Dictionary("op" : <OperationToComplete", "l" : <LengthOfAnimation>, "m" : <EasingFunction>, "t" : <StartTime>, "s" : <StartValue>, "e" : <EndValue>)
You can see that all we’ve really done from Part 3 is add a Key/Value pair for the easing. I assigned this to the “m” key to represent “method of change”, but you could use anything you like here.
MouseEnter and MouseExit
We actually don’t need any changes here for our color animation, as this will continue to use the
lerpColor function we previously defined. Color change animations should typically be quick and applying anything but a linear easing to them can both look and feel bad if done improperly, so I won’t cover it here. When in doubt, for color animation, stick with linear and keep it around 100-250ms.
MouseUp, likewise isn’t changing much. We’re basically just adding the easing Key/Value to our existing Dictionary objects like so:
animQueue_Add(new Dictionary("op" : "width", "l" : manimTime, "m" : mAnimationEasing, "t" : Microseconds, "s" : Width, "e" : If(isExpandedWidth, 10, expandedWidth)))
We need a function to take our
Easings enumeration value and perform the specified easing function. In this example I’m using a
Select Case block, but I’ve also implemented a nice jump table using delegates in other projects.
Private Function doEasing(easing as Easings, startValue as Double, endValue as Double, alphaValue as Double) as Double select case easing case easings.Linear Return lerp( startValue, endValue, alphaValue ) case easings.InQuad Return easeInQuad( startValue, endValue, alphaValue ) case easings.OutQuad Return easeOutQuad( startValue, endValue, alphaValue ) case easings.InOutQuad Return easeInOutQuad( startValue, endValue, alphaValue ) case easings.InCubic Return easeInCubic( startValue, endValue, alphaValue ) case easings.OutCubic Return easeInOutCubic( startValue, endValue, alphaValue ) case easings.InOutCubic Return easeInOutCubic( startValue, endValue, alphaValue ) case easings.InElastic Return easeInElastic( startValue, endValue, alphaValue ) case easings.OutElastic Return easeOutElastic( startValue, endValue, alphaValue ) case easings.InOutElastic Return easeInOutElastic( startValue, endValue, alphaValue ) end select End Function
As you can see, it takes the easing parameter representing the desired function, and the values we need to animate, passes all of that along to the requested easing function, and returns the value. A call to this method will replace our previous call to
lerp in our Timer’s Action event handler.
Our Easing Functions
As I said earlier, I won’t cover all of the functions in the article, but I will include the Elastic functions here for your perusal. To see all of the easing functions included, be sure to download the example project at the end of the article.
Private Function easeInElastic(startValue as Double, endValue as Double, alphaValue as Double) as Double Return ((endValue - startValue) * (0.04 * alphaValue / (alphaValue - 1) * Sin( 25 * (alphaValue - 1) ))) + startValue End Function Private Function easeOutElastic(startValue as Double, endValue as Double, alphaValue as Double) as Double Return ((endValue - startValue) * ((0.04 - 0.04 / alphaValue) * Sin( 25 * alphaValue ) + 1)) + startValue End Function Private Function easeInOutElastic(startValue as Double, endValue as Double, alphaValue as Double) as Double dim opChange as Double = Abs(endValue - startValue) if alphaValue <= 0.5 then return easeInElastic(startValue, opChange / 2, alphaValue * 2) else return easeOutElastic(opChange / 2, endValue, Abs(1 - (alphaValue * 2))) end if End Function
We’ve already discussed purpose and expectation for
Out, but take a look at
easeInOutElastic. We accomplish this effect by splitting the run-time of the animation into two distinct parts and executing the appropriate easing function based on where we are in the animation.
In happens at <= 50% of our total animation time, and
Outhappens at > 50% of our animation time. This gives us a nice, seamless transition in our
InOut animation as the value at 50% should be close — if not identical — in both easing functions.
Our changes in animTimer_Action are also pretty minimal. Since we’re not applying one of the more advanced easing functions to our previously discussed color animation, I’ll only cover the changes we’re making to the Width and Height operations.
First, we need to add a new variable declaration to the top of our method. This is where we’ll store the easing type.
Dim animOpEasing As Easings
Then, after our
Else at line 87, where we get our values from the current
animOp Dictionary, we will assign the current operation’s easing (or “m” key) value to our new variable.
animOpEasing = animOp.Value("m")
Now that we know which easing function we want to apply to the current operation, and we’ve already defined our
doEasing function, all we need to do is swap out our line (line 100) that previously used the
lerp function to assign the appropriate value to
animStepDouble, to use our new
animStepDouble = doEasing(animOpEasing, animStartDouble, animEndDouble, timePercent)
I do hope that, throughout this series, you’ve learned a little something about how and when to implement animation in user interfaces, some decent practices for doing so in an encapsulated way, and maybe picked up a few other pointers.
I had a lot of fun writing this and interacting with readers who reached out with questions and suggestions, and I hope to do more guest posts on the Xojo blog in the future. I’d like to thank Team Xojo not only for their great product and support, but also for allowing me to stand at their podium for a short time and share with you all.
If you would like to see more easing functions, or how I’ve implemented a full animation controller and the delegate jump table, GraffitiAnimator will soon be coming to GraffitiSuite Desktop Edition, and can be acquired directly, via the Xojo Extras store, or for a limited time as part of the Omega Bundle.
Finally, you can download the example project for Part 4 here.
Thank you all for following along with me!
Anthony G. Cyphers is the Lead Developer and Sole Proprietor of GraffitiSuite Solutions, and has been providing custom Xojo components and contract development since 2003. He runs the Xojo User’s Discord server, and is an administrator in the Xojo User’s Facebook user group.