2018 05 21

電光掲示板を作るの4

昨日の続き。 今日はもう少し汎用性を持たせてみる。

昨日までのは、入力欄には30文字が入力可能だが、表示するキャンバスの幅は全然足りてなかった。 半角なら8文字分、全角なら5文字分ぐらいしかなくて、この幅を超えた部分は描画されない。 いや、描画はされているのだが、表示範囲外には何を描いても透明な黒になってしまうので見えないのだな。 これを、ちゃんと30文字分は表示できるようにしたい。 いっそ100文字ぐらい扱えるようにしたい。

じゃあキャンバスの幅を広げればいいのでは? と単純にはいかなくて、長くすると今度は動かす処理を考える必要がある。

昨日までのは、動かす処理にキャンバスの幅をそのまま使用していた。 具体的には、表示幅400に対して、x座標が +400 から -400 への移動。 動かすのが描画位置にせよキャンバスそのものにせよ、動かす時の始点終点は同じ。

サンプルに使った 「高幡不動」 なら、文字列の幅と表示幅とが大体同じなので、これでもよかった。 キャンバスの幅を広げても、キャンバス全体が文字で埋まっているならいい。

でも例えば1文字だけを動かした場合、キャンバスの幅をそのまま使うと、残りの見えない数十文字分が表示領域外になるまで待たなきゃいけない。 この待ち時間とその間の処理が全くの無駄なんだよな。

そんなこんなをあれこれ考えた結果。

ソース。

<input type="text" id="t1" size="30" maxlength="100" value="高幡不動" /> <select id="s1"> <option value="1">1</option> <option value="2">2</option> </select> <input type="button" id="b1" value="適用" onclick="apply()"/> <div style="display:none;"> <canvas id="c1" width="8000" height="100" style="background:black;"></canvas> </div> <div style="width:400px;height:100px;background:black;overflow:hidden;"> <canvas id="c2" width="8000" height="100" style="position:relative;left:0;top:0;"></canvas> </div> <script> function apply() { const ct1 = document.getElementById("c1").getContext("2d"); const ct2 = document.getElementById("c2").getContext("2d"); const cs2 = document.getElementById("c2").style; const txt = document.getElementById("t1").value; const btn = document.getElementById("b1"); const r = Number( document.getElementById("s1").value ); const d = r * 2 + 1; ct1.clearRect( 0, 0, 8000, 100 ); ct1.font = "80px sans-serif"; ct1.fillStyle = "red"; ct1.fillText( txt, 0, 80 ); let ofs = 400; cs2.left = ofs + "px"; ct2.clearRect( 0, 0, 8000, 100 ); btn.disabled = true; let eot = Math.floor( txt.length * 80 / d + 0.5 ) * d; for ( let x = r + 1; x < eot; 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( x, y, r, 0, Math.PI * 2, true ); ct2.fill(); } } for ( let x = eot; x > 0; x -= d ) { let col = []; for ( let y = r + 1; y < 100; y += d ) { col.push( ct1.getImageData( x, y, 1, 1 ).data.slice( 0, 4 ) ); } if ( col.some( p => p.some( q => q ) ) ) { eot = x + d; break; } } const tmr = setInterval( () => { cs2.left = ( ofs -= d ) + "px"; if ( ofs <= -eot ) { clearTimeout( tmr ); btn.disabled = false; } }, 100 ) } </script>

構成要素は昨日までと同じで、文字を描画するためのキャンバスと、そこからサンプリングした結果を描画するためのキャンバスとの2段構成。 80pxの文字を最大100表示するために、キャンバスの幅はどちらも8000にしている。 で、文字を描画する方は非表示。 サンプリング結果を表示する方は、幅400分だけの表示窓の裏側において、キャンバスの位置を動かすことでアニメーションしている。

アニメーションする領域を表示する文字列に応じて決定しているのだが、その決め方は少々泥臭い。

たぶん環境依存なのだが、俺の環境だと、文字幅は全角なら80、半角なら50ぐらい。 なのでとりあえず文字数×80として末端の当たりをつける。 そこから右に向かって、縦列の全サンプリング位置の画素を調べる。 そして一つでも透明な黒じゃない画素を持つ列が見つかったら、その一つ左の列位置を末端としている。

文字が何も入力されていないことのチェックはしていない。 した方がいいのだろうが、しなかったところで10秒程度待たされるだけだからね。 実害は無いと言っていいだろう。

あと諸々の数値を直接書いてたり変数名が必要以上に短かったりするのが気にはなるが、見なかったことにして、ここまでで一応の完成とする。

ちょっと長い文章、例えば 「高幡不動尊では6月1日から紫陽花祭りを開催します。」 とか 「高幡不動駅付近で発生した人身事故のため、京王線は全線運休です。」 とかを表示させると、なかなか電光掲示板っぽくていい。 何の役にも立たないが。