2018 05 20

電光掲示板を作るの3

昨日の続き。 電光掲示板風に変換した文字(の画像集合)を動かす。

ところでキャンバスのアニメーション機能だが、実は無い。 SVGやCSSが持つようなアニメーション機能は何も無い。 キャンバスにできるのは、描くことと消すことだけ。 キャンバス内に描いたものを移動することもできない。

ということで、キャンバスでアニメーションするには

  1. 描く。
  2. 消す。
  3. 少し変化した後の状態で描く。

という繰り返しを自分でやる必要がある。

キャンバスそのものを動かすことはできるので、単純な移動だけならこっちでアニメーションを実現することもできる。

せっかくだから両方やってみよう。 サンプリングの元は、昨日のをそのまま使う。

まずは描いて消しての繰り返しパターン。 サンプリングそのものは昨日と同じだが、サンプリングで採取した値を描画する位置を画素単位で移動させる。

移動させる部分のソース。

<select id="s2"> <option value="1">1</option> <option value="2">2</option> </select> <input type="button" id="b2" value="適用" onclick="apply2()"/> <canvas id="c2" width="400" height="100" style="background:black;"></canvas> <script> function apply2() { const ct1 = document.getElementById("c1").getContext("2d"); const ct2 = document.getElementById("c2").getContext("2d"); const btn = document.getElementById("b2"); const r = Number( document.getElementById("s2").value ); const d = r * 2 + 1; let ofs = 400; btn.disabled = true; const tmr = setInterval( () => { ct2.clearRect( 0, 0, 400, 100 ); for ( let x = r + 1; x < 400; x += d ) { for ( let y = r + 1; y < 100; y += d ) { ct2.fillStyle = "rgba(" + ct1.getImageData( x, y, 1, 1 ).data.slice( 0, 4 ).join( "," ) + " )"; ct2.beginPath(); ct2.arc( ofs + x, y, r, 0, Math.PI * 2, true ); ct2.fill(); } } ofs -= d; if ( ofs <= -400 ) { clearTimeout( tmr ); btn.disabled = false; } }, 100 ) } </script>

画素のサイズは昨日の結論を受けて1か2しか選択できないようにしている。 あと移動中に移動を要求できないように、ボタンの操作可否も制御している。

サイズが1だと、キャンバスのサイズ400*100に対してだいたい4400ぐらいの画素を全描き換えすることになる。 動かす前は結構重い処理じゃないかと思っていたのだが、実際にやってみたら大したことはなかったな。 CPUの使用状況も跳ね上がったりしないし。

と、ちょっと安心したのだが、大したことないのはMacBookProだからかもしれないと思い直して、すでに絶品のnexusでやってみたらなかなか厳しい状況だった。 動かないことは無い。 が、快適とは言い難い。 サイズ2を選択すればまだマシだが、サイズ1はかなりもっさり。

手元にある他の環境では、iPadは問題無い。 iPhone5sは微妙。 でもまあ、今の時代にiPhone5sで微妙程度なら問題無いのかな。

これ、描き換えデータはずっと変わらなくて、描き込む位置だけが違うだけなんだよな。 だからタイマーの周期毎に元画像からサンプリングするのではなく、最初にサンプリングした結果を使い回すことができる。 すぐに思いつく改善点はここだが、それでもっさりが改善されるかどうかは、やってみないと判らない。 キャンバスのオブジェクトがメモリ上にデータを持っているのだとすると、それを自分用のローカル配列に移し替えるのは、メモリとCPUの無駄遣いでしかないし。 圧倒的なボトルネックはキャンバスの描き換えそのものだろうし。

でもまあせっかくだから試してみる。

元となる文字列の設定は、上のをそのまま使う。 そして移動させる部分のソース。

<select id="s3"> <option value="1">1</option> <option value="2">2</option> </select> <input type="button" id="b3" value="適用" onclick="apply3()"/> <canvas id="c3" width="400" height="100" style="background:black;"></canvas> <script> function apply3() { const ct1 = document.getElementById("c1").getContext("2d"); const ct3 = document.getElementById("c3").getContext("2d"); const btn = document.getElementById("b3"); const r = Number( document.getElementById("s3").value ); const d = r * 2 + 1; const v = []; for ( let x = r + 1, vx = 0; x < 400; x += d, vx++ ) { v[vx]=[]; for ( let y = r + 1, vy = 0; y < 100; y += d, vy++ ) { v[vx][vy] = ct1.getImageData( x, y, 1, 1 ).data.slice( 0, 4 ); } } let ofs = 400; btn.disabled = true; const tmr = setInterval( () => { ct3.clearRect( 0, 0, 400, 100 ); for ( let x = r + 1, vx = 0; x < 400; x += d, vx++ ) { for ( let y = r + 1, vy = 0; y < 100; y += d, vy++ ) { if ( 0 <= ofs + x && ofs + x <= 400 ) { ct3.fillStyle = "rgba(" + v[vx][vy].join( "," ) + " )"; ct3.beginPath(); ct3.arc( ofs + x, y, r, 0, Math.PI * 2, true ); ct3.fill(); } } } ofs -= d; if ( ofs <= -400 ) { clearTimeout( tmr ); btn.disabled = false; } }, 100 ) } </script>

やってみた結果は期待通りだった。 つまり大して改善されなかった。

描き換えそのものがどの程度の負荷になっているかを確認するために、描画しなくていいところは描画しないようにもしている。 これでnexusで実行すると、最初とか最後の描画領域が小さいうちは速く、描画領域が広くなるにつれて遅くなり、文字列が全て表示される間はもっさり。 とは言えもっさり具合は毎回サンプリング版よりは軽目なので、効果が無い訳ではないようだが。

次はキャンバスそのものを動かすパターン。

ここでも、元となる文字列の設定は一番上のをそのまま使っている。 移動させる部分のソースは以下の通り。

<select id="s4"> <option value="1">1</option> <option value="2">2</option> </select> <input type="button" id="b4" value="適用" onclick="apply4()"/> <div style="width:400px;height:100px;background:black;overflow:hidden;"> <canvas id="c4" width="400" height="100" style="position:relative;left:0;top:0;"></canvas> </div> <script> function apply4() { const ct1 = document.getElementById("c1").getContext("2d"); const ct4 = document.getElementById("c4").getContext("2d"); const cs4 = document.getElementById("c4").style; const btn = document.getElementById("b4"); const r = Number( document.getElementById("s4").value ); const d = r * 2 + 1; let ofs = 400; btn.disabled = true; cs4.left = ofs + "px"; ct4.clearRect( 0, 0, 400, 100 ); for ( let x = r + 1; x < 400; x += d ) { for ( let y = r + 1; y < 100; y += d ) { ct4.fillStyle = "rgba(" + ct1.getImageData( x, y, 1, 1 ).data.slice( 0, 4 ).join( "," ) + " )"; ct4.beginPath(); ct4.arc( x, y, r, 0, Math.PI * 2, true ); ct4.fill(); } } const tmr = setInterval( () => { cs4.left = ( ofs -= d ) + "px"; if ( ofs <= -400 ) { clearTimeout( tmr ); btn.disabled = false; } }, 100 ) } </script>

試す前から判っていたことだが、こいつが一番軽い。 キャンバスへの描画は1回だけで、後は画像をスライドするのと同じなので、性能が画素の大小に影響され無いのも大きい。 MacBookProじゃ微妙な差しかなかったが、nexusだとはっきりと軽くなっていると体感できる。

欠点は複雑な動きができないことだが、複雑な動きをさせる必要が無いならこれが正解だろう。

今日はここまで。