blog.oat.zone
λ
cd /the-easy-and-memorable-solution-to-easing/

The easy and memorable solution to easing

· by jill

If you've ever written any sort of UX/UI component, you'll likely have encountered the problem of easing - making things move in a nice-looking way. So, here's my idea. I've used this method in a few games and UIs and it works really well.


If you've ever written any sort of UX/UI component, you'll likely have encountered the problem of easing - making things move in a nice-looking way. (Unless you're an HTML developer, in which case CSS handles all of that for you, and in which case I hate your guts.)

The problem with easing is it's, really complicated to get right. It's one of those things that you can't exactly calculate; it's something you mess around with until it looks right. It's also really hard to get into.

We've invented like, what, 50 easing functions now, all for different purposes? It feels really overcomplicated, especially when you get into the area of actually applying the ease functions, since then you have to deal with overlapping eases, you have to deal with cancelling them, you have to deal with scheduling them, and it's all a huge mess.

A bunch of ease functions along with their graphs.
"Yes, I'd like one easeInOutBounce" - statements dreamt up by the UTTERLY DERANGED

Most of the time you can find some library for whatever you're using that can handle it all for you, which like, cool, but it still sucks to implement in the first place, and often is hard to design a good interface for aswell. What I'm trying to say is, the current standard system of easing is great, but god is it overcomplicated.

So, here's my idea. I've used this method in a few games and UIs and it works really well. While with ease functions, you have to schedule a function and run it over time, my proposed solution technically never ends the ease.


So, let's set a hypothetical situation. Let's say, you have an object. Once you press the arrow keys, the object moves around. This is great, but it snaps instantly in place, which is bad, looks awful and nowadays isn't acceptable.

You'd probably have a variable for the x and y coordinates, or, if your language allows it, a 2D vector containing its position:

-- init
local x = 0
local y = 0

-- render
renderSquare(x, y)

-- input
x = newx
y = newy

Okay, so your next step, if you were to use a standard easing function, is to schedule an ease whenever you modify the x or y variables:

-- input
-- ease x from x to newx in 2 seconds. hypothetical function
ease(x, x, newx, 2)
ease(y, y, newy, 2)

This isn't very great, as now you can't use x or y as internal variables for the position, so you may do something like this:

-- render
renderSquare(displayx, displayy)

-- input
ease(displayx, x, newx, 2)
ease(displayy, y, newy, 2)
x = newx
y = newy

But now we're overcomplicating it, and we still need an external easing library. However, this is very close to my proposed solution!

So, instead of using ease functions, we could linearly interpolate (lerp) between the current value and the actual position:

function lerp(x, y, a)
  return x * (1 - a) + y * a
end

-- init
local x = 0
local y = 0
local displayx = x
local displayy = y

-- update
displayx = lerp(displayx, x, 0.1)
displayy = lerp(displayy, y, 0.1)

-- input
x = newx
y = newy

Now you may notice right away that the a value in lerp is a hardcoded 0.1. This defines how fast the movement finishes, and is.. kind of a magic number. It's fine to have a few of those, sometimes. (Citation needed.)

You'll also notice (hopefully) that this won't work well unless the update function runs on a consistent timer - 30 times per second, 60 times per second, etc. So to make this work across any framerate and any tickrate, you can do this:

-- update
-- dt can be defined as deltatime - the time
-- that has passed since the last time
-- the update function has been run
displayx = lerp(displayx, x, 12 * dt)
displayy = lerp(displayy, y, 12 * dt)

The a value in lerp is still a hardcoded number, but this time it's multiplied by dt! I'd recommend never raising it below 12, as at that point it gets way too slow for any purposes.

And now it'll just, ease magically, with no extra libraries, ease functions, just plain ol' lerp and deltatime trickery!

Now, I will admit. This is still a bit stiff - the object moves right away rather than, say, having an in ease, letting it accelerate before moving. But this is a good enough solution for quick solutions, fits almost anywhere, and if you want it to move faster or slower, all you need to change is one simple value.

And the best part is how extremely memorable it is. In comparasion to having to import libraries or scheduling eases, this is just something you can shove in an update loop, at any time, for any variable.

For example, it's useful in a situation like this:

If you were to define an in ease, then it'd kinda suck, as it won't feel as snappy, but it still looks really nice, as there's a slight bit of easing between you clicking and the slider moving to where you've clicked.

There are definitely pros to using ease functions however - you can't recreate movements like this without them:

But, overall, this is a very good and quick alternative.

I hope you consider it next time you need to make something move smooth, and I also hope this isn't something that's very commonly used in game development that I'm just reinventing ^^; ...

Edit - 2021-10-29

So I just remembered that I forgot to mention one thing in this post - managing velocity. See, when you have an ease of sorts, you can move your object with it, sure, but for extra emphasis you can apply all sorts of effects on top when it's moving - stretch it, skew it, rotate it, all of that makes the movement look better.

I will admit, the solution I came up with for velocity is kinda jank, but it gets the job done (most of the time...)

-- update
displayx = lerp(displayx, x, 12 * dt)
displayy = lerp(displayy, y, 12 * dt)
velocityx = lerp(displayx, x, 0.1)
velocityy = lerp(displayy, y, 0.1)

This will give you a velocityx and velocityy value to play around with. You may want to lower the hardcoded 0.1 to make the movement smoother, and you'll also likely have to multiply the resulting velocity by a lot to make it usable. But overall it gets the job done - it's not the most accurate, but who would even pay attention to if the velocity is accurate or not mid-motion? Some kinda motion design nerd??


published