BLCK4777の絵の作られ方も知っておきたい

JavaScriptデモBLCK4777音の作られ方について前に調べたので、ついでにその絵の方もどのように作られているか調べてみた。多分に憶測が入っているけど、多分以下の様な感じ、のはず。

前の記事で追ったAudio生成部分を除いてコードを整理してみると、以下になる。

ctx = b.getContext('2d');
p = 0;

window.onload = function() {
  b.style.background = "radial-gradient(circle,#345,#000)";
  b.style.position = "fixed";
  b.style.height = b.style.width = "100%";
  b.height = 720;
  h = b.style.left = b.style.top = A = f = C = 0;
  //       x, y, vx, vy,    r, color1, color2, idx,
  p = [    0, 0,  0,  0,  180,      2,      0,   1, // triangle 1
        -360, 0,  0,  0,   99,      1,      0,   2, // triangle 2
         360, 0,  0,  0,   99,      1,      0,   3, // triangle 3
       -2880, 0,  0,  0, 1280,      0,   1280,   0];// line
  u();
}

まず最初にCanvasのstyle設定や変数の初期化を行っている。変数'p'が重要で、画面に多数ある三角形はこの配列で制御されている。一つの三角形は8つのパラメタで管理されている。左からx, y座標, x, yの移動量, 大きさ, 色の設定用パラメタ2つ, インデックスだ。初期設定されているのは画面に最初からある横に並んでいる大三角形3つと、その大三角形を貫く線の4つだ。

function u() {
  requestAnimationFrame(u);
  g = f + 1;//audio.currentTime * 60;
  for (; g > f; h *= f % 1 ? 1 : 0.995) {
    s = Math.pow(Math.min(f / 5457, 1), 87) +
      Math.pow(1 - Math.min(f / 5457, 1), 8);
    if (f == [1280, 1599, 2175, 2469, 2777, 3183,
              3369, 3995, 4199, 4470, 4777, 5120][C]) {
      C++;
      h = 640;
    }
    b.width = 1280;
    ctx.translate(640, 360 + h / 45 * Math.random());
    ctx.rotate(A / 5457 - h / 5457);
    ctx.scale(1 + s * 8, 1 + s * 8);
    f++;

続いてrequestAnimationFrameで回る1フレーム毎の描画部分。gは音の経過時間、fは現在のフレーム数を表していて、これらの比較は絵と音の同期を取るために使われている。今回は音の部分を外してコードを整理してしまったため、gにはfより1フレーム先を設定し、同期処理を行わないようにした。

sは少しずつ引くカメラワークのために後でctx.scaleで使われている。

fと比較している配列は、大三角形が光るタイミングのフレーム数を表している。hに640を設定していることをトリガに、後で色の制御を行う。Cは何回光ったかに相当する値で、音の場面転換などにも使われる。

b.width = 1280は画面サイズを設定するとともに、画面を消去している。その後のctxに対する操作はカメラのブレや回転の制御用だ。

    i = p.length;
    for (; i; ) {
      y = p[i -= 7];
      x = p[i ^= 1];
      r = p[i + 4];
      l = p[i + 6];
      s = 2 * Math.random() + 1;
      t = s * 4;
      a = 122;
      if (640 > r) {
        // triangle
        // y += vy
        if (!(640 > Math.abs(p[i ^= 1] += p[i + 2]))) {
          p[i + 2] *= -1;
        }
        // x += vx
        if (!(640 > Math.abs(p[i ^= 1] += p[i + 2]))) {
          p[i + 2] *= -1;
        }

ここからpの中の各三角形の処理。まず三角形のパラメタを取り出す。rが640より小さいのは三角形で、それより大きいのは線だ。まず三角形の場合、x, y座標の移動、および画面端にぶつかった場合の反転処理をしている。i ^= 1がちょっと面妖だが、ビットのXORを取っていて、偶数を奇数、奇数を偶数にする処理をしている。これでy座標、次にx座標という処理が行える。

        t = Math.random() > p[i + 7] ||
          p[i + 7] == "22312131212313"[C] & h == 640;

三角形を光らせる処理。大三角形が光るフレーム数の時はhが640になるので、文字列と対応するインデックスを持つ三角形の色を設定する。それ以外の小さな三角形もたまに光るようになっている。

        w = x - A;
        if (!p[i + 2]) {
          if (r * r / 3 > w * w) {
            t = s * (r - Math.abs(w)) / 45 + 2;
            a = 2 * Math.random() + 5;
            p.push(A, 0, s * Math.sin(a += 599), s * Math.sin(a - 11),
                   s * t, C + s, 640, 0.995);
            s = 2 * Math.random() + 1;
            a = 2 * Math.random() + 5;
            p.push(A, 0, s * Math.sin(a += 599), s * Math.sin(a - 11),
                   s * t, C + s, 640, 0.995);
            s = 2 * Math.random() + 1;
            a = 2 * Math.random() + 2;
            p.push(A, 0, s * Math.sin(a += 599), s * Math.sin(a - 11),
                   s * t, C + s, 640, 0.995);
          }
        }
        a = p[i + 2] * y / 45;
        l = p[i + 6] = t ? 640 : 0.9 * l;
        t = r;

線が大三角形を貫く時に小さな三角形をばらまく処理。Aが線の先端のx座標を表しているので、それを使って衝突を判定、衝突している時はpにパラメタを追加することで、小さな三角形をばらまいている。

      } else {
        // line
        A = p[i]++;
      }

線の処理、x座標をインクリメントしているだけ。これで右に動く。

      if (!(g > f)) {
        s = r;
        ctx.beginPath();
        ctx.lineTo(x + s * Math.sin(a += 599), y - s * Math.sin(a - 11));
        s = t;
        ctx.lineTo(x + s * Math.sin(a += 599), y - s * Math.sin(a - 11));
        ctx.lineTo(x + s * Math.sin(a += 599), y - s * Math.sin(a - 11));
        ctx.shadowBlur = r;
        s = l;
        x = s * 2;
        a = p[i + 5];
        ctx.shadowColor = ctx.fillStyle = "rgb(" +
          [x + s * Math.sin(a += 599) | 1,
           x + s * Math.sin(a += 599) | 1,
           x + s * Math.sin(a += 599) | 1] + ")";
        ctx.fill();
      }
    }
  }
  ctx.fillText("BLCK4777", 90, 99);
};

描画処理。gfは音と絵の同期用。三角形のパラメタにしたがってctx.lineToで頂点座標を設定し、シャドウのブラー、色を設定、fillする。最後に固定のメッセージBLCK4777を表示。

うん、実は全体の流れは結構分かりやすかった。もちろん実際は細かなパラメタの値などに演出の妙がたくさんあって、その辺の加減が肝なんだけども、そこを深追いしなければ読み解けないこともなさそう。一見とっつきにくそうに見えるデモのコードも、ちゃんとフローに応じて読みやすい形に展開すれば、なんとか追うこともできるかも、という一条の光が得られました。

デモコーダーにはもちろんショートコーディングのテクニックも必要なんだけども、限られたコードでどのような演出をし、音との同期も考えた盛り上がりポイントをどう入れるか、っていうところの力量もかなり必要とされているんだなあという印象を受けた。BLCK4777だとh = 640のタイミングで、光り、音が変化し、パーティクルが飛び始める、これが短いコードの中で効率良く実現されているのが美しいですな。

追記:ここ2つの記事で分かった内容を受けて整理、コメントを足したBLCK4777のコードをGitHubに置いた