Animation

Bezier curve

<https://javascript.info/bezier-curve> ノート。

Bezier 曲線はコンピューターグラフィックスで図形を描いたり、CSS アニメーションに使ったり、いろいろな用途がある。 Bezier 曲線はひじょうに単純なもので、一度学習しておけば、ベクターグラフィックスや高度なアニメーションの世界でも違和感なく使えるようになる。

Control points

Bezier 曲線は制御点によって定義される。二点、三点、四点、またはそれ以上の場合がある。

学習者ノート

本書ではここに二、三、四点それぞれの場合の曲線の例が図示されている。

これらの曲線をよく見てみると、すぐに気づくことがある。

  1. 制御点は常に曲線上にあるわけではない。

  2. 曲線の次数は制御点から 1 を引いた数に等しい。二点の場合は直線、三点の場合は放物線、四点の場合は三次曲線となる。

  3. 曲線は常に制御点の凸包の中にある。

最後の convex hull property を利用して、コンピュータグラフィックスでは、交点検定を最適化することが可能だ。凸包が交差しないのであれば、曲線も交差しない。したがって、最初に凸包の交差をチェックすれば、高速に「交差なし」という結果を得られる。凸包は長方形や三角形など(本書の図参照)、曲線よりもずっと単純な図形なので、凸包の交差をチェックするのはずっと簡単だ。

学習者ノート

曲線が平面上にあることを仮定しているが、それは問題にしなくていい。

Bezier 曲線の描画における主な価値とは、制御点を移動することによって、曲線が直感的に明らかな方法で変化することにある。本書の例を使って、マウスを使って制御点を対話的に動かせるので試す。

曲線は接線 12 と接戦 34 に沿って伸びている。練習を重ねると、必要な曲線を得るために、どのように点を配置すればよいかが分かってくる。また、いくつかの曲線をつなげば、ほとんどどんなものでも得ることができる。

学習者ノート

本書で言う接続とは、単に端点を共有する程度に留まる。

De Casteljau’s algorithm

学習者ノート

このデモは De Casteljau の計算手順を視覚的に示すものだ。制御点が三つの(すなわち放物線を形成する)例を試す。制御点はマウスで動かすことができ、再生ボタンをクリックすると、曲線パラメーター t に対する点を 0 から 1 まで評価する。

De Casteljau の三点 Bezier 曲線構築の計算手順:

  1. 制御点を描く。当デモでは 1, 2, 3 とラベルを付けてある。

  2. 制御点 1, 2, 3 の間の線分を作図する(茶色の線)。

  3. パラメータ t を 0 から 1 へと変化させる。この例では 0.05 刻みにしてある。

    これらの t の値のそれぞれについて。

    • 茶色の線分それぞれにおいて、その始点から t に比例した距離にある点を取る。線分が二つあるので、点も二つある。

      例えば、t=0 の場合、点は線分の始点、t=0.25 の場合、始点から線分の長さの 25% にあり、t=0.5 の場合、中間にあり、t=1 の場合、線分の終点だ。

    • 今得られた二点を結ぶ(青い線分)。

  4. つまり、t=0.25 の場合は、線分の左 1/4 の端に点があり、t=0.5 の場合は、線分の中央に点があることになる(赤い点)。

  5. t は 0 から 1 まであり、t の値ごとに曲線に一点ずつ追加される。そのような点の集合が Bezier 曲線を形成する(赤い放物線)。

以上の手順は三制御点に対するものだが、四点以上の場合も本質的には同じだ。この計算手順は再帰的であり、任意の数の制御点に対して一般化できる: N 個の制御点が与えられたとする。

  1. それらを結んで N - 1 個の線分を最初は得る。

  2. 次に、0 から 1 までの各 t について、t に比例した距離の各線分上の点を取り、それらを端点とする線分を作図する。すると N - 2 個の線分が得られる。

  3. 点が一つだけになるまで 2. を繰り返す。

これらの点が曲線を作る。

上記の説明で不明な点があれば、本書のライブサンプルを触って、どのように曲線が構築されるかを確認する。アルゴリズムは再帰的なので、任意の次数の Bezier 曲線を構築することができる。しかし、実際には多くの点はほとんど役に立たない。通常、2, 3 個の制御点を取り、複雑な線はそのような小次数の曲線を端点で接続して表現する。その方が簡単だからだ。

学習者ノート

次数の大きい Bezier 曲線は計算量が明らかに多い。


Bezier 曲線を指定するには制御点を用いる。それらは最初と最後の点を除いて曲線上にない。また、いくつかの点を通る曲線を描き、すべての点が一本の滑らかな曲線になるようにするという問題もある。この作業は補間と呼ばれるが、ここでは扱わない。このような曲線には、Lagrange 多項式などの数学的な公式がある。コンピュータグラフィックスでは、多くの点を結ぶ滑らかな曲線を作るために、スプライン補間がよく使われる。

Maths

制御点 \(P_i\) が座標の組で与えられると、最初の制御点の座標は \({P_1 =(x_1, y_1)}\), 二番目は \({P_2 = (x_2, y_2)}\), というように、曲線の座標は線分 \({[0, 1]}\) からパラメータ \(t\) に依存する方程式で記述される。

二点の公式:

\[P(t) = (1-t)P_1 + tP_2.\]

三点の公式:

\[P(t) = (1-t)^2P_1 + 2(1-t)tP_2 + t^2P_3.\]

四点の公式:

\[P(t) = (1-t)^3P_1 + 3(1-t)^2tP_2 +3(1-t)t^2P_3 + t^3P_4.\]

変数 \(t\) が 0 から 1 まで動くと、各 \(t\) の値 \({(x, y)}\) の集合がそのような制御点の曲線を形成する。

CSS-animations

<https://javascript.info/css-animations> ノート。

CSS では、JavaScript を全く使わずに簡単なアニメーションを行える。さらに、 JavaScript を使って CSS アニメーションを制御することで、少ないコードでより良いアニメーションを作れる。

CSS transitions

CSS 遷移の考え方は単純だ。あるプロパティーを記述し、その変化をどのようにアニメーションさせるかを記述する。プロパティーが変化すると、ブラウザーがそのアニメーションを描画する。つまり、プロパティーを変更するだけで、ブラウザーが流動的な遷移を行うのだ。例えば、次の CSS は background-color の変化を 3 秒間アニメーションさせる。これで、要素に .animated クラスがあれば 3 秒間の背景色の変化がアニメーションで表示される:

.animated {
    transition-property: background-color;
    transition-duration: 3s;
}

学習者ノート

値が CSS のプロパティー名となるプロパティーは初めて見ると思う。本書のデモでは、ボタンをクリックすると背景がアニメーションで赤へと変化していく。ボタンの onclickthis.style.backgroundColor = 'red'; としている。これが最終状態だ。

CSS 遷移を記述するプロパティーは四つある:

  • transition-property

  • transition-duration

  • transition-timing-function

  • transition-delay

差し当たり、共通 transition プロパティーによって、property duration timing-function delay の順番でまとめて宣言できることと、複数のプロパティーを一度にアニメーションさせることができることを押さえておく。

<style>
#growing {
    transition: font-size 3s, color 2s;
}
</style>

<script>
growing.onclick = function() {
    this.style.fontSize = '36px';
    this.style.color = 'red';
};
</script>

学習者ノート

組 (property duration timing-function delay) をカンマ区切りで列挙できるのだろう。

transition-property

プロパティー transition-property には、アニメートさせたいプロパティーのリストを記述する。また、all と書くと、すべてのプロパティーをアニメートすることになる。一般的に使われているプロパティーのほとんどはアニメート可能だが、できないプロパティーもある。

transition-duration

プロパティー transition-duration でアニメーション時間を指定できる。 CSS 時間書式で指定する。秒なら s, ミリ秒なら ms だ。

transition-delay

プロパティー transition-delay には、アニメーションを開始するまでの遅延時間を指定できる。例えば、transition-delay: 1stransition-duration: 2s の場合、アニメーションは当該プロパティーの変化から 1 秒後に始まり、全体の継続時間は 2 秒となる。

負の値も許される。その場合、アニメーションはすぐに表示されるが、アニメーションの開始点は与えられた値(時間)後になる。例えば、transition-delay: -1stransition-duration: 2s の場合、アニメーションは中間点から始まり、全体の継続時間は 1 秒となる。

学習者ノート

デモのスクリプトに注意。onclick で CSS クラスを与える方法を採っている。

また、transition-delay を負の値にすることで、遷移の途中、例えば現在の秒に相当する正確な数字から開始させることも可能だ。

transition-timing-function

プロパティー transition-timing-function には、アニメーションの進行を時間軸に沿ってどのように配分するかを記述する。このプロパティーは Bezier 曲線と階段関数の二種類の値を取ることができる。

Bezier curve

タイミング関数を、次の条件を満たす四制御点からなる Bezier 曲線として設定できる:

  1. 最初の制御点は (0, 0)

  2. 最後の制御点は (1, 1)

  3. 中間点では x の値は区間 (0, 1) 内でなければならず、y は何でもよい。

CSS での Bezier 曲線の構文はこうなる:

cubic-bezier(x2, y2, x3, y3)

最初と最後の制御点は固定されているので、間の二点だけを指定する。タイミング関数はアニメーション処理の速さを記述する:

  1. x 成分は時刻を指定する: 0: 開始、1: 終了。

  2. y 成分は処理の完了を指定する。0: 開始値、1: 最終値。

最も単純な変種は、アニメーションが等速で進む場合だ。これは曲線 cubic-bezier(0, 0, 1, 1) で指定することができる。

列車のデモは解説がほとんど要らない。要素の位置を指定するのに left を援用するくらいか。

次の曲線は初速がえらいことになっている。

組み込み曲線がいくつか用意されている。

  • linear: cubic-bezier(0, 0, 1, 1) と形は同じ曲線。すなわち直線。

  • ease: cubic-bezier(0.25, 0.1, 0.25, 1.0)

  • ease-in: cubic-bezier(0.42, 0, 1.0, 1.0)

  • ease-out: cubic-bezier(0, 0, 0.58, 1.0)

  • ease-in-out: cubic-bezier(0.42, 0, 0.58, 1.0)|

transition-timing-function の既定値は ease だ。

Bezier 曲線をアニメーションがその範囲を超えるようにとることができる。制御点はどんな y 座標でもかまわない。そうすると、曲線は非常に低いか高いか知らないが延長して、通常の範囲を超えるアニメーションになる。

ここで本書では cubic-bezier(.5, -1, .5, 2) のデモが来るが、この曲線のプロットが示されているのでわかりやすい。

二点目の y 座標を 0 以下にし、三点目については 1 以上にしたため、曲線は「正規の」象限の外に出る。曲線の y 座標はアニメーション処理の完成度を測るものだ。値 \({y = 0}\) はプロパティーの開始値に対応し、\({y = 1}\) は終了値に対応する。つまり、\({y \lt; 0}\) の値はプロパティーを開始時の左 (left) から動かし、\({y \gt; 1}\) の値は終了時の左から動かす。

特定の課題のために Bezier 曲線を作図するのに、ツールがいろいろとある:

  • 例えば、<https://cubic-bezier.com>

  • ブラウザーの開発ツールも CSS における Bezier 曲線を特別に対応している。

    1. 開発者ツールを開く。

    2. Elements タブを選択し、右側の Styles サブパネルに注目する。

    3. CSS プロパティーで cubic-bezier とあるものは、その前にアイコンがある。

    4. このアイコンをクリックして曲線を編集する。

Steps

タイミング関数の steps(number of steps[, start/end]) は、遷移を複数の段階に分割できる。

<div id="digit"> <!-- border: 1px solid red; width: 1.2em; -->
    <div id="stripe">0123456789</div> <!-- display: inline-block; font: 32px monospace; -->
</div>

タグ #digit は幅が固定で枠があるので、赤い窓のように見える。

これからタイマーを作る。数字が一文字ずつバラバラに表示されるように。そのために、#digit の外側に #stripeoverflow: hidden で隠し、#stripe を段階的に左にずらしていくことにする。各桁ごとに一歩ずつ移動し、九歩になる。

学習者ノート

この表現がわかりにくい。

#stripe.animate{
    transform: translate(-90%);
    transition: transform 9s steps(9, start);
}

第一引数は段階数だ。変換が九つの部分に分割される。時間間隔も自動的に九分割されるので、transition: 9s であることから一桁あたり一秒ということになる。

第二引数は start または end と書く。start はアニメーションの始まりで、最初の段階をすぐに作る必要があることを意味する。

桁をクリックするとすぐに 1 に変わり、次の秒の始めに変化する。このように処理が進行する:

  • 0s: -10%; 1s の頭に最初の変化がすぐにある。

  • 1s: -20%

  • ……

  • 8s: -90%

  • 最後の一秒は最終値を示す。

ここでは、step()start を与えたので、最初の変化は即時だ。代替値の end は、変更を最初ではなく、各秒の終わりに適用することを意味する。つまり、steps(9, end) の処理は次のようになる:

  • 0s: 0; 最初の一秒間は何も変化しない。

  • 1s: -10%; 最初の一秒の終わりに変化が起こる。

  • 2s: -20%

  • ……

  • 9s: -90%

steps(9, end) の動作デモもある。最初の桁が変わる前の休止時間に注目する。

また、steps() には定義済みの短縮形がある。

  • step-start: steps(1, start) と同じ。アニメーションはすぐに開始され、ワンステップを取る。

  • step-end: steps(1, end) と同じ。transition-duration の終了時に、ワンステップでアニメーションを作成する。

これらの値は、実際のアニメーションではなく、一段階の変化を表しているため、使用されることはほとんどない。

Event: "transitionend"

CSS アニメーションが終了すると、イベント transitionend が起こる。このイベントはアニメーションが終了した後に何か行動するために広く用いられる。また、アニメーションを結合することもできる。

船の例は、クリックするとそこへ向かって航行し始め、そのたびに右へ向かって遠くへ行く。アニメーションは、遷移が終了するたびに再実行される関数 go() によって開始され、方向を反転させる。

学習者ノート

コードの構造は transitionend が起こるたびに go() が呼び出される。呼び出された回数が奇数か偶数かで船要素の CSS クラスとボックスの位置が更新される。

transitionend イベントオブジェクトには固有のプロパティーがある。

event.propertyName

アニメーションが終了したプロパティー。プロパティーを複数同時に処理させる場合に有用だ。

event.elapsedTime

アニメーションにかかった時間(transition-delay は含まず)。

Keyframes

CSSの @keyframes 規則を使って、単純なアニメーションを複数結合できる。これはアニメーションの「名前」と規則(何を、いつ、どこでアニメーションさせるか)を指定する。それから、プロパティー animation を用いて、アニメーションを要素に取り付け、そのパラメーターを追加的に指定できる。

学習者ノート

難しいかもしれない。本書の説明だけでは厳しい。

キーフレームについては多くの記事があり、詳細な仕様も記載されている。常に動いているサイトでない限り、@keyframes が必要になることはあまりないだろう。

Performance

CSS プロパティーのほとんどは数値であるため、アニメーションさせることができる。例えば、width, color, font-size はすべて数値だ。これらをアニメーションにすると、ブラウザーはこれらの数値を 1 フレームずつ徐々に変化させ、滑らかな効果を生み出す。ただし、CSS プロパティーによって変更にかかる労力が異なるため、すべてのアニメーションが思いどおりに表示されるわけではない。

スタイルが変更されると、ブラウザーは三つの段階を経て新しい外観を描画する。

  1. Layout: 各要素の幾何と位置を再計算する

  2. Paint: 背景や色など、それぞれの場所でどのように見えるかを再計算する。

  3. Composite: 最終結果を画面上のピクセルに描画し、CSS 変換がある場合はそれを適用する。

CSS アニメーションではこの処理がフレームごとに繰り返される。しかし、色など、幾何や位置に影響を与えない CSS プロパティーは、レイアウト段階を飛ばすことができる。色が変更された場合、ブラウザーは新しい幾何を計算せず、Paint Composite に進む。また、直接 Composite に移動するプロパティーはほとんどない。 CSS プロパティーとそれがどの段階で引き起こされるかについては <https://csstriggers.com> に詳しい。

特に、多くの要素や複雑なレイアウトを持つページでは、計算に時間がかかることがある。また、この遅延はデバイスのほとんどで実際に目にすることができ、カクカクした流動性の低いアニメーションとなる。

Layout 段階を飛ばしたプロパティーのアニメーションはより高速になる。Paint も飛ばされるとより効果的だ。

プロパティー transform を採用すると素晴らしい:

  • CSS 変換は対象要素のボックス全体に作用する(回転、反転、伸縮、移動)。

  • CSS 変換は隣接する要素に作用することはない。

以上の理由により、ブラウザーは Composite 段階で、既存の Layout と Paint の計算の「上に」``transform`` を適用する。言い換えると、ブラウザーは Layout(サイズ、位置)を計算し、Paint 段階で色や背景などで塗り、それから transform を必要とする要素ボックスに適用する。

プロパティー transform の変更(アニメーション)が、Layout と Paint の段階を引き起こすことはない。しかも、ブラウザーは CSS 変換のためにグラフィックスアクセラレーターを活用するため、ひじょうに効率的な処理を行う。

プロパティー transform はひじょうに強力だ。要素に transform を用いると、要素の回転や反転、拡大や縮小、移動など、さまざまなことが可能になる。つまり、プロパティー left/margin-left の代わりに transform: translateX(...) を用いたり、要素のサイズを大きくするために transform: scale を用いたりできる。

プロパティー opacity は Layout を引き起こすこともない。これは、表示・非表示やフェードイン・フェードアウトの効果に利用できる。

通常、transformopacity をペアで使うと、ほとんどの需要が解決され、流動的で見栄えのするアニメーションが得られる。

たとえば、ここでは要素 #boat をクリックすると、transform: translateX(300) および opacity: 0 のクラスが追加され、300px 右に移動して消える。

学習者ノート

IMG 要素の元々の CSS 定義を一目見ただけで transition の仕様が洗練されていることがわかる。

キーフレームの例は難解。

Tasks

Animate a plane (CSS)

画像をクリックすると三秒かけて寸法を十倍にするアニメーションを作れ。終了したらメッセージボックスを出せ。アニメーション中にクリックされるときの対応も考えろ。

私の答案はこう:

#flyjet {
    width: 40px;
    height: 24px;
    transition-property: width, height;
    transition-duration: 3s;
}
let done = false;
flyjet.onclick = function(event){
    if(done){
        return;
    }
    this.style.width = "400px";
    this.style.height = "240px";
    setTimeout(() => alert('Done!'), 3000);
    done = true;
};

フラグを使うのがみっともないのならば、イベントハンドラーを着脱する方法も考えられる。

本書では transitionend イベントを処理しろとある。なるほど。ただし widthheight の遷移終了それぞれに対してイベントが起こるので注意が要る。

Animate the flying plane (CSS)

前の課題の解答を修正して、画像を animate して元のサイズより大きくし、そして元のサイズに戻るようにしろ。

これは transition-timing-function に適当な Bezier 曲線を与えるだけでいい。

Animated circle

膨張する円をアニメーションで表示する関数 showCircle(cx, cy, radius) を書け。

  • cx, cy は円の中心のウィンドウ相対座標、

  • radius は円の半径

とする。

  1. ボタンの HTML コード片を書く。ブラウザーの検証コマンドを利用して構わない。

  2. テンプレのスタイルシートの width, height をゼロにしておく。

  3. スクリプト。

function showCircle(cx, cy, radius){
    const circle = document.querySelector('div');
    circle.style.left = cx + 'px';
    circle.style.top = cy + 'px';
    circle.style.width = radius * 2 + 'px';
    circle.style.height = radius * 2 + 'px';
}

Animated circle with callback

単なる円ではなく、その中にメッセージを表示する必要があるとしよう。メッセージはアニメーションが完了した後(円が最大になった時点)に表示されなければならない。そうでなければ醜く見える。

先の関数 showCircle(cx, cy, radius) は円を描くが、準備がいつできたかを追跡する方法を与えていない。そこで、アニメーションが完了したときに呼び出されるコールバック引数を関数の引数リストに追加しろ。コールバック関数は引数として円の <div> を受け取る必要がある。

このようにしたい:

showCircle(150, 150, 100, div => {
    div.classList.add('message-ball');
    div.append("Hello, world!");
});

テキストを垂直方向に中央に置く方法がわからない。解答を見ると message-ballline-height を直接定義している。それでいいのか。

しかし、本問の急所はここだ:

circle.addEventListener('transitionend', function handler() {
    circle.removeEventListener('transitionend', handler);
    callback(C);
});

JavaScript animations

<https://javascript.info/js-animation> ノート。

JavaScript のアニメーションは CSS では扱えないものを扱える。例えば、Bezier 曲線とは異なるタイミング関数で複雑な経路を移動したり、キャンバス上でアニメーションを行ったりする。

Using setInterval

アニメーションは、一連のフレームとして実装できる。通常は、HTML/CSS のプロパティーに変更を小さく加えていく。たとえば、style.left0px から 100px に変更すると、要素が動く。そして、setInterval() でそれを増加させ、1 秒間に 50 回のように小さな遅延で 2px ずつ変化させると滑らかに見える。これは映画と同じ原理で、1秒間に 24 フレームあれば十分滑らかに見える。

let timer = setInterval(function() {
    if (style.left >= 100px){
        clearInterval(timer);
    }
    else{
        style.left += 2px;
    }
}, 20); // change by 2px every 20ms, about 50 frames per second

Using requestAnimationFrame

複数のアニメーションを同時に実行する場合を考える。これらを別々に実行すると、それぞれに setInterval(..., 20) が設定されているにもかかわらず、ブラウザーは 20ms ごとよりもずっと頻繁に再描画しなければならない。これは、アニメーションの開始時間が異なるため、20ms ごとがアニメーションの種類によって異なるからだ。間隔が揃っていないのだから、20ms の中に独立した複数の実行があることになる。

setInterval(animate1, 20); // independent animations
setInterval(animate2, 20); // in different places of the script
setInterval(animate3, 20);

// This is lighter than three independent calls:
setInterval(function() {
    animate1();
    animate2();
    animate3();
}, 20);

独立した複数の再描画をグループ化することで、ブラウザーが再描画するのが容易になり、その結果、CPU 負荷が軽減し、見た目も滑らかになるはずだ。もうひとつ注意すべきことがある。CPU に負荷がかかっていたり、再描画の頻度を少なくする理由があったり(ブラウザーのタブが隠されているなど)するので、本当は 20ms ごとに実行するべきではない。

JavaScript でそれを知るにはどうしたらいいのか。Animation timing という仕様があり、requestAnimationFrame() という関数が用意されている。これは、これらすべてとそれ以上の問題に対応している。

let requestId = requestAnimationFrame(callback);

この呼び出しにより、ブラウザーがアニメーションを行いたいときに最も近いタイミングで関数 callback が実行されるようスケジュールされる。コールバックで要素の変更を行うと、他の requestAnimationFrame() コールバックや CSS アニメーションと一緒にまとめられる。そのため、幾何の再計算と再描画は、複数回ではなく、一度だけ行われる。

学習者ノート

ある文書によると requestAnimationFrame() のコールバック回数はディスプレイのリフレッシュレートに合致するようだ。したがって、環境によってコールバックが呼び出される頻度が異なると想定するべきだ。そこで、コールバックの第一引数が経過時間を表す数値であることを利用する。例えば、物体の運動を描画するのであれば、コールバック呼び出しに対応する時刻における変位なり速度なりを計算して、物体の位置を決定するようにする。等速直線運動と等角速度円運動を実装してコールバック実装のコツをつかみたい。

戻り値 requestId は、呼び出しを取り消すために用いる。

cancelAnimationFrame(requestId);

このコールバック関数は、ページロードの開始時点からのミリ秒単位の経過時間を引数に取る。同じものを performance.now() を呼び出すことによっても得られる。 CPU に負荷がかかっていたり、ノートパソコンのバッテリーがほとんど放電していたり、その他の理由がない限り、通常、コールバックはすぐに実行される。

学習者ノート

関連するアニメーションの次回 requestAnimationFrame() 呼び出しの戻り値で requestId の値を更新するのを忘れないようにする。

以下のコードは、requestAnimationFrame() の最初の十回の実行の間の時間を示す。通常、10ms から 20ms になる。

let prev = performance.now();
let times = 0;

requestAnimationFrame(function measure(time) {
    document.body.insertAdjacentHTML("beforeEnd", Math.floor(time - prev) + " ");
    prev = time;
    if (times++ < 10) requestAnimationFrame(measure);
});

学習者ノート

実際に実行するとそうでもない。

Structured animation

requestAnimationFrame() の上に、より汎用的なアニメーション機能を作る。

function animate({timing, draw, duration}) {
    let start = performance.now();

    requestAnimationFrame(function animate(time) {
        // timeFraction goes from 0 to 1
        let timeFraction = (time - start) / duration;
        if (timeFraction > 1) timeFraction = 1;

        // calculate the current animation state
        let progress = timing(timeFraction)

        draw(progress); // draw it

        if (timeFraction < 1) {
          requestAnimationFrame(animate);
        }
    });
}

関数 animate() は、アニメーションを本質的に記述する引数を三つ取る。

duration

アニメーションの総時間。

timing(timeFraction)

CSS プロパティー transition-timing-function のようなタイミング関数。経過した時間の割合(開始時 0、終了時 1)を取り、アニメーションの完成度(曲線の y 座標のようなもの)を返す関数だ。例えば、一次関数を与えると、アニメーションは一様に同じ速度で進行する。

function linear(timeFraction) {
    return timeFraction;
}

CSS で言えば transition-timing-function: linear に相当する。

draw(progress)

アニメーションの進行状態を取り、それを描画する関数。 progress=0 はアニメーションの開始状態、 progress=1 は終了状態を表す。実際にアニメーションを描くのはこの関数である。

function draw(progress) {
    train.style.left = progress + 'px';
}

CSS アニメーションとは異なり、ここでは任意のタイミング機能、任意の描画機能を作ることができる。タイミング機能は Bezier 曲線にとらわれない。また、描画はプロパティーを超えて、花火のアニメーションのような新しい要素を作ることができる。

Timing functions

学習者ノート

GLSL で修行したものと同じ。

Power of n

アニメーションの速度を上げたい場合は、べき乗の累進性を利用すればよい。JavaScript には Math.pow() がある。

学習者ノート

こうは言っているが、立ち上がりの速度はむしろ遅くなる。何しろ \({t \le 1}\) なのだから。

The arc

学習者ノート

これはやり過ぎ。1 - Math.sqrt(1 - t * t) くらいで十分。

Back: bow shooting

この関数は「弓を射る」。まず「弓の弦を引く」。そして「射る」。これまでの関数とは異なり、追加の引数である「弾性係数」にも依存する。弓の弦を引く距離は、これによって定義される。

function back(x, t) {
    return Math.pow(t, 2) * ((x + 1) * t - x);
}

Bounce

ボールを落とすと下に落ち、数回跳ね返って止まる。関数 bounce() はこれと同じことをするが、順序は逆だ。跳ね返りが直ちに始まる。そのために、特殊な係数がいくつか使われている。

function bounce(t) {
    for (let a = 0, b = 1; ; a += b, b /= 2) {
        if (t >= (7 - 4 * a) / 11) {
            return -Math.pow((11 - 6 * a - 11 * t) / 4, 2) + Math.pow(b, 2);
        }
    }
}

学習者ノート

ひじょうにクセがある。

Elastic animation

プロットを見ると、順序が逆の減衰関数だ。

function elastic(x, t) {
    return Math.pow(2, 10 * (t - 1)) * Math.cos(20 * Math.PI * x / 3 * t);
}

Reversal: ease*

タイミング関数のコレクションを用意した。その直接の用途は easeIn と呼ばれる。時には、アニメーションを逆の順序で表示する必要がある。それは easeOut 変換で行う。

easeOut

easeOut モードでは、タイミング関数はラッパー timingEaseOut に包められる。つまり、通常のタイミング関数を取り、そのラッパーを返す変換関数 makeEaseOut があるのだ。

function makeEaseOut(timing) {
    return function(t) {
        return 1 - timing(1 - t);
    };
}

例えば、先ほどの関数 bounce() に適用できる。そうすると、跳ねるのがアニメーションの最初ではなく、最後になる。具合が良い。

let bounceEaseOut = makeEaseOut(bounce);

一般に、easeOut 化すると、アニメーション効果が最初にあれば、最後になる。

  • 通常版:オブジェクトは下部で跳ねて、最後に急激に上部へ飛び上がる。

  • easeOut 適用後:最初に上部へ飛んで、そこで跳ねる。

easeInOut

また、アニメーションの最初と最後の両方で効果を示す変換も考えられる。これを easeInOut と呼ぶ。

function makeEaseInOut(timing) {
    return function(t) {
        if (t < .5)
            return timing(2 * t) / 2;
        else
            return (2 - timing(2 * (1 - t))) / 2;
    }
}

bounceEaseInOut = makeEaseInOut(bounce);

easeInOut 変換はアニメーションの前半は easeIn, 後半は easeOut で合成したようなものだ。関数 circ() の easeIn, easeOut, easeInOut のプロットを比較すると、効果は明白だ。このように、アニメーションの前半のグラフは、easeIn を縮小したもので、後半は easeOut を縮小したものであることがわかる。その結果、アニメーションは同じ効果で始まり、終わる。

More interesting “draw”

要素を移動させる代わりに、他のことをすることができる。必要なのは適切な描画を書くことだ。ここに「跳ねる」テキストタイピングがある。

学習者ノート

ソースを見ると、ほんとうに本書の内容の関数で実装されている。

Tasks

Animate the bouncing ball

弾むボールを作れ。

次の量を先に計算しておく必要がある:

field.clientHeight - ball.clientHeight;

Animate the ball bouncing to the right

ボールを右に弾ませろ。左からの距離は 100px とする。前の問題の解から作れ。

水平方向に等速運動させたい。こういう場合には座標軸別に aminate() するという発想をする。次の呼び出しを前問の解答に追加:

animate({
    duration: 2000,
    timing(t){
      return t;
    },
    draw(progress) {
      ball.style.left = 100 * progress + 'px';
    }
});