Home‎ > ‎

映画「マトリックス」の再現JavaScriptコードを読む

2013/03/22

1. はじめに

映画「マトリックス」で有名な,文字が落ちてくる映像を JavaScript で再現したコードがある.そのサイトはこちら

とてもコンパクトなコードなので,どういう処理をしているのか分かりにくい.そこで,少し読み解いてみた.

2. 読みやすいように変換

まず,もともとのコードを見てみると

<body style=margin:0 
onload="for(s=window.screen,w=q.width=s.width,h=q.height=s.height,m=Math.random,p=[],i=0;i<256;p[i++]=1);setInterval('9Style=\'rgba(0,0,0,.05)\'9Rect(0,0,w,h)9Style=\'#0F0\';p.map(function(v,i){9Text(String.fromCharCode(3e4+m()*33),i*10,v);p[i]=v>758+m()*1e4?0:v+10})'.split(9).join(';q.getContext(\'2d\').fill'),33)"><canvas
 id=q>

難読という程ではないが,それでも読みにくい.手始めにHTMLタグの解釈から始めよう.タグはbodyとcanvasの2つだけであることが分かる.

<body style=margin:0 onload="...">
<canvas id=q>

bodyタグはスタイルにマージンを 0 に設定しているのと,HTML の読み込みが終わった時に実行される JavaScript を,onloadで指定している.また,canvasタグのIDを q とし,JavaScript から変数 q でアクセスできるようにしている.

では,onload で指定された JavaScript を見てみよう.実はセミコロンで区切られた2つの文しかない.

for(s=window.screen,w=q.width=s.width,h=q.height=s.height,m=Math.random,p=[],i=0;i<256;p[i++]=1);
setInterval('9Style=\'rgba(0,0,0,.05)\'9Rect(0,0,w,h)9Style=\'#0F0\';p.map(function(v,i){9Text(String.fromCharCode(3e4+m()*33),i*10,v);p[i]=v>758+m()*1e4?0:v+10})'.split(9).join(';q.getContext(\'2d\').fill'),33)

つまるところ,for 文による初期化と,setInterval による定期的な画面描画の2つである.読みにくい原因の1つが,setInterval の第1引数にある,京都銀行もびっくりの,なが〜い文字列だ.ここの構造を見てみると

setInterval('..9を含む文字列..'.split(9).join('..置換え文字列..'), 33)

まず,最初の文字列を split で配列に分割している.だが,引数が 9 とは!?.本来,この引数は文字列か正規表現オブジェクトであるので,数字の 9 は文字 "9" に変換されると思われる.良い子は真似をしないほうがいいと思う.あと「何故 9 なのか」とも思うが,この文字列であるソースコードには,2,6 を除き 0 から 8 までの数字がすべて使われている.変数に使える文字だと変数と解釈されてしまうので,2,6,9 しか残っていなかったということか.

ともかく "9" で分割された配列を,join で文字列に繋げている.要するに,最初の文字列に対し, split の引数になっている文字列を, join の引数の文字列に置換しているだけである.ということで,9;q.getContext(\'2d\').fillに置換し,セミコロンで改行して見やすくしてみる.

for(s=window.screen,w=q.width=s.width,h=q.height=s.height,m=Math.random,p=[],i=0;i<256;p[i++]=1);
setInterval(';
    q.getContext(\'2d\').fillStyle=\'rgba(0,0,0,.05)\';
    q.getContext(\'2d\').fillRect(0,0,w,h);
    q.getContext(\'2d\').fillStyle=\'#0F0\';
    p.map(function(v,i){;
        q.getContext(\'2d\').fillText(String.fromCharCode(3e4+m()*33),i*10,v);
        p[i]=v>758+m()*1e4?0:v+10
    })'
  ,33)

かなり見やすくなったが,もう少し読みやすく書き換えてしまう.書き換えた点は

  • setIntervalの第1引数である文字列は,関数表記にする.
  • 変数 s, w, h, m は省略のために短い変数名に置き換えているだけなのでそれらを下記の通り戻す
    • s = window.screen
    • m = Math.random など
  • 1e4 などの指数表記を 10000 の数値表記する
  • 変数 q は canvas タグへの参照なので canvas とする
  • for 文の最初は初期化なので,for 文の前に出す
canvas.width = window.screen.width;
canvas.height = window.screen.height;
p = [];
for(i = 0; i < 256; p[i++] = 1);

setInterval(function(){
    canvas.getContext('2d').fillStyle = 'rgba(0,0,0,.05)';
    canvas.getContext('2d').fillRect(0, 0, window.screen.width, window.screen.height);
    canvas.getContext('2d').fillStyle = '#0F0';
    p.map(function(v,i){
        canvas.getContext('2d').fillText(String.fromCharCode(30000 + Math.random() * 33), i * 10 ,v);
        p[i] = v > (758 + Math.random() * 10000) ? 0 : (v + 10);
    });
  }, 33);

これでようやく読みやすくなった

3. 処理を順に読んでみる

まず最初に

canvas.width = window.screen.width;
canvas.height = window.screen.height;
p = [];
for(i = 0; i < 256; p[i++] = 1);

canvas エリアのサイズを画面サイズに設定し,256個の要素を持つ配列 p のすべての要素を 1 に初期化する.この配列は各行ごとの文字を描画する高さを表している

setInterval(function(){
 ...
}, 33);

33ms 毎つまり 30fps で画面描画を行う.この後は,描画処理について

    canvas.getContext('2d').fillStyle = 'rgba(0,0,0,.05)';
    canvas.getContext('2d').fillRect(0, 0, window.screen.width, window.screen.height);

毎回,描画前にキャンパス全体を,透明度 0.05 の黒で塗りつぶしている.つまり,描画する毎に画面がフェードアウトしていく

    canvas.getContext('2d').fillStyle = '#0F0';

続いて,文字の描画を行うので,「マトリックス」の特徴でもある,緑に設定する

    p.map(function(v,i){
        canvas.getContext('2d').fillText(String.fromCharCode(30000 + Math.random() * 33), i * 10 ,v);
        p[i] = v > (758 + Math.random() * 10000) ? 0 : (v + 10);
    });

ここが文字描画の中心である.文字コード 30000 から 30032 までの文字をランダムで1つ選び,それを座標 (i*10, v) に表示している.配列 p が 256 個の要素を持つので,i が 0 から 255 まで,つまり X座標が 0 から 2550 まで横に広がる.ということは,横の解像度が 2560 を超えるディスプレイだと,右端が表示されないということか.(そんなディスプレイ持ってないので確認できません...)

一方,Y座標は,配列 p の要素の値なので,最初はすべて 1 で,その後 10 ずつ加算されて下に落ちていく.このサイト,最初に文字が一斉に上から揃って落ちてくるが,それが配列 p のすべての要素が同じように 10 ずつ加算されているだけだからである.そして,758 を超えると,758 から 10757 までのランダムで選ばれた値をしきい値にして 0 にリセットされるので,行毎にばらばらに上から文字が降ってきているように演出される

4. おわり

なんか読みにくいソースかなっと思ったけど,内容はいたってシンプルだった.頻繁に出てくる q.getContext('2d').fill を文字列置換処理で置き換えているところがミソだろうか.個人的には split(9) が一番衝撃だった...

Comments