Studi kasus - JAM dengan Chrome

Cara kami membuat UI menjadi keren

Pengantar

JAM with Chrome adalah project musik berbasis web yang dibuat oleh Google. JAM dengan Chrome memungkinkan orang-orang dari seluruh dunia membentuk band dan JAM secara real time di dalam browser. DinahMoe mendorong batas kemampuan Web Audio API Chrome, tim kami di Tool of North America membuat antarmuka untuk memetik, menabuh drum, dan memainkan komputer Anda seolah-olah itu adalah alat musik.

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

Montase jam penuh

Karena setiap instrumen secara visual unik, saya dan Direktur Teknis Tool, Bartek Drozdz, menggabungkannya menggunakan kombinasi gambar PNG, CSS, SVG, dan elemen Kanvas.

Banyak instrumen yang harus menangani berbagai metode interaksi (seperti klik, tarik, dan petik - semua hal yang Anda harapkan untuk dilakukan dengan instrumen) sambil mempertahankan antarmuka dengan mesin suara DinahMoe yang sama. Kami mendapati bahwa kita memerlukan lebih dari sekadar mouseup dan mousedown JavaScript untuk dapat memberikan pengalaman bermain yang menarik.

Untuk menangani semua variasi ini, kami membuat elemen “Panggung” yang mencakup area yang dapat dimainkan, menangani klik, tarik, dan petikan di semua instrumen yang berbeda.

Panggung

Stage adalah pengontrol yang kita gunakan untuk menyiapkan fungsi di seluruh instrumen. Misalnya, menambahkan berbagai bagian instrumen yang akan berinteraksi dengan pengguna. Saat menambahkan lebih banyak interaksi (seperti "hit"), kita dapat menambahkannya ke prototipe Panggung.

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 mempertimbangkan posisi Stage di halaman.

Karena kita perlu menemukan posisi elemen relatif terhadap seluruh jendela, bukan hanya elemen induknya, hal 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 dengan baik di browser yang lebih baru.

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, kita memiliki fungsi sederhana yang hanya akan menambahkan offset, yang memindahkan rantai induk elemen ke atas hingga mencapai isi. Kemudian, kita mengurangi seberapa jauh jendela telah di-scroll untuk mendapatkan posisi yang relatif terhadap jendela. Jika Anda menggunakan jQuery, fungsi offset() akan menangani kompleksitas untuk mengetahui lokasi di seluruh platform dengan baik, tetapi Anda tetap harus mengurangi jumlah yang di-scroll.

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

Sebelum kita menangani deteksi hit, contoh pertama ini hanya akan menghasilkan output x dan y relatif 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 mengamati gerakan mouse, kita akan membuat objek Stage baru dan meneruskan ID div yang ingin kita gunakan sebagai Stage.

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

Deteksi hit sederhana

Di JAM dengan Chrome, tidak semua antarmuka instrumen bersifat kompleks. Pad mesin drum kita hanyalah persegi panjang sederhana, sehingga memudahkan untuk mendeteksi apakah klik berada dalam batasnya.

Drum machine

Mulai dari persegi panjang, kita akan menyiapkan beberapa jenis bentuk dasar. Setiap objek bentuk perlu mengetahui batasnya dan memiliki kemampuan untuk memeriksa apakah 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 memerlukan fungsi dalam objek Stage untuk mendaftarkannya sebagai zona hit.

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 x dan y mouse yang diteruskan merupakan hit untuknya dan menampilkan true atau false.

Kita juga dapat menambahkan class "active" ke elemen panggung yang akan mengubah kursor mouse menjadi pointer saat mengarahkan kursor ke kotak.

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

Seiring dengan semakin rumitnya bentuk, matematika untuk menemukan apakah titik berada di dalamnya menjadi lebih rumit. Namun, persamaan ini telah ditetapkan dan didokumentasikan secara mendetail di banyak tempat secara online. Beberapa contoh JavaScript terbaik yang pernah saya lihat berasal dari library geometri Kevin Lindsey.

Untungnya, dalam mem-build JAM dengan Chrome, kita tidak perlu menggunakan bentuk selain lingkaran dan persegi panjang, dengan mengandalkan kombinasi bentuk dan lapisan untuk menangani kompleksitas tambahan.

Bentuk drum

Lingkaran

Untuk memeriksa apakah titik berada dalam drum lingkaran, kita harus membuat bentuk dasar lingkaran. Meskipun cukup mirip dengan persegi panjang, lingkaran akan memiliki metodenya sendiri untuk menentukan batas dan memeriksa apakah titik 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;
};

Daripada mengubah warna, menambahkan class hit akan memicu animasi CSS3. Ukuran latar belakang memberi kita cara yang bagus untuk menskalakan gambar drum dengan cepat, tanpa memengaruhi posisinya. Anda harus menambahkan awalan browser lain agar dapat berfungsi dengan browser tersebut (-moz, -o, dan -ms) dan sebaiknya tambahkan juga 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, lalu menggambar garis di bagian 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;
}

Jika ingin membuatnya bergetar, kita akan memanggil fungsi petikan untuk menggerakkan string. Setiap frame yang kita render akan sedikit mengurangi gaya yang digunakan untuk memetik dan meningkatkan penghitung yang akan menyebabkan string berosilasi maju mundur.

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 Petikan

Area hit untuk string ini akan menjadi kotak lagi. Mengklik dalam kotak tersebut akan memicu animasi string. Namun, siapa yang ingin mengklik gitar?

Untuk menambahkan petikan, kita perlu memeriksa persimpangan kotak string dan garis yang dilalui mouse pengguna.

Untuk mendapatkan jarak yang cukup antara posisi mouse sebelumnya dan saat ini, kita harus memperlambat kecepatan saat kita mendapatkan peristiwa gerakan mouse. Untuk contoh ini, kita cukup menetapkan tanda 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 harus mengandalkan beberapa kode persimpangan yang ditulis Kevin Lindsey untuk melihat apakah garis gerakan mouse bersimpangan dengan bagian tengah persegi panjang kita.

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. Tindakan ini akan membuat Stage baru, menyiapkan sejumlah string, dan mendapatkan konteks Kanvas 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 melalui semua string dan memanggil metode rendernya. Fungsi ini berjalan sepanjang waktu, secepat yang diperlukan requestAnimationFrame. Anda dapat membaca selengkapnya tentang requestAnimationFrame di artikel Paul Irish requestAnimationFrame untuk animasi cerdas.

Dalam aplikasi yang sebenarnya, Anda mungkin ingin menetapkan tanda saat tidak ada animasi yang terjadi untuk berhenti menggambar frame 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);
  }
};

Rangkuman

Memiliki elemen Stage umum untuk menangani semua interaksi kita tidak tanpa kekurangan. Secara komputasi, peristiwa ini lebih kompleks, dan peristiwa pointer kursor terbatas tanpa menambahkan kode tambahan untuk mengubahnya. Namun, untuk JAM dengan Chrome, manfaat dari kemampuan untuk memisahkan peristiwa mouse dari setiap elemen berfungsi dengan sangat baik. Hal ini memungkinkan kita bereksperimen lebih lanjut dengan desain antarmuka, beralih di antara metode animasi elemen, menggunakan SVG untuk mengganti gambar bentuk dasar, menonaktifkan area hit dengan mudah, dan banyak lagi.

Untuk melihat cara kerja Drum dan Sting, mulai JAM Anda sendiri dan pilih Drum Standar atau Gitar Listrik Bersih Klasik.

Logo Jam