Nghiên cứu điển hình – ỨNG TÁC với Chrome

Cách chúng tôi tạo ra giao diện người dùng tuyệt vời

Fred Chasen
Fred Chasen

Giới thiệu

JAM with Chrome là một dự án âm nhạc trên web do Google tạo ra. JAM with Chrome cho phép mọi người trên khắp thế giới thành lập ban nhạc và JAM theo thời gian thực bên trong trình duyệt. DinahMoe đã đẩy ranh giới của những gì có thể làm được bằng API Âm thanh trên web của Chrome, nhóm của chúng tôi tại Tool of North America đã tạo ra giao diện để gảy, đánh trống và chơi máy tính như thể đó là một nhạc cụ.

Với định hướng sáng tạo của Google Creative Lab, họa sĩ Rob Bailey đã tạo ra các hình minh hoạ phức tạp cho từng trong số 19 nhạc cụ có thể dùng để JAM. Dựa trên những thông tin đó, Giám đốc tương tác Ben Tricklebank và nhóm thiết kế của chúng tôi tại Tool đã tạo ra một giao diện dễ dùng và chuyên nghiệp cho từng nhạc cụ.

Bản tổng hợp đầy đủ về màn biểu diễn nhạc

Vì mỗi nhạc cụ đều có hình ảnh riêng biệt, nên Giám đốc kỹ thuật của Tool Bartek Drozdz và tôi đã kết hợp các nhạc cụ đó bằng cách kết hợp các phần tử hình ảnh PNG, CSS, SVG và Canvas.

Nhiều nhạc cụ phải xử lý nhiều phương thức tương tác (chẳng hạn như nhấp, kéo và gảy – tất cả những việc bạn muốn làm với một nhạc cụ) trong khi vẫn giữ nguyên giao diện với công cụ âm thanh của DinahMoe. Chúng tôi nhận thấy rằng chỉ cần mouseup và mousedown của JavaScript là chưa đủ để mang lại trải nghiệm chơi đẹp mắt.

Để xử lý tất cả các biến thể này, chúng tôi đã tạo một phần tử "Stage" (Sân khấu) bao phủ khu vực có thể chơi, xử lý các lượt nhấp, kéo và gảy trên tất cả các nhạc cụ.

Giai đoạn

Stage (Giai đoạn) là bộ điều khiển mà chúng ta sử dụng để thiết lập hàm trên một công cụ. Chẳng hạn như thêm các phần khác nhau của các công cụ mà người dùng sẽ tương tác. Khi thêm các lượt tương tác khác (chẳng hạn như "lượt nhấn"), chúng ta có thể thêm các lượt tương tác đó vào nguyên mẫu của 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
};

Lấy phần tử và vị trí chuột

Nhiệm vụ đầu tiên của chúng ta là dịch toạ độ con trỏ chuột trong cửa sổ trình duyệt tương ứng với phần tử Stage (Sân khấu). Để làm việc này, chúng ta cần xem xét vị trí của Stage trong trang.

Vì chúng ta cần tìm vị trí của phần tử so với toàn bộ cửa sổ, chứ không chỉ phần tử mẹ, nên việc này sẽ phức tạp hơn một chút so với việc chỉ xem các phần tử offsetTop và offsetLeft. Cách dễ nhất là sử dụng getBoundingClientRect. Phương thức này cung cấp vị trí tương ứng với cửa sổ, giống như các sự kiện chuột và được hỗ trợ tốt trong các trình duyệt mới hơn.

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

Nếu getBoundingClientRect không tồn tại, chúng ta có một hàm đơn giản chỉ cần cộng các độ dời, di chuyển lên chuỗi của phần tử mẹ cho đến khi đạt đến phần thân. Sau đó, chúng ta trừ đi khoảng cách cuộn cửa sổ để lấy vị trí tương ứng với cửa sổ. Nếu bạn đang sử dụng jQuery, hàm offset() sẽ giúp bạn xử lý tốt sự phức tạp của việc xác định vị trí trên các nền tảng, nhưng bạn vẫn cần trừ đi lượng đã cuộn.

Bất cứ khi nào trang được cuộn hoặc đổi kích thước, vị trí của phần tử có thể đã thay đổi. Chúng ta có thể theo dõi các sự kiện này và kiểm tra lại vị trí. Các sự kiện này được kích hoạt nhiều lần khi cuộn hoặc đổi kích thước thông thường, vì vậy, trong một ứng dụng thực tế, tốt nhất bạn nên giới hạn tần suất kiểm tra lại vị trí. Có nhiều cách để thực hiện việc này, nhưng HTML5 Rocks có một bài viết về cách giảm độ trễ của các sự kiện cuộn bằng cách sử dụng requestAnimationFrame. Phương thức này sẽ hoạt động hiệu quả ở đây.

Trước khi chúng ta xử lý bất kỳ hoạt động phát hiện lượt nhấn nào, ví dụ đầu tiên này sẽ chỉ xuất ra x và y tương đối bất cứ khi nào bạn di chuyển chuột trong khu vực Stage (Sân khấu).

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

Để bắt đầu theo dõi chuyển động của chuột, chúng ta sẽ tạo một đối tượng Stage mới và truyền vào đó mã nhận dạng của div mà chúng ta muốn sử dụng làm Stage.

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

Phát hiện lượt nhấn đơn giản

Trong JAM với Chrome, không phải tất cả giao diện của nhạc cụ đều phức tạp. Các bàn phím của máy trống chỉ là hình chữ nhật đơn giản, giúp dễ dàng phát hiện liệu một lượt nhấp có nằm trong giới hạn của các bàn phím đó hay không.

Máy đánh trống

Bắt đầu với hình chữ nhật, chúng ta sẽ thiết lập một số loại hình cơ bản. Mỗi đối tượng hình dạng cần biết giới hạn của đối tượng đó và có khả năng kiểm tra xem một điểm có nằm trong đối tượng đó hay không.

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

Mỗi loại hình dạng mới mà chúng ta thêm vào sẽ cần một hàm trong đối tượng Stage để đăng ký hình dạng đó làm vùng nhấn.

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

Trên các sự kiện chuột, mỗi thực thể hình dạng sẽ xử lý việc kiểm tra xem x và y của chuột đã truyền có phải là lượt nhấn hay không và trả về true hoặc false.

Chúng ta cũng có thể thêm một lớp "active" (đang hoạt động) vào phần tử sân khấu để thay đổi con trỏ chuột thành con trỏ khi di chuột qua hình vuông.

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

Hình dạng khác

Khi các hình dạng trở nên phức tạp hơn, phép toán để tìm xem một điểm có nằm bên trong các hình dạng đó hay không cũng trở nên phức tạp hơn. Tuy nhiên, các phương trình này đã được thiết lập và ghi chép chi tiết trên nhiều trang web. Một số ví dụ JavaScript hay nhất mà tôi từng thấy là trong thư viện hình học của Kevin Lindsey.

Rất may, trong quá trình xây dựng JAM bằng Chrome, chúng tôi không bao giờ phải vượt quá hình tròn và hình chữ nhật, dựa vào các tổ hợp hình dạng và lớp để xử lý mọi độ phức tạp khác.

Hình dạng trống

Vòng tròn

Để kiểm tra xem một điểm có nằm trong trống hình tròn hay không, chúng ta cần tạo một hình cơ sở hình tròn. Mặc dù khá giống với hình chữ nhật, nhưng hình tròn sẽ có các phương thức riêng để xác định giới hạn và kiểm tra xem điểm có nằm bên trong hình tròn hay không.

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

Thay vì thay đổi màu sắc, việc thêm lớp hit sẽ kích hoạt ảnh động CSS3. Kích thước nền giúp chúng ta có một cách hay để nhanh chóng điều chỉnh tỷ lệ hình ảnh của trống mà không ảnh hưởng đến vị trí của trống. Bạn sẽ cần thêm tiền tố của trình duyệt khác để làm việc này với chúng (-moz, -o và -ms) và cũng có thể muốn thêm một phiên bản không có tiền tố.

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

Chuỗi

Hàm GuitarString của chúng ta sẽ lấy một mã nhận dạng canvas và đối tượng Rect rồi vẽ một đường thẳng qua chính giữa hình chữ nhật đó.

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

Khi muốn làm cho dây đàn rung lên, chúng ta sẽ gọi hàm strum để đặt dây đàn chuyển động. Mỗi khung hình chúng ta kết xuất sẽ giảm nhẹ lực đánh và tăng bộ đếm khiến dây dao động qua lại.

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

Giao điểm và cách gảy

Khu vực nhấn của chuỗi sẽ lại là một hộp. Thao tác nhấp vào hộp đó sẽ kích hoạt ảnh động chuỗi. Nhưng ai muốn nhấp vào một cây đàn guitar?

Để thêm âm thanh gảy, chúng ta cần kiểm tra giao điểm của hộp chuỗi và đường mà con chuột của người dùng đang di chuyển.

Để có đủ khoảng cách giữa vị trí trước đó và vị trí hiện tại của chuột, chúng ta cần làm chậm tốc độ nhận sự kiện di chuyển chuột. Đối với ví dụ này, chúng ta chỉ cần đặt một cờ để bỏ qua các sự kiện mousemove trong 50 mili giây.

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

Tiếp theo, chúng ta sẽ cần dựa vào một số mã giao nhau mà Kevin Lindsey đã viết để xem liệu đường di chuyển của chuột có giao nhau với giữa hình chữ nhật hay không.

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

Cuối cùng, chúng ta sẽ thêm một Hàm mới để tạo một String Instrument (Nhạc cụ dây). Phương thức này sẽ tạo Stage mới, thiết lập một số chuỗi và lấy ngữ cảnh của Canvas mà bạn sẽ vẽ.

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

Tiếp theo, chúng ta sẽ định vị các khu vực nhấn của các chuỗi, sau đó thêm các khu vực này vào phần tử Stage (Sân khấu).

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

Cuối cùng, hàm kết xuất của StringInstrument sẽ lặp lại qua tất cả các chuỗi và gọi các phương thức kết xuất của chúng. Lớp này chạy liên tục, nhanh chóng như khi requestAnimationFrame thấy phù hợp. Bạn có thể đọc thêm về requestAnimationFrame trong bài viết requestAnimationFrame để tạo ảnh động thông minh của Paul Irish.

Trong một ứng dụng thực tế, bạn có thể muốn đặt cờ khi không có ảnh động nào đang diễn ra để ngừng vẽ khung canvas mới.

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

Tóm tắt

Việc có một phần tử Stage chung để xử lý tất cả các hoạt động tương tác của chúng ta không phải là không có nhược điểm. Phương thức này phức tạp hơn về mặt tính toán và các sự kiện con trỏ con trỏ bị hạn chế nếu không thêm mã bổ sung để thay đổi các sự kiện đó. Tuy nhiên, đối với JAM với Chrome, lợi ích của việc có thể trừu tượng hoá các sự kiện chuột khỏi các phần tử riêng lẻ đã hoạt động rất hiệu quả. Điều này cho phép chúng ta thử nghiệm nhiều hơn với thiết kế giao diện, chuyển đổi giữa các phương thức tạo ảnh động cho phần tử, sử dụng SVG để thay thế hình ảnh của các hình dạng cơ bản, dễ dàng tắt vùng nhấn và làm nhiều việc khác.

Để xem các tính năng Trống và Sting hoạt động, hãy bắt đầu JAM của riêng bạn rồi chọn Trống tiêu chuẩn hoặc Guitar điện sạch cổ điển.

Biểu trưng của Jam