SVGのPathの当たり判定を取る方法

Pathで囲まれたSVGの当たり判定を取ることができれば、ベクター形状の間で正確に当たり判定が取れる。現代版1ドットのエクスタシーを魅せることが可能だ。以下の様なコードを書いた。

stars stars

SVG自体にはバウンディングボックスを使ったおおまかな当たり判定の仕組みしかないので、厳密な当たり判定は自前でなんとかするしかない。

この記事にあるようにまずバウンディングボックスでの衝突を判定し、衝突している場合のみパスを使った厳密な当たり判定を取るのがパフォーマンスの面から見て望ましい。

バウンディングボックスはこのスレにあるように、getBBox()で取ったバウンディングボックスを自力でtransformすることで得た。

パスを使った当たり判定は、まじめにパスとパスの交差を計算するととても大変なので、いくつかの線分で近似するのが現実的だろう。SVGPathElementにはgetPointAtLength()というパスの始点から特定の長さの位置にある点を取る関数があるのでこれを利用する。getTotalLength()で取ったパス全体の長さを適当な数で割ってその長さごとの点で囲まれた多角形を当たり判定とする。

実際に当たり判定を行うのはsat-jsに丸投げ。ただsat-jsのPolygonの当たり判定は凸面のポリゴンにしか対応してないので、凹面の多角形は適当な凸面ポリゴンに分割する必要がある。今回は安直に多角形の中心点と各辺から成る三角形に分割した。

こうすればそこそこ正確に当たり判定を取れるはずだが、処理としては結構重たいかも。ゲームで使っていいスピードが出るかどうかは微妙かもしれず。

Elmのエラーメッセージが分かりやすくなっている

だいぶ前にElm触ってゲーム作った時はElmに色々と不満があったんだけど、あれからElmもだいぶバージョンアップして0.15.1ではエラーが人フレンドリーになったらしい。前回の不満がどれだけ解決されているか調べてみよう。

関数の引数の数間違いが謎の型エラーになったりする

module Test where

import Html exposing (text)

message2: String -> String -> String
message2 world world2 = "Hello, " ++ world ++ " " ++ world2

main = text (message2 "World?" "World!")

というメッセージを表示するだけのコードを書いて、

main = text (message2 "World?")

と引数の数を間違うと、

The 1st argument to function `text` has an unexpected type.

8|        text (message2 "World?")
                ^^^^^^^^^^^^^^^^^
As I infer the type of values flowing through your program, I see a conflict
between these two types:

    String

    String -> String

8行目のここの引数がStringで無くString -> Stringだぞと出る。素晴らしい。抜群に分かりやすくなっとる。

レコードのアップデートと追加構文を間違った時のパーサーのエラーが謎だった

main関数を

main =
  let
     world = {str = "World??"}
     world2 = {world | str2 = "World!!"}
  in text (message2 world2.str world2.str2)

とし、

     world2 = {world | str2 = "World!!", str3 = "World?!"}

と許可されていない複数のフィールド追加を行うと、

11|      world2 = {world | str2 = "World!!", str3 = "World?!"}
                                           ^
I am looking for one of the following things:

    a closing bracket '}'
    an expression
    an infix operator like (+)
    whitespace

とちゃんとここで'}'を閉じろと言う。素晴らしい。

型の宣言でのエラーや変数名の重複でその行番号を教えてくれない

message2: String -> String -> String2

とすると

Cannot find type `String2`

5| message2: String -> String -> String2
                                 ^^^^^^^
Maybe you want one of the following?

    String

こう。

     world = {str = "World??"}
     world = {world | str2 = "World!!"}

とすると

Naming multiple values `world` in a single let-expression makes
things ambiguous. When you say `world` which one do you want?

11|      world = {world | str2 = "World!!"}
         ^^^^^
Find all the values named `world` in this let-expression and
do some renaming. Make sure the names are distinct!

こう。完璧ではないか。

乱数シードに現在時刻が欲しい

これはエラーメッセージとは関係ないのだが、乱数の初期化のために現在時刻が欲しかったのだが、その方法が良く分からなかった。

Elmの中だけでなんとかしようと思わないでportを使ってJavaScriptから供給すればいいらいいぞ。

portはElmとJavaScriptの間で値をやりとりする仕組みで、JavaScriptからsendされた値をElmのSignalとして受け取ることができる。

Elm.fullscreen(Elm.Test, {beginTime: Date.now()});

JavaScriptからElmを起動する時に引数で与えた値を、

port beginTime: Float

main =
  let
     world = {str = toString beginTime}

とportを介してもらえば良い。portってSignalじゃなくてもいいのね。たぶん不変な値限定だけど。

requestAnimationFrame対応

ElmのPongの例とかだと画面の更新はSignal.map inSeconds (fps 35)とか使っていてタイマーベースだけど、できればこれはrequestAnimationFrameにしたい。

それにはelm-animation-frameを使えば良い。elm package install jwmerrill/elm-animation-frameでパッケージをインストールし、

main =
  let
     frameNumSignal = Signal.map toString 
       (Signal.foldp (+) beginTime AnimationFrame.frame)
     messageSignal = Signal.map (message2 "World?!") frameNumSignal
  in
    Signal.map text messageSignal

のようにAnimationFrame.frameをトリガに画面を更新すれば良いらしいぞ。

Elmイケているではないか

バージョンアップで着実に欠点が埋められている感じ。portを使ったJavaScriptとの連携がうまく使えれば、ゲーム作りにもいいかも。あとは良いIDEが欲しいよな。

物理エンジンMatter.jsをテキストでレンダリング

CRTっぽいテキスト画面をWebGLで作るLocatePrintを使って物理エンジンをレンダリングするというデモも作った。

consolephysics

物理エンジンMatter.jsを使っている。Matter.jsはもちろん自前のレンダラを持っているんだけどそれを他のレンダラで差し替えられる

差し替える独自レンダラはMatter.jsのRender.jsを参考に作れば良い。特にRender.bodyWireframes関数にボディの頂点を線でつなぐ一番単純な描画方法が実装されているので、これを真似るのが簡単だ。

デモのコードでは、頂点を線でつなぐ部分でテキストを書くようにした。画面をテキストの幅と高さ(40キャラ x 20キャラ)のグリッドに分割し、線とグリッドが交わるポイントをリストアップする。グリッド内の各キャラについて、それらポイントがどこにあるかに応じて書くテキストを変える。例えば上端右と下端左にポイントがあれば/、右端上と下端左にあればFという具合に。それらのパターンを全部配列で記述した。

後はボディごとに設定された色に従ってcolor, locate, printするだけ。昔ながらの画面で近代的な物理エンジンが動く妙な絵が作れる。楽しい。

昔のCRTっぽい画面をWebGLで作る

マイコンをつないだテレビみたいな画面を作りたいと思ってLocatePrintっていうコードを書いた。

skigame

こんな画面が作れる、し遊べる。locate, print, colorなどの命令でテキストを書けます。というかテキストしか書けない。

こういうブラウン管っぽい画面を作るにはWebGLをポストプロセスとして使うのが便利。

この記事に必要なことはほとんど書いてある。glfx.jsっていうWebGLのイメージエフェクトライブラリを使って、ブラウン管の丸みっぽく画面を歪ませ (bulgePinch)、ふちをちょっと暗くすれば (vignette)、昔懐かしのテレビっぽい絵になる。

ただこの記事だとスキャンラインはPNGイメージを重ねることで実現しているけど、これもできればWebGLのシェーダーでやりたい。あとパピコンAppleIIみたいな色のにじみもつけたい。

そういう時はglfx.jsにカスタムのエフェクトが追加できればいいんだけど、残念ながらglfx.js自体はそのような仕組みを用意してない、が、やればできないことはない。

glfx.jsのいくつかの関数を無理やりexportsして外部から使えるようにする。そうすればスキャンライン色のにじみを再現するシェーダーをglfx.js内で使うことができる。

WebGL使えばブラウザ上でも簡単にポストエフェクトで遊べて良いね。フラットなLCDを丸いCRTにするという後ろ向きな使い方でも楽しければ良いのだ。

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に置いた

JS1KデモBLCK4777のせめて音を作ってる部分だけでも分かりたい

今年のAssembly 2015 1kb intro勝者のJavaScriptデモBLCK4777、わずか1023 bytesでできているとは思えない美しいデモで話題になった。

1023 bytes版のファイルはPNG bootstrapping techniqueで圧縮されているそうで見た目はバイナリである。見ても分からない。

Full archiveに入っているsafe版は非圧縮のものなので、ちゃんと見えるJavaScriptが書いてある。さらにそれを見やすく整形して置いてくれた人がいるので、それを見ると、

はい分からない。

いや、もうちょっと頑張って読もう。safe版には、上のgistに置いてあるコードの他にブートストラップ用の以下のコードが含まれている。

c = b.getContext('2d');
T = String.fromCharCode;
p = 0;
document.querySelector('button').onclick = function() {
  this.textContent = 'Starting...';
  this.disabled = true;
  setTimeout(u,1);
};

ここで変数pは0に初期化されている。function uの最初の方にあるg = p ?から始まる三項演算子は0がfalse相当なので後ろの式が採用される。後ろの式はまるで関数の引数のようなものが書いてあるが、これはコンマ演算子だ。順に実行され、最後の結果が採用される。三項演算子コンマ演算子を使えば、if文相当を短く書ける。

ちなみにJavaScriptのショートコーディングについて知りたければSuperpacking JS Demosなどの記事を読むと良い。WebGL用の長いメソッド正規表現で無理やり短くする方法とか、PNGを使ったコードの圧縮方法などが書いてある。

コードに戻ると、pに値を設定するのは一番下のAudio.playの引数部分だ。Audio.playの引数は何の効用もないはずなので単にここで値を代入しているだけ。なので、ここでpの値が設定され、requestAnimationFrame(u)で再度uが呼び出されるまでにAudio、つまりこのデモの音が生成されている。

デモ全体を把握するのはキツイので、このAudioを作るまでをせめて見てみたい。pが0である条件で通るパスだけを切り出すと、以下のコードになる。

T = String.fromCharCode;
function u() {
  audio = "RIFFdataWAVEfmt " + atob("EAAAAAEAAQAAeAAAAHgAAAEACAA") + "data";
  g = 6177;
  h = f = C = 0;
  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;
    }
    audio += T((1 + s * 8) * Math.random() + 
               (1 - s) * (h / 45 * (f * (2 + C / 3 % 1) & 1) + 
               (C > 3) * 8 * (f * (2 + (f / 8 & 3)) % 1)) | 1);
    f += 1 / 512;
  }
  new Audio("data:Audio/WAV;base64," + btoa(audio)).play();
}

はい分からない。

いや言いたいことは分かる。最初のaudioの初期化ではwavファイルのヘッダ相当のものを作っている。atobbase64をデコードしているのはフォーマットやチャネル数を指定するためのデータで、これはリニアPCM、モノラル、サンプリングレート30720Hz、bit/sampleは8bitを示している。

なのでaudioにTString.fromCharCodeを使って数値を足しているところで8bitのサンプルごとのデータを生成している。最後のnew Audioで作ったデータをbtoabase64エンコードし音を作っているのだ。

じゃあTの後ろの面妖な式は何だ。何だ。分からん。

ちくしょう!可視化だ!

データを適当に間引いた上でc3.js使ってグラフ化してみたけど……分からん。fと比較している配列内のフレーム数のところでなんらかの変化が起きているから、ここで曲の内容を変化させているのだとは思う。変化はCをインクリメントし、hを640にすることで起こしている。式にはC > 3という部分もあるので、ある程度フレームが進んだ後のみ使われる部分もあるのだろう。hは少しずつ減衰しているように見える。

これはいわゆるbytebeatと似たような手法なのだと思う。bytebeatは単純な式から様々な音を生成することができる手法で、実装方法もごく単純にフレーム数を引数とした式の出力をリニアPCMのサンプルごとの値にするだけというもの。本当に単純なのでショートコーディングにはとても向いた方法なのだが、問題はどんな式がどんな音をだすのかさっぱり分からないこと。

それっぽい式を適当に組わせるUIがあれば、簡単に音が作れるんじゃないかと思って、前にbytebeatbank (これも音が出ます)ってのを作ったんだけど、やはり型にはまった式の組み合わせだけだとあまりバリエーションが出ないという問題があった。かといって自由な式を生成してしまうとまともな音が鳴ることはほぼ無いという別の問題が起こる。

bytebeatに限らず、こういった数式でそれっぽい音を簡単に作る方式があれば、短いコードでBGMなり効果音なりが作れて楽しいんだろうけど、そういう手法を解説しているページとか、欲しいところだ。bytebeatについては以下のページがたぶん役に立つ、んだろうけどこれを読み解くのも大変そうである。

ちなみに今回まるっとスキップした、絵を作る部分はこれにさらに輪をかけて謎の数式が乱舞しているので誰か解説して欲しい。BLCK4777の例では無いけど、短い数式や関数で驚くべきグラフィックスを作る方法については、以下のページなどが役に立つ、はず。

というわけでデモコーダーの超絶技法を知るのはとても大変そうであるという話でした。

New 3DSブラウザのゲーム開発向けJavaScript API対応具合チェック

Newニンテンドー3DS インターネットブラウザーの主な仕様を見ると

HTML4.01/HTML5/XHTML1.1/Fullscreen/Gamepad/SVG/WebSocket/Video Subtitle/
WOFF/Web Messaging/Server-Sent/Web Storageの一部/XMLHttpRequest/
canvas/Video/DOM1-3/ECMAScript/CSS1/CSS2.1/CSS3 の一部

のウェブ標準に対応していると書いてある。旧3DSの仕様と比べるとだいぶリッチに見える。特にFullscreenとGamepadに対応しているならゲーム開発向けに使えるんじゃないのかね、と思って少し調べた。

Fullscreen

これ本当に対応しているの?

  • document.documentElement.webkitRequestFullScreen():何も起きない
  • document.documentElement.webkitRequestFullscreen():何も起きない
  • document.documentElement.requestFullscreen():多分関数が無い
  • document.documentElement.mozRequestFullScreen():多分関数が無い

もちろんPCのChromeだとちゃんとフルスクリーンになるんだけど、New 3DS上では何も起きない。何か別の呼び出し方が必要なのか。そもそもNetFront Browserのベンダープレフィックスは何だ。

Gamepad

navigator.getGamepads()を使う標準的な方法で確かにパッドの状態をとれる。けど、デジタルパッドとAボタン以外はブラウザ自体の操作に割り当てられているので使えない。アナログパッドや他のボタン全滅。

いろいろと勘違いがあった、下記追記参照。

その他

CanvasはfillRectとputImageDataしか試してないけどちゃんと動く。JavaScript自体はあんまり速い印象はない。128 x 128ドットのピクセル操作とかをやらせるとてきめんに遅くなる。

当たり前だがデベロッパツールとかコンソールとかは無い。

これらをふまえると

フルスクリーンもゲームパッドもパフォーマンスもイマイチなので、これでアクションゲーム開発はちょっとキツイ。フルスクリーンが効いて、その時は全てのパッド入力が取れる、という仕様が理想だったのだが。タッチペン使うのならもうちょっといけるのかもしれん。

Gamepad追記

@tkoharaさんに教えてもらって、htmlに

<meta name="viewport" content="width=device-width, height=device-height, 
user-scalable=no, initial-scale=1, maximum-scale=1">

を追記してアナログパッドなどでの画面の拡縮、移動操作を禁止すれば、パッド操作も問題なくできることがわかった。

あとテストプログラムでキー入力で取れていたものとゲームパッドで取れていたものを混同していて、いろいろ間違っていた。

  • ゲームパッドを取得するのはwebkitベンダープレフィックス付きのnavigator.webkitGetGamepads()
  • gamepad.buttonsは押し込みの値が直接取れる古いインタフェース(pressedやvalueは無い)
  • 左のアナログパッドがaxesの0, 1、右が2, 3
  • デジタルパッドはボタンとして取れる
  • L, RボタンやBボタンは進む、戻るのナビゲーションに割り当てられているので、ゲームに使えるかどうかは画面遷移に依存。基本使えない

なのでアナログパッドとデジタルパッド、A, X, Yボタンは使えそうなので、頑張ればなんとかなる、か?