Practice of Programming

プログラム とか Linuxとかの話題

JavaScriptのcanvasで遊んでいたらPCのパフォーマンスが上がった

はい。嘘です。

正しくは、PCが遅い原因がわかった。でした。顛末は最後に書いていますが、最初は、canvasの使い方から。

canvasの使い方

HTML

HTMLで下記のように書きます。

  <canvas id="canvas"  width="300" height="170"></canvas>

JavaScript

基本

cotnext
const c = document.getElementById('canvas');
const ctx = c.getContext('2d');

context を取得してから、描画をします。

ctx.beginPath()

beginPath をしないと、最後にいた座標から、線を描き出してしまう場合がありますが、これを呼んでおけば問題ないです。

stroke, fill

storke の場合は、線を描きますが、fillの場合は、中身を塗ります。

  • strokeRect, fillRect
  • stroke, fill

のように、対になっています。

座標

x= 0, y =0 は、左上になります。 最初に定義したcanvasだと、x=300, y = 170 が右下になります。

線を描く

書きたい場所に移動して、

ctx.moveTo(x, y);
ctx.lineTo(x, y);
ctx.stroke();

moveToで移動した、x, y から、lineToで指定した、x, y まで線を描画します。

最初に、moveToで、移動して、そこから、lineTo で線の終端をさし、stroke で線を描きます。

四角を描く

strokeRectを使います。

ctx.strokeRect(x, y, xの幅, yの幅)

円を描く

ctx.beginPath()。
ctx.arc( x, y, radius, startAngle, endAngle,  counterclockwise) ;
ctx.stroke();

radius は半径です。startAngleは開始角度で、endAngleは終了角度。counterclockwiseは、trueなら左回りに線を描きます。

startAngle = 0, endAngle = Math.PI * 2 であれば、真円が描けます。 半円であれば、endAngle = Math.PIです。

ctx.beginPath() をしないと、今いる座標から、円の開始点までの線も描画してしまうので、注意してください。

下向きの半円

ctx.beginPath();
ctx.arc( 10,10, 10, 0, Math.PI,  false) ;
ctx.stroke();

下向きの1/4の円

ctx.beginPath();
ctx.arc( 10,10, 10, 0, Math.PI / 2,  false) ;
ctx.stroke();

fill で中身を塗りつぶしたい場合は、

ctx.moveTo(10,10);
ctx.arc(10, 10, 10, 0, Math.PI / 2,  false) ;
ctx.fill();

fill を使う場合、moveToで移動した座標から、円の開始点から終了点を結んだ部分を塗ります。

なので、下記のようにすると、

ctx.moveTo(90,90);
ctx.arc( 40,40, 50, 0, Math.PI / 2, false) ;
ctx.fill();

外側が塗りつぶされます。

描画したものを消す

clearRect を使います。strokeRectと使い方は同じです。

ctx.clearRect(x, y, xの幅, yの幅);

これで、指定した座標の位置から、x, y の領域を削除します。消す領域ですが、若干多めに取っておいたほうが良いかもしれません。

textを描く

ctx.fillText("テキスト", x, y, 幅);

x, y の座標から幅分の領域に、指定したテキストを描画します。

x, y の位置ですが、テキストの左下が、座標の位置です。 一度描いたものを削除する場合、clearRectで指定するのは、y - フォントサイズ + アルファ くらいを指定するひつようがあります。 y の幅指定も、フォントサイズ+アルファくらいです。

フォントの指定は、下記のように行います。

ctx.font  = '9px serif';

canvas の良いところ

プログラマブルに絵を描けるので、配置場所や、スケールも自由に変更できます。




下向きにに複数の円が描かれる。

const c = new MyCircle({id: 'canvas', x: 0, y: 0, scale: 1, direction: false}); c.draw();

上向きに複数の円が描かれる。

const c = new MyCircle({id: 'canvas', x: 0, y: 100, scale: 1, direction: true}); c.draw();

と行った感じにできます。

PCのボトルネックに気づいた理由

アニメーションをしようと思って、setTimeoutで、canvasに描画しまくっていたわけです。

先程のclassに、下記のようなメソッドを追加した感じですね(実際はもっと色々やってた)。

   animate (max) {
        const ctx = this.ctx;
        max ||= 50;
        ctx.clearRect(0,0, this.canvas.width, this.canvas.height);
        const fn = (x) => {
            x ||= 0;
            ctx.beginPath();
            ctx.arc( this.x + x, this.y + x * (this.direction ? -1 : 1), x, 0, Math.PI * 2, false);
            ctx.stroke();
            if (x < max * this.scale) {
                setTimeout(() => {fn(x + 1)}, 1);
            }
        };
        fn();
    }

そうすると、描画がクソ重いのが、目に見えてわかるわけですね。

で、たまたま、外部ディスプレイが切れて、ノートPCの方の解像度が変更されたタイミングでスピードがかなり改善されました。

重い原因は何だったのか?

Ubuntuを使っているのですが、解像度の設定と、画面の拡大を指定することが出来ます。下記の赤ラインで引いたところです。

ここを調節していると、思った以上にパフォーマンスが落ちるようです。 Firefoxがまともに動かないレベルで重かったのですが、これを100%にしたら、普通に動きました。

というわけで、せっかくなので、canvasを使ったベンチマークを作りました。反復横飛びの片道で1とカウントしています。 (※遊びでつくっただけです。めっちゃ雑な計測です)

www.rwds.net

スペックは、下記な感じです。

外部ディスプレイ(3840x2160:100%) ノートPCのディスプレイ(3840x2160:100%)

外部ディスプレイ(3840x2160:100%) ノートPCのディスプレイ(1960x1200:100%)

外部ディスプレイ(3840x2160:100%) ノートPCのディスプレイ(3840x2160:125%)

外部ディスプレイ(3840x2160:125%) ノートPCのディスプレイ(3840x2160:125%)

外部ディスプレイ(3840x2160:150%) ノートPCのディスプレイ(3840x2160:125%)

外部ディスプレイ(3840x2160:125%) ノートPCのディスプレイ(1920x1200:100%)

外部ディスプレイ(3840x2160:125%) ノートPCのディスプレイ(3840x2160:100%)

結論

外部ディスプレイとノートPCのディスプレイをいずれかを拡大している場合に一番、パフォーマンスが劣化するようです。

なお、Windoows のSurface Pro 7 Core i7 1.3GHz だと、0.9/sec でした。スマホ(Xiaomi Readmi Note 10 Pro)も0.9/secでした。