canvasでtypescript を入門してみた

はじめに

こんにちは、たじまです。

この記事は Aizu Advent Calender 2019 の 12/9 の記事です。

すっごい遅刻で申し訳ない orz

adventar.org

つくったもの

f:id:tjmschk:20191218200930j:plain
THE MATRIX

以前、CAのインターンの時に初めてtsを触りその安全さ(jsと比べて)と設計をしっかりしていればパズルのように楽に書けて感動したのでもう一度触ってみたかったのと、

canvasでtsは型がしっかりしているから楽だよという話を聞いたので、matrixのあのeffectをcanvasで作ってみました。

完成したのはこちら。

https://matrix.schktjm.now.sh/

www.youtube.com

実装

絵画のループ処理をするのに以前は setInternal を使っていたのですが、 今回はWindow.requestAnimationFrame を使ってみました。

Window.requestAnimationFrame とは

The window.requestAnimationFrame() method tells the browser that you wish to perform an animation and requests that the browser calls a specified function to update an animation before the next repaint. The method takes a callback as an argument to be invoked before the repaint. --MDN

特徴として以下の点があります。

  • ブラウザの絵画更新と同時に呼び出される
  • タブがバックグラウンドや非表示の <iframe> の場合、パフォーマンス改善のためにFPSが下がる。

setIntervalはブラウザがアクティブの有無に関わらず、常に一定間隔で実行されるため今回のような場合は requestAnimationFrame のほうが優しいかなと思い使いました。 (それでもchromeは何回か落としてしまいましたが)

実装方法

f:id:tjmschk:20191218161014p:plain

今回、この縦の文字列を1つの状態としてそれを配列で持ち requestAnimationFrame でそれをforEachでループしレンダリングしました。

持っていた状態として

interface Oneline {
    fontSize: number, 
    x: number, // x座標
    delay: number,  // 降りてくるタイミング
    v: number, // 速度
    color?: RGB,
}

難しかった点

はじめにやろうとした方法は、各 line がxミリ秒ごとにy fontSize分下がるという実装を想定していたため、各line で 絵画のタイミングを計算しようとしていたため、絵画のタイミングを下のようにしていました。

    const step = (timestamp: DOMHighResTimeStamp) => {
 
            lines.forEach(x => {   
                if (Math.floor(timestamp - x.delay) % x.v == 0) {
                    // レンダリング
                }
            })
        }

        window.requestAnimationFrame(step)
    }
    window.requestAnimationFrame(step);

しかし、requestAnimationFrame はcallback関数が呼ばれるのはブラウザのリフレッシュレートによるところが多いため、一定間隔で呼ばれず上記の方法で実装すると時々(しょっちゅう) カクついたりしていました。

参考

The number of callbacks is usually 60 times per second, but will generally match the display refresh rate in most web browsers as per W3C recommendation. --MDN

そこでxミリ秒ごとに更新じゃなく、xミリ秒の時にどこにいるかを計算する方向に変えました。ただし、呼び出し毎に更新をすると文字が見えないレベルで早すぎたので 30ミリ秒毎に更新するようにしました。

    let lastTime: number = null;
    const step = (timestamp: DOMHighResTimeStamp) => {
        if (lastTime === null) lastTime = timestamp;
        if (timestamp - lastTime > 30) {
            lastTime = timestamp;

            lines.forEach(x => {
                const height = (timestamp - x.delay) / x.v * x.fontSize;
                // レンダリング
            })
        }

        window.requestAnimationFrame(step)
    }
    window.requestAnimationFrame(step);
};

文字列の状態はレンダリング関数中でランダムに生成していたので、一文字ごとに降りるという想定していたアニメーションはできませんでしたが、やってみたらこっちのほうがmatrixぽかったので最初からこっちにすればよかったです 笑

おわりに

10日も遅れてしまってすごい申し訳ない。:pray: この記事に対するコメント、ツッコミ大歓迎です。

typescript入門するはずがアニメーションのレンダリングで詰まっていたのが多かったのでリファクタリングしてもう少し型の優位性を理解したいと思います。

f:id:tjmschk:20191218223833j:plain
型の優位性を理解しているのか?!