事例紹介 - Chrome での JAM

UI の完成度

はじめに

JAM with Chrome は、Google が制作したウェブベースの音楽プロジェクトです。JAM with Chrome を使えば、世界中の人々がブラウザ内でリアルタイムでバンドと JAM を結成できます。DinahMoe は、Chrome の Web Audio API の限界を押し広げました。Tool of North America のチームは、パソコンを楽器のようにストラム、ドラム、演奏するためのインターフェースを作成しました。

イラストレーターの Rob Bailey は、Google Creative Lab のクリエイティブなディレクションを活用し、JAM で利用できる 19 種類の楽器それぞれについて、複雑なイラストを作成しました。そこで、インタラクティブ ディレクターの Ben Tricklebank と Google のデザイン チームは Tool の設計チームとともに、それぞれの楽器のプロ向けの簡単で使いやすいインターフェースを作成しました。

本格的なジャム モンタージュ

各楽器は視覚的にユニークなため、ツールのテクニカル ディレクターである Bartek Drozdz と私は、PNG 画像、CSS、SVG、Canvas 要素を組み合わせて、それらをつなぎ合わせました。

多くの楽器は、DinahMoe のサウンド エンジンとのインターフェースを同じに保ちながら、異なる操作方法(クリック、ドラッグ、ストラムなど、楽器に想定されるすべてのこと)を処理する必要がありました。私たちは、美しいプレイ体験を提供するためには、JavaScript のマウスアップとマウスダウンだけでは不十分であることがわかりました。

こうしたすべてのバリエーションに対応するため、プレイアブル エリアを覆う「ステージ」要素を作成し、さまざまな楽器のクリック、ドラッグ、ストラムを処理できるようにしました。

ステージ

ステージは、楽器全体で機能を設定するために使用するコントローラです。たとえば、ユーザーが操作する楽器のさまざまな部分の追加などです。さらにインタラクション(「ヒット」など)を追加すると、それらをステージのプロトタイプに追加できます。

function Stage(el) {

  // Grab the elements from the dom
  this.el = document.getElementById(el);
  this.elOutput = document.getElementById("output-1");

  // Find the position of the stage element
  this.position();

  // Listen for events
  this.listeners();

  return this;
}

Stage.prototype.position = function() {
  // Get the position
};

Stage.prototype.offset = function() {
  // Get the offset of the element in the window
};

Stage.prototype.listeners = function() {
  // Listen for Resizes or Scrolling
  // Listen for Mouse events
};

要素とマウス位置を取得する

最初のタスクは、ブラウザ ウィンドウ内のマウス座標を Stage 要素に相対的に変換することです。そのためには、ページ内のステージの位置を考慮する必要がありました。

親要素だけでなくウィンドウ全体に対する要素の相対位置を見つける必要があるため、要素 offsetTop と offsetLeft を確認するよりも少し複雑になります。最も簡単な方法は、マウスイベントと同様に、ウィンドウに対する相対位置を示す getBoundingClientRect を使用することです。これは新しいブラウザで適切にサポートされています。

Stage.prototype.offset = function() {
  var _x, _y,
      el = this.el;

  // Check to see if bouding is available
  if (typeof el.getBoundingClientRect !== "undefined") {

    return el.getBoundingClientRect();

  } else {
    _x = 0;
    _y = 0;

    // Go up the chain of parents of the element
    // and add their offsets to the offset of our Stage element

    while (el && !isNaN( el.offsetLeft ) && !isNaN( el.offsetTop ) ) {
      _x += el.offsetLeft;
      _y += el.offsetTop;
      el = el.offsetParent;
    }

    // Subtract any scrolling movment
    return {top: _y - window.scrollY, left: _x - window.scrollX};
  }
};

getBoundingClientRect が存在しない場合は、オフセットを加算する単純な関数を使用して、要素の親のチェーンを本体に到達するまで上に移動します。次に、ウィンドウがスクロールされた距離を差し引いて、ウィンドウに対する相対的な位置を取得します。jQuery を使用している場合、offset() 関数はプラットフォームをまたいで場所を特定するという複雑な処理には優れていますが、スクロールした量を差し引く必要があります。

ページがスクロールされたりサイズ変更されたりすると、要素の位置が変わる可能性があります。これらのイベントをリッスンし、位置を再度確認できます。これらのイベントは一般的なスクロールやサイズ変更で何度も発生するため、実際のアプリケーションでは、位置を再確認する頻度を制限することをおすすめします。これを行う方法は多数ありますが、HTML5 Rocks で requestAnimationFrame を使用してスクロール イベントをデバウンドする方法を紹介しています。

この最初の例では、ヒット検出を処理する前に、ステージ領域内でマウスを動かすたびに相対的な x と y を出力します。

Stage.prototype.listeners = function() {
  var output = document.getElementById("output");

  this.el.addEventListener('mousemove', function(e) {
      // Subtract the elements position from the mouse event's x and y
      var x = e.clientX - _self.positionLeft,
          y = e.clientY - _self.positionTop;

      // Print out the coordinates
      output.innerHTML = (x + "," + y);

  }, false);
};

マウスの動きを監視するために、新しい Stage オブジェクトを作成し、それに Stage として使用する div の ID を渡します。

//-- Create a new Stage object, for a div with id of "stage"
var stage = new Stage("stage");

シンプルなヒット検出

Chrome の JAM では、すべての計測インターフェースが複雑というわけではありません。当社のドラムマシンのパッドは単純な長方形なので、クリックが境界内に入ったかどうかを簡単に検出できます。

ドラムマシン

まず、長方形から、基本のシェイプをいくつかセットアップします。各シェイプ オブジェクトは、その境界を認識し、ポイントが範囲内にあるかどうかを確認する機能を備えている必要があります。

function Rect(x, y, width, height) {
  this.x = x;
  this.y = y;
  this.width = width;
  this.height = height;
  return this;
}

Rect.prototype.inside = function(x, y) {
  return x >= this.x && y >= this.y
      && x <= this.x + this.width
      && y <= this.y + this.height;
};

追加する新しいシェイプタイプごとに、ステージ オブジェクト内に関数を作成して、ヒットゾーンとして登録する必要があります。

Stage.prototype.addRect = function(id) {
  var el = document.getElementById(id),
      rect = new Rect(
        el.offsetLeft,
        el.offsetTop,
        el.offsetWidth,
        el.offsetHeight
      );

  rect.el = el;

  this.hitZones.push(rect);
  return rect;
};

マウス イベントが発生すると、各シェイプ インスタンスは、渡されたマウス x と y がヒットしているかどうかをチェックし、true または false を返します。

また、ステージ要素に「active」クラスを追加し、正方形をロールオーバーしたときにマウスカーソルをポインタに変えることもできます。

this.el.addEventListener ('mousemove', function(e) {
  var x = e.clientX - _self.positionLeft,
      y = e.clientY - _self.positionTop;

  _self.hitZones.forEach (function(zone){
    if (zone.inside(x, y)) {
      // Add class to change colors
      zone.el.classList.add('hit');
      // change cursor to pointer
      this.el.classList.add('active');
    } else {
      zone.el.classList.remove('hit');
      this.el.classList.remove('active');
    }
  });

}, false);

その他の図形

図形が複雑になるにつれ、その内部に点があるかどうかを確認する計算はより複雑になります。しかし、これらの方程式は十分に確立され、オンラインの多くの場所で詳細に文書化されています。JavaScript の優れた例は、Kevin Lindsey のジオメトリ ライブラリです。

幸いなことに、Chrome で JAM を構築する際に、円や長方形に留まらず、図形とレイヤの組み合わせによって余分な複雑さに対処する必要はありませんでした。

ドラムの形状

点が円形ドラム内にあるかどうかを確認するには、基本の形状を作成する必要があります。長方形とよく似ていますが、独自の方法で境界を決定したり、ポイントが円の内側にあるかどうかを確認したりできます。

function Circle(x, y, radius) {
  this.x = x;
  this.y = y;
  this.radius = radius;
  return this;
}

Circle.prototype.inside = function(x, y) {
  var dx = x - this.x,
      dy = y - this.y,
      r = this.radius;
  return dx * dx + dy * dy <= r * r;
};

ヒットクラスを追加すると、色を変更する代わりに CSS3 アニメーションがトリガーされます。背景のサイズによって、ドラムの画像の位置を変えずにすばやく拡大縮小できます。そのためには、他のブラウザのプレフィックス(-moz、-o、-ms)を追加する必要があります。プレフィックスの付いていないバージョンも追加することをおすすめします。

#snare.hit{
  { % mixin animation: drumHit .15s linear infinite; % }
}

@{ % mixin keyframes drumHit % } {
  0%   { background-size: 100%;}
  10%  { background-size: 95%; }
  30%  { background-size: 97%; }
  50%  { background-size: 100%;}
  60%  { background-size: 98%; }
  70%  { background-size: 100%;}
  80%  { background-size: 99%; }
  100% { background-size: 100%;}
}

文字列

GuitarString 関数は、キャンバス ID と Rect オブジェクトを受け取り、その長方形の中心に線を描画します。

function GuitarString(rect) {
  this.x = rect.x;
  this.y = rect.y + rect.height / 2;
  this.width = rect.width;
  this._strumForce = 0;
  this.a = 0;
}

振動させる場合は、strum 関数を呼び出して文字列の動きを設定します。フレームをレンダリングするたびに、かき回される力がわずかに減少し、文字列が前後に振動するカウンタが増加します。

GuitarString.prototype.strum = function() {
  this._strumForce = 5;
};

GuitarString.prototype.render = function(ctx, canvas) {
  ctx.strokeStyle = "#000000";
  ctx.lineWidth = 1;
  ctx.beginPath();
  ctx.moveTo(this.x, this.y);
  ctx.bezierCurveTo(
      this.x, this.y + Math.sin(this.a) * this._strumForce,
      this.x + this.width, this.y + Math.sin(this.a) * this._strumForce,
      this.x + this.width, this.y);
  ctx.stroke();

  this._strumForce *= 0.99;
  this.a += 0.5;
};

交差点とストラム

文字列のヒットエリアは再びボックスになります。ボックス内をクリックすると、文字列アニメーションがトリガーされます。でも、ギターは誰でしょうか?

ストラムを追加するには、文字列ボックスとユーザーのマウスが移動している線の交点を確認する必要があります。

マウスの以前の位置と現在の位置の間に十分な距離を取るには、マウスの移動イベントを取得する速度を遅くする必要があります。この例では、単にマウス移動イベントを 50 ミリ秒無視するフラグを設定します。

document.addEventListener('mousemove', function(e) {
  var x, y;

  if (!this.dragging || this.limit) return;

  this.limit = true;

  this.hitZones.forEach(function(zone) {
    this.checkIntercept(
      this.prev[0],
      this.prev[1],
      x,
      y,
      zone
    );
  });

  this.prev = [x, y];

  setInterval(function() {
    this.limit = false;
  }, 50);
};

次に、Kevin Lindsey が作成した交差コードを利用して、マウスの動きの線が長方形の中央と交差するかどうかを確認する必要があります。

Rect.prototype.intersectLine = function(a1, a2, b1, b2) {
  //-- http://www.kevlindev.com/gui/math/intersection/Intersection.js
  var result,
      ua_t = (b2.x - b1.x) * (a1.y - b1.y) - (b2.y - b1.y) * (a1.x - b1.x),
      ub_t = (a2.x - a1.x) * (a1.y - b1.y) - (a2.y - a1.y) * (a1.x - b1.x),
      u_b = (b2.y - b1.y) * (a2.x - a1.x) - (b2.x - b1.x) * (a2.y - a1.y);

  if (u_b != 0) {
    var ua = ua_t / u_b;
    var ub = ub_t / u_b;

    if (0 <= ua && ua <= 1 && 0 <= ub && ub <= 1) {
      result = true;
    } else {
      result = false; //-- No Intersection
    }
  } else {
    if (ua_t == 0 || ub_t == 0) {
      result = false; //-- Coincident
    } else {
      result = false; //-- Parallel
    }
  }

  return result;
};

最後に、文字列楽器を作成するための新しい関数を追加します。新しいステージを作成し、いくつかの文字列を設定して、描画されるキャンバスのコンテキストを取得します。

function StringInstrument(stageID, canvasID, stringNum){
  this.strings = [];
  this.canvas = document.getElementById(canvasID);
  this.stage = new Stage(stageID);
  this.ctx = this.canvas.getContext('2d');
  this.stringNum = stringNum;

  this.create();
  this.render();

  return this;
}

次に、文字列のヒットエリアを配置して、ステージ要素に追加します。

StringInstrument.prototype.create = function() {
  for (var i = 0; i < this.stringNum; i++) {
    var srect = new Rect(10, 90 + i * 15, 380, 5);
    var s = new GuitarString(srect);
    this.stage.addString(srect, s);
    this.strings.push(s);
  }
};

最後に、StringInstrument のレンダリング関数はすべての文字列をループ処理し、そのレンダリング メソッドを呼び出します。これは常に実行され、requestAnimationFrame が適切であると見なされるとすぐに実行されます。requestAnimationFrame について詳しくは、Paul Irish の記事 requestAnimationFrame for smart animating をご覧ください。

実際のアプリケーションでは、アニメーションが発生していないときにフラグを設定して、新しいキャンバス フレームの描画を停止することをおすすめします。

StringInstrument.prototype.render = function() {
  var _self = this;

  requestAnimFrame(function(){
    _self.render();
  });

  this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height);

  for (var i = 0; i < this.stringNum; i++) {
    this.strings[i].render(this.ctx);
  }
};

まとめ

すべてのインタラクションを処理する共通のステージ要素を使用することに、欠点がないわけではありません。計算がより複雑になり、カーソル ポインタ イベントを変更するコードを追加するだけで、カーソル ポインタ イベントが制限されます。しかし、Chrome の JAM では、マウスイベントを個々の要素から切り離すことができるという利点は非常にうまくいきました。インターフェース デザインのテスト、要素のアニメーション化方法の切り替え、SVG による基本シェイプの画像の置き換え、ヒットエリアの簡単な無効化などを行えるようになりました。

ドラムとスティングの動作を確認するには、独自の JAM を起動し、[Standard Drums] または [Classic Clean Electric Guitar] を選択します。

Jam ロゴ