UIアニメーションの線形補間を理解する

従来の(手描きの)アニメーションでは、シニアまたはキーアーティストがモーションを定義するキーフレームを描画します。

次に、アシスタント、通常はインターンまたはジュニアアーティストが、シーンに必要な中間を描画します。インビトウィーナーとも呼ばれるアシスタントの仕事は、キーポーズ間のトランジションをスムーズかつ自然に見せることです。

それらのないアニメーションは途切れ途切れに見えるため、中間が必要です。残念ながら、中間を描くことは多かれ少なかれ不機嫌な仕事です。しかし、それは21世紀であり、この種のタスクを処理できるコンピューターがあります。

小学校でコンピューターが馬鹿げていると先生が言ったことを覚えていますか?コンピュータは、アクションを実行するためのステップの正確なシーケンスを知らされる必要があります。今日は、コンピューターがスムーズなアニメーションを作成するために必要な中間を描画するのに役立つ、そのような一連のステップまたはアルゴリズムを見ていきます。

アルゴリズムを説明するために、HTML5キャンバスとJavaScriptを使用します。ただし、それらに精通していなくても、記事を読んで理解することはできます。

意図

私たちの目標は単純で、ポイントA(startX, startY)からBまでボールをアニメートすること(endX, endY)です。

このシーンが伝統的なアニメーションを行うスタジオに渡された場合、シニアアーティストは次の重要なポーズを描きます…

…そして、そのように中間を描くために、画板をジュニアアーティストに渡します。

私たちの状況では、アニメーションスタジオもジュニアアーティストもいません。私たちが持っているのは、目標、コンピューター、そしてコードを書く能力だけです。

アプローチ

HTMLコードは単純で、必要なのは1行だけです。

JavaScriptコードのこの部分(以下に表示)は単に取得します s/> from the Document Object Model (DOM), gets context, and sets the width and height property of the canvas to match the viewport.

const canvas = document.getElementById(‘canvas’), context = canvas.getContext(‘2d’), width = canvas.width = window.innerWidth, height = canvas.height = window.innerHeight;

The function below draws a green solid circle of radius radius at x and y coordinates.

function drawBall(x, y, radius) { context.beginPath(); context.fillStyle = ‘#66DA79’; context.arc(x, y, radius, 0, 2 * Math.PI, false); context.fill();}

All of the above code is boilerplate to set up our animation, here’s the juicy part.

// Point Alet startX = 50, startY = 50;
// Point Blet endX = 420, endY = 380;
let x = startX, y = startY;
update();function update() { context.clearRect(0, 0, width, height); drawBall(x, y, 30); requestAnimationFrame(update);}

First of all, notice the update function being called right above its declaration. Second of all, notice requestAnimationFrame(update) which calls updaterepeatedly.

Original text


Flipbook animation is a good analogy for the kind of program we’re writing. Just like repeatedly flipping through a flipbook creates the illusion of motion, repeatedly calling the update function creates the illusion of motion for our green ball.

One thing to note about the code above is that “update” is just a name. The function could have been named anything else. Some programmers like the names nextFrame, loop, draw, or flip because the function is repeatedly called. The important part is what the function does.

On each subsequent call of update, we expect the function to draw a slightly different image on the canvas than the previous one.

Our current implementation of update draws the ball at the same exact position on each call drawBall(x, y, 30). There is no animation, but let’s change that. Below is a pen that contains the code we have written so far, you can open it and follow along.

On each iteration of update let’s go ahead and increment the value of x and y and see the kind of animation it creates.

function update() { context.clearRect(0, 0, width, height); drawBall(x, y, 30); x++; y++; requestAnimationFrame(update);}

Each iteration moves the ball forward in both the x and y directions, and repeated calls of update results in the animation as shown.

Heres’ the deal though, our intent was to move the ball from a start position to an end position. But we’re doing absolutely nothing about stopping the ball at an end position. Let’s fix that.

One obvious solution is to only increment the coordinates when they are smaller than endX and endY values. This way, once the ball crosses endX, endY its coordinates will stop updating and the ball will stop.

function update() { context.clearRect(0, 0, width, height); drawBall(x, y, 30); if(x <= endX && y <= endY) { x++; y++; } requestAnimationFrame(update);}

There’s an error in this approach though. Do you see it?

The problem here is that you cannot make the ball reach any end coordinate you want just by incrementing x and y values by 1. For example, consider end coordinates (500, 500), of course if you start at (0, 0) and add 1 to x and y, they will eventually get to(500, 500). But what if I chose (432, 373) as end coordinates?

Using the above approach, you can only reach points lying in a straight line 45 degrees from the horizontal axis.

Now you can use trigonometry and fancy math to calculate precise amounts that x and y should be incremented by to reach any coordinate you want. But you don’t need to do that when you have linear interpolation.

Approach with linear interpolation

Here’s what linear interpolation function a.k.a lerp looks like.

function lerp(min, max, fraction) { return (max — min) * fraction + min;}

To understand what linear interpolation does, consider a slider with a min value on the left end and a max value on the right end.

The next thing we need to choose is fraction. lerp takes fraction and converts that to a value between min and max.

When I put 0.5 in the lerp formula — no surprises — it translates to 50. This is exactly the halfway point between 0 (min) and 100 (max).

Similarly, if we choose another value for fraction say 0.85

And if we let fraction = 0, lerp will output 0 (min) and on fraction = 1, lerp will produce 100 (max).

I chose 0 and 100 as min and max to keep this example simple, but lerp will work for any arbitrary choice of min and max.

For values of fraction between 0 and 1, lerp allows you to interpolate between min and max. Or in other words, traverse between min and max values, where choosing 0 for fraction puts you at min, choosing 1 puts you at max and for any other value between 0 and 1, puts you anywhere between min and max. You can also see min and max as key poses, like in traditional animation, and lerp outputs as inbetweens ;-).

Alright, but what if someone gives a value outside the bounds of 0 and 1 as fraction to lerp? You see the formula for lerp is extremely straightforward with most basic of mathematical operations. There’s no trick or bad values here, just imagine extending the slider in both directions. Whatever value for fraction is supplied, lerpwill produce a logical result. We shouldn’t pay much thought to bad values here though, what we should think about is how all of this maps to animating the ball.

If you’re following along go ahead and change the update function to match the following code. Also don’t forget to add in the lerp function we defined at the beginning of this section.

function update() { context.clearRect(0, 0, width, height); drawBall(x, y, 30); x = lerp(x, endX, 0.1); y = lerp(y, endY, 0.1); requestAnimationFrame(update);}

Here’s a pen of what our program looks like now. Try clicking around :)

Smooth right? Here’s how lerp helps to improve the animation.

In the code, notice the variables x and y — which are initially set to startX and startY— mark the current position of the ball in any frame. Also my choice of 0.1 as fraction is arbitrary, you can choose any fractional value you want. Keep in mind that your choice of fraction affects the speed of the animation.

In every frame x and endX are taken as min and max and interpolated with 0.1 as fraction to obtain a new value forx. Similarly y and endY are used as min and max to obtain a new value for y using 0.1 as fraction.

The ball is then drawn at the newly calculated(x, y) coordinate.

These steps are repeated until x becomes endX and y becomes endY in which case min = max. When min and max become equal lerp throws the exact same value(min/max) for any further frames thus stopping the animation.

And that is how you use linear interpolation to smoothly animate a ball.

This short article covers a lot. We started by defining terms like key poses and inbetweens. Then we tried a trivial approach for drawing inbetweens and noticed its limitations. Finally, with linear interpolation, we were able to achieve our intent.

I hope all the math made sense to you. Feel free to play with linear interpolation even more. This article was inspired by Rachel Smith’s post on CodePen. Rachel’s post has many more examples, be sure to check it out.

Looking for more? I publish regularly on my blog at nashvail.me. See you there, have a good one!