Help us understand the problem. What is going on with this article?

JavaScriptで砂嵐

More than 3 years have passed since last update.

はじめに

テレビの砂嵐をご存知だろうか。アナログテレビで画像を受信できない時に出るノイズのようなものである。たまに歌のネタなんかにされたりもするが、テレビの地上波がデジタル化されて久しいので、今の若い人は知らないかもしれない。

テレビの砂嵐には有名な都市伝説がある。地方のテレビ局の夜勤の人が、放送終了後の砂嵐の時間帯に会社の機材でAVを見ようとしたら、うっかり公共の電波に乗せてしまい、すぐに数十件のクレームの電話が来たという。

これは、「放送終了後の砂嵐を見続けている人が少なくとも数十人いる」という点が怖い、というオチなのだが、なんとなく砂嵐を見てしまう気持ちもわからないではない。最近、自分の液晶ディスプレイが変な壊れ方をして砂嵐の画面になった時、妙に見入ってしまった。砂嵐画面には人を引きつける何かがあるようだ。

というわけで砂嵐をJavaScriptで再現してみよう。コードは

https://github.com/kaityo256/noise

に置いてある。

準備

とりあえずHTML5のCanvasを使おう1。砂嵐が全画面で表示されるように、onloadとonresizeでcanvasのサイズを変更する。こんな感じのHTMLになろう。

<!DOCTYPE html>
<html>
<head>
<title>Noise</title>
<meta charset="UTF-8">
<meta http-equiv="Content-Script-Type" content="text/javascript" />
<style>
body{
  margin: 0px;
  height: 100%;
  width: 100%;
  overflow: hidden;
}
</style>
</head>
<body>
<canvas id="canvas"></canvas>
<script type="text/javascript">
var canvas = document.getElementById('canvas');
function expandCanvas(){
  canvas.width = document.documentElement.clientWidth;
  canvas.height = document.documentElement.clientHeight;
}
window.onresize = expandCanvas;
onload = function(){
  expandCanvas();
}
</script>
</body>
</html>

bodyに適当にスタイルを設定し、document.documentElementのclientWidthとclientHeightをcanvasのwidthとheightに設定するようにすれば、所望の動作になると思う。

以下、砂嵐の作成。

Step 1

とりあえず普通にfillStyleとfillRectで描いてみよう。やってみるとわかるが、1ドット単位の描画だと遅くて仕方ないので、2ドット×2ドットで描いてみる。こんな感じ。

function draw() {
  w = canvas.width;
  h = canvas.height;
  s = 2;
  var wi = w/s;
  var hi = h/s;
  var cc = canvas.getContext('2d');
  for (var x = 0;x <wi;x++){
    for (var y = 0;y <hi;y++){
      var n = Math.floor(Math.random()*255);
      cc.fillStyle='rgb('+n+','+n+','+n+')'
      cc.fillRect(x*s,y*s,s,s);
    }
  }
}

これをonloadでsetIntervalに渡してやれば良い。

onload = function(){
  expandCanvas();
  setInterval(draw,10);
}

実際にやってみた画面はこんな感じになる。

うーん、遅いですなぁ。

Step 2

直接canvasに描くのはアレなので、バックグラウンドバッファに描いて、ダブルバッファをやってみよう。こんな感じかな。

var mycanvas = document.createElement('canvas');

function draw() {
  w = canvas.width;
  h = canvas.height;
  mycanvas.width = w;
  mycanvas.height = h;
  s = 2;
  var wi = w/s;
  var hi = h/s;
  var cc = mycanvas.getContext('2d');
  for (var x = 0;x <wi;x++){
    for (var y = 0;y <hi;y++){
      var n = Math.floor(Math.random()*255);
      cc.fillStyle='rgb('+n+','+n+','+n+')'
      cc.fillRect(x*s,y*s,s,s);
    }
  }
  var context = canvas.getContext('2d');
  context.drawImage(mycanvas,0,0);
}

document.createElementでcanvas要素を作り、そこに描いてからdrawImageでコピーしてみる。結果はこんな感じになる。

うーん、たいしてかわらんですなぁ。

Step 3

全画面毎回律儀にランダム要素を発生させてるから遅いんで、あらかじめブロック単位で描いておいて、それをランダムに表示させるのはどうだろう?とりあえず256×256ドットのランダム柄を、7パターンくらい用意して、それをランダムに表示させてみる。

var mycanvas = document.createElement('canvas');
var buf = [];
var bnum = 7;
var size = 256;
function makebuf(){
  for (var i=0;i<bnum;i++){
    var c = document.createElement('canvas');
    c.width = size;
    c.height = size;
    for (var x=0;x<size;x++){
      for (var y=0;y<size;y++){
        var cc = c.getContext('2d');
        var n = Math.floor(Math.random()*255);
        cc.fillStyle='rgb('+n+','+n+','+n+')'
        cc.fillRect(x,y,1,1);
      }
    }
    buf.push(c);
  }
}
function draw(){
  w = canvas.width;
  h = canvas.height;
  mycanvas.width = w;
  mycanvas.height = h;
  var iw = w/size + 1;
  var ih = h/size + 1;
  var cc = canvas.getContext('2d');
  for (var x=0;x<iw;x++){
    for (var y=0;y<ih;y++){
      var s = Math.floor(Math.random()*bnum);
      cc.drawImage(buf[s],x*size,y*size);
    }
  }
  var context = canvas.getContext('2d');
  context.drawImage(mycanvas,0,0);

onload = function(){
  expandCanvas();
  makebuf();
  setInterval(draw,10);
}

makebufという関数を作り、そこでバッファを作って、drawではランダムにそれを表示している。結果はこんな感じ

ありゃ、256×256のパッチが見えてしまっている。ランダムに表示しているため、たまに「同じフレーム」を表示することがあり、そうすると隣のパッチと更新タイミングがずれるために256×256のパッチが見えてしまうんだ。

Step 4

パッチ模様が見えてしまうのは、ランダムにフレームを選ぶことで「同じフレーム」が表示されてしまうから。ということは、フレームをシーケンシャルに選べば、必ず次のイメージが表示されるようになるから、パッチは見えなくなる。こんな感じ。

var s = 0;
function draw(){
  w = canvas.width;
  h = canvas.height;
  mycanvas.width = w;
  mycanvas.height = h;
  var iw = w/size + 1;
  var ih = h/size + 1;
  s = (s+1)%bnum;
  var cc = canvas.getContext('2d');
  for (var x=0;x<iw;x++){
    for (var y=0;y<ih;y++){
      var s2 = Math.floor(y + x*iw + s) % bnum;
      cc.drawImage(buf[s2],x*size,y*size);
    }
  }
  var context = canvas.getContext('2d');
  context.drawImage(mycanvas,0,0);
}

結果はこんな感じになる。

うん、まぁ満足いく速度でノイズが表示された・・・ん?

Step 5

前回、

  • あらかじめ用意された複数のイメージを
  • パラパラマンガの要領でアニメーションさせ
  • それを全画面にタイル表示する

ことで砂嵐を表現したわけだけど・・・

あれ?それってアニメーションGIFじゃね?

というわけで最終的にこうなった。

<!DOCTYPE html>
<html>
<head>
<title>Noise</title>
<meta charset="UTF-8">
<meta http-equiv="Content-Script-Type" content="text/javascript" />
<style>
body{  background-image: url("./noise.gif");}
</style>
</head>
<body>
</body>
</html>

結果はこんな感じ

これでいいじゃん。

っつーかJavaScriptいらんかった・・・○| ̄|_

まとめ

下手の考え休むに似たり。

なお、この記事は、夜なかなか寝ない娘をあやしながら書きなぐったものなので、乱文乱筆バグ間違いはご容赦願いたい。っていうか砂嵐見せたら眠くなるという噂があったけど娘に見せても寝なかった。多分画面よりも音の方が大事なんだと思う。


  1. IE? 知らん。Explorer Canvasとか使えばいいんじゃない? 

Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away