Studi kasus - JAM dengan Chrome

Cara kami membuat UI menjadi batu

Pengantar

JAM dengan Chrome adalah proyek musik berbasis web yang dibuat oleh Google. JAM dengan Chrome memungkinkan orang dari seluruh dunia membentuk band dan JAM secara real time di dalam browser. DinahMoe menerapkan Web Audio API di Chrome, dan tim kami di Tool of North America membuat antarmuka untuk memetik musik, bermain drum, dan bermain komputer seolah-olah komputer tersebut adalah alat musik.

Dengan arahan kreatif dari Google Creative Lab, ilustrator Rob Bailey membuat ilustrasi rumit untuk masing-masing dari 19 instrumen yang tersedia untuk JAM. Berdasarkan hal itu, Direktur Interaktif Ben Tricklebank dan tim desain kami di Tool membuat antarmuka yang mudah dan profesional untuk setiap instrumen.

Montase selai lengkap

Karena setiap instrumen memiliki keunikan secara visual, saya dan Direktur Teknis Tool, Bartek Drozdz, dan saya merangkainya menggunakan kombinasi gambar PNG, CSS, SVG, dan elemen Canvas.

Banyak instrumen tersebut harus menangani metode interaksi yang berbeda (seperti klik, tarik, dan pemukul - semua hal yang ingin Anda lakukan dengan alat musik) sekaligus menjaga antarmuka dengan mesin suara DinahMoe tetap sama. Kami menemukan bahwa kami membutuhkan lebih dari sekadar pergerakan mouse dan mouse JavaScript agar dapat memberikan pengalaman bermain yang indah.

Untuk menangani semua variasi ini, kami membuat elemen “Stage” yang mencakup area bermain, menangani klik, tarik, dan strum di semua instrumen yang berbeda.

Panggung

Stage adalah pengontrol yang digunakan untuk menyiapkan fungsi pada instrumen. Seperti menambahkan berbagai bagian instrumen yang akan berinteraksi dengan pengguna. Setelah menambahkan lebih banyak interaksi (seperti "hit"), kita dapat menambahkannya ke prototipe Stage.

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
};

Mendapatkan elemen dan posisi mouse

Tugas pertama kita adalah menerjemahkan koordinat mouse di jendela browser agar relatif terhadap elemen Stage. Untuk melakukannya, kita perlu memperhitungkan di mana Stage berada di halaman.

Karena kita perlu menemukan di mana elemen tersebut relatif terhadap seluruh jendela, bukan hanya elemen induknya, ini sedikit lebih rumit daripada hanya melihat elemen offsetTop dan offsetLeft. Opsi termudah adalah menggunakan getBoundingClientRect, yang memberikan posisi relatif terhadap jendela, seperti peristiwa mouse dan didukung di browser yang lebih baru dengan baik.

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};
  }
};

Jika getBoundingClientRect tidak ada, kami memiliki fungsi sederhana yang hanya akan menambahkan offset, memindahkan rantai induk elemen hingga mencapai isi. Kemudian, kurangi seberapa jauh jendela di-scroll untuk mendapatkan posisi relatif terhadap jendela. Jika Anda menggunakan jQuery, fungsi offset() melakukan pekerjaan yang baik dalam menangani kompleksitas untuk mencari tahu lokasi di seluruh platform, tetapi Anda masih perlu mengurangi jumlah yang di-scroll.

Setiap kali halaman di-scroll atau diubah ukurannya, mungkin posisi elemen telah berubah. Kita dapat memproses peristiwa ini dan memeriksa posisinya lagi. Peristiwa ini diaktifkan berkali-kali pada scroll atau ukuran yang biasa, sehingga dalam aplikasi yang sebenarnya, sebaiknya batasi seberapa sering Anda memeriksa ulang posisinya. Ada banyak cara untuk melakukannya, tetapi HTML5 Rocks memiliki artikel untuk debouncing peristiwa scroll menggunakan requestAnimationFrame yang akan berfungsi dengan baik di sini.

Sebelum kita menangani deteksi klik, contoh pertama ini hanya akan menghasilkan output relatif x dan y setiap kali mouse dipindahkan di area Stage.

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);
};

Untuk mulai melihat gerakan mouse, kita akan membuat objek Stage baru dan meneruskan ID div yang ingin digunakan sebagai Stage.

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

Deteksi hit sederhana

Pada JAM dengan Chrome, tidak semua antarmuka instrumen yang rumit. Bantalan mesin drum kami berbentuk persegi panjang sederhana, sehingga mudah untuk mendeteksi jika sebuah klik berada dalam batasnya.

Mesin drum

Dimulai dengan persegi panjang, kita akan menyiapkan beberapa jenis bentuk dasar. Setiap objek bentuk perlu mengetahui batas-batasnya dan memiliki kemampuan untuk memeriksa apakah sebuah titik berada di dalamnya.

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;
};

Setiap jenis bentuk baru yang kita tambahkan akan membutuhkan fungsi dalam objek Stage untuk mendaftarkannya sebagai zona klik.

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;
};

Pada peristiwa mouse, setiap instance bentuk akan menangani pemeriksaan apakah mouse x dan y yang diteruskan adalah hit untuknya dan menampilkan true atau false.

Kita juga dapat menambahkan class "active" ke elemen stage yang akan mengubah kursor mouse menjadi pointer saat bergulir di atas persegi.

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);

Bentuk lainnya

Karena bentuk menjadi lebih rumit, perhitungan untuk mengetahui apakah suatu titik ada di dalamnya akan menjadi lebih kompleks. Namun, persamaan ini telah diuraikan dengan baik dan didokumentasikan dengan sangat mendetail di banyak tempat secara online. Beberapa contoh JavaScript terbaik yang pernah saya lihat adalah dari library geometri Kevin Lindsey.

Untungnya, dalam membangun JAM dengan Chrome, kami tidak perlu melampaui lingkaran dan persegi panjang, dengan mengandalkan kombinasi bentuk dan pelapisan untuk menangani kerumitan tambahan.

Bentuk drum

Lingkaran

Untuk memeriksa apakah sebuah titik berada dalam drum lingkaran, kita perlu membuat bentuk dasar lingkaran. Meskipun sangat mirip dengan persegi panjang, ikon ini akan memiliki metodenya sendiri untuk menentukan batas-batas dan memeriksa apakah titiknya berada di dalam lingkaran.

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;
};

Alih-alih mengubah warna, menambahkan class hit akan memicu animasi CSS3. Ukuran latar belakang memberi cara yang baik untuk menskalakan gambar drum dengan cepat, tanpa memengaruhi posisinya. Anda perlu menambahkan awalan browser lain untuk pekerjaan ini dengan browser tersebut (-moz, -o, dan -ms) dan mungkin juga ingin menambahkan versi tanpa awalan.

#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%;}
}

String

Fungsi GuitarString kita akan mengambil ID kanvas dan objek Rect dan menggambar garis di tengah persegi panjang tersebut.

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

Ketika ingin membuatnya bergetar, kita akan memanggil fungsi strum untuk mengatur string yang bergerak. Setiap bingkai yang kita render akan sedikit mengurangi gaya yang dipukulnya dan meningkatkan penghitung yang akan menyebabkan string bergerak bolak-balik.

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;
};

Persimpangan dan Permainan Strum

Area hit untuk string ini akan menjadi kotak lagi. Mengklik dalam kotak tersebut akan memicu animasi string. Tapi, siapa yang mau klik gitar?

Untuk menambahkan strumming, kita perlu memeriksa persimpangan kotak {i>string<i} dan garis yang dilalui {i>mouse<i} pengguna.

Untuk mendapatkan jarak yang cukup antara posisi mouse sebelumnya dan saat ini, kita perlu memperlambat kecepatan saat mendapatkan peristiwa gerakan mouse. Untuk contoh ini, kita hanya akan menetapkan flag untuk mengabaikan peristiwa mousemove selama 50 milidetik.

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);
};

Selanjutnya kita perlu mengandalkan beberapa kode persimpangan yang ditulis Kevin Lindsey untuk melihat apakah garis gerakan mouse berpotongan di tengah persegi panjang.

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;
};

Terakhir, kita akan menambahkan Fungsi baru untuk membuat Instrumen String. Ini akan membuat Stage baru, mengatur sejumlah {i>string<i} dan mendapatkan konteks Canvas yang akan digambar.

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;
}

Selanjutnya kita akan memosisikan area hit string, lalu menambahkannya ke elemen Stage.

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);
  }
};

Terakhir, fungsi render StringInstrument akan melakukan loop pada semua string dan memanggil metode rendernya. Aplikasi ini berjalan sepanjang waktu, secepat requestAnimationFrame yang sesuai. Anda dapat membaca lebih lanjut tentang requestAnimationFrame dalam artikel Paul Irish, requestAnimationFrame for smart animating.

Dalam aplikasi yang sebenarnya, Anda mungkin ingin menetapkan flag saat tidak ada animasi yang terjadi untuk berhenti menggambar bingkai kanvas baru.

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);
  }
};

Penutup

Memiliki elemen Stage yang sama untuk menangani semua interaksi kita bukan tanpa kekurangannya. Komputasinya lebih kompleks, dan peristiwa pointer kursor dibatasi tanpa menambahkan kode tambahan untuk mengubahnya. Namun, untuk JAM dengan Chrome, manfaat untuk memisahkan peristiwa mouse dari setiap elemen bekerja dengan sangat baik. Ini memungkinkan kita lebih banyak bereksperimen dengan desain antarmuka, beralih di antara metode elemen animasi, menggunakan SVG untuk mengganti gambar bentuk dasar, mudah menonaktifkan area klik, dan banyak lagi.

Untuk melihat aksi Drum and Stings, mulai JAM Anda sendiri dan pilih Standard Drums atau classic Clean Electric Guitar.

Logo Jam