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 đột phá

Fred Chasen
Fred Chasen

Giới thiệu

JAM with Chrome là một dự án âm nhạc dựa trên nền tảng web do Google tạo ra. ỨNG TÁC với Chrome cho phép mọi người từ khắp nơi trên thế giới tạo thành một ban nhạc và ỨNG TÁC theo thời gian thực trong trình duyệt. DinahMoe đã vượt qua những giới hạn của những điều hiện thực bằng API Âm thanh web của Chrome, nhóm của chúng tôi tại Tool of Bắc Mỹ đã tạo ra giao diện cho việc đánh trống, đá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, hoạ sĩ minh hoạ Rob Bailey đã tạo các hình minh hoạ phức tạp cho từng nhạc cụ trong số 19 nhạc cụ có sẵn để ỨNG TÁC. Dựa trên những yếu tố đó, 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ễ sử dụng và chuyên nghiệp cho mỗi công cụ.

Dựng phim đầy đủ

Vì mỗi nhạc cụ đều có hình ảnh độc đáo nên Giám đốc kỹ thuật của công cụ Bartek Drozdz và tôi đã nối chúng lại với nhau 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ý các phương pháp tương tác khác nhau (chẳng hạn như nhấp chuột, kéo và gõ - tất cả những việc bạn muốn thực hiện 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úng tôi không chỉ cần di chuột lên và di chuột xuống của JavaScript để có thể cung cấp trải nghiệm chơi đẹp mắt.

Để xử lý tất cả biến thể này, chúng tôi đã tạo một phần tử “Giai đoạn” bao gồm khu vực có thể phát, xử lý các lượt nhấp, kéo và gảy trên tất cả các công cụ.

Giai đoạn

Giai đoạn là bộ điều khiển mà chúng ta sử dụng để thiết lập chức năng trên một công cụ. Chẳng hạn như thêm các phần khác nhau của công cụ mà người dùng sẽ tương tác. Khi thêm nhiều lượt tương tác hơn (chẳng hạn như "lượt truy cập"), chúng ta có thể thêm chúng vào nguyên mẫu của Giai đoạn.

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ạ độ chuột trong cửa sổ trình duyệt sao cho tương ứng với phần tử Giai đoạn. Để làm điều này, chúng ta cần xem xét vị trí của Giai đoạn trên trang.

Khi 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ẹ, điều này sẽ phức tạp hơn một chút so với việc chỉ xem xét các phần tử offsetTop và offsetLeft. Lựa chọn 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ư sự kiện chuột và được hỗ trợ 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ỉ thêm các độ lệch, di chuyển lên trên chuỗi của phần tử mẹ cho đến khi phần tử đó đến được phần nội dung. Sau đó, chúng ta trừ đi khoảng cách cuộn cửa sổ để có được vị trí tương ứng với cửa sổ. Nếu bạn đang sử dụng jQuery, thì hàm offset() làm rất tốt việc xử lý độ 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 sẽ cần phải trừ đi số lượt cuộn.

Bất cứ khi nào người dùng cuộn hoặc đổi kích thước trang, vị trí của phần tử có thể đã thay đổi. Chúng tôi có thể theo dõi những 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 trên một thao tác 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 là 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 loại bỏ sự kiện cuộn bằng cách sử dụng requestAnimationFrame. Hoạt động này sẽ hoạt động tốt tại đây.

Trước khi chúng ta xử lý việc phát hiện lượt truy cập, ví dụ đầu tiên này sẽ cho ra kết quả x và y tương đối bất cứ khi nào chuột được di chuyển trong khu vực Giai đoạn.

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 xem chuyển động của chuột, chúng ta sẽ tạo một đối tượng Giai đoạn mới và truyền đối tượng đó mã nhận dạng của div mà chúng ta muốn sử dụng làm Giai đoạn.

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

Phát hiện lượt truy cập đơn giản

Trong BẢN ỨNG TÁC với Chrome, không phải tất cả các giao diện công cụ đều phức tạp. Miếng đệm máy đánh trống của chúng tôi chỉ là các hình chữ nhật đơn giản, giúp bạn dễ dàng phát hiện xem một lần nhấp có nằm trong giới hạn của chúng 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ơ sở. Mỗi đối tượng hình dạng cần biết các ranh giới của nó và có thể kiểm tra xem một điểm có nằm trong đó 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 chúng ta thêm sẽ cần có một hàm trong đối tượng Giai đoạn để đăng ký nó làm vùng truy cập.

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 chuột x và y đã truyền có phải là lần truy cập cho nó hay không và trả về giá trị true hoặc false.

Chúng ta cũng có thể thêm một lớp "đang hoạt động" vào phần tử giai đoạn. Thao tác này sẽ thay đổi con trỏ chuột thành con trỏ khi di chuyển 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 để xác định xem một điểm có nằm trong đó 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 tốt và được ghi lại rất chi tiết ở nhiều nơi trực tuyến. Một số ví dụ JavaScript hay nhất mà tôi thấy là từ thư viện hình học của Kevin Lindsey.

Thật may là khi tạo ỨNG TÁC bằng Chrome, chúng tôi không bao giờ phải dừng lại ở hình tròn và hình chữ nhật, dựa vào sự kết hợp của các hình dạng và phân lớp để xử lý những hoạt động phức tạp hơn.

Hình dạng trống

Vòng tròn

Để kiểm tra xem một điểm có nằm trong trống tròn hay không, chúng ta cần tạo một hình đế hình tròn. Mặc dù khá giống với hình chữ nhật, nhưng 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 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 lượt truy cập sẽ kích hoạt ảnh động CSS3. Kích thước nền cho phép chúng ta nhanh chóng chia tỷ lệ hình ảnh trống mà không ảnh hưởng đến vị trí của trống. Đối với công việc này, bạn sẽ cần phải thêm tiền tố của trình duyệt khác (-moz, -o và -ms) và cũng có thể muốn thêm 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ã nhận dạng canvas và đối tượng Rect rồi vẽ một đường thẳng ở 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 chuỗi đó rung, chúng ta sẽ gọi hàm strum để làm cho chuỗi chuyển động. Mỗi khung hình chúng ta kết xuất sẽ làm giảm lực nhấn mạnh một chút và tăng bộ đếm sẽ khiến chuỗi 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à gắp

Vùng nhấn của chúng ta cho chuỗi sẽ lại là một hộp. Thao tác nhấp vào bên trong hộp đó sẽ kích hoạt ảnh động dạng chuỗi. Nhưng ai muốn nhấp vào ghi-ta?

Để thêm âm thanh, chúng ta cần chọn giao điểm của hộp chuỗi và đường mà chuột 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 sẽ cần làm chậm tốc độ nhận được sự kiện di chuyển chuột. Trong ví dụ này, chúng ta sẽ chỉ đặt một cờ để bỏ qua các sự kiện di chuyển bằng chuột 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 lộ mà Kevin Lindsey đã viết để xem liệu đường chuyển động của chuột có giao cắt với điểm giữa củ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 Nhạc cụ dây. Thao tác này sẽ tạo Giai đoạn mới, thiết lập một số chuỗi và nhận ngữ cảnh của Canvas sẽ được vẽ trên đó.

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 vùng truy cập của các chuỗi rồi thêm chúng vào phần tử Giai đoạn.

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 phương thức kết xuất của các chuỗi đó. Nó luôn chạy, một cách nhanh chóng 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 Ireland.

Trong ứng dụng thực tế, bạn có thể đặt cờ khi không có ảnh động nào xuất hiện để dừng vẽ một 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ử Giai đoạn chung để xử lý mọi hoạt động tương tác không phải là không có một số hạn chế. Phép tính phức tạp hơ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 ỨNG TÁC với Chrome, lợi ích của việc có thể tách riêng các sự kiện chuột ra khỏi các thành phần riêng lẻ đã hoạt động rất tốt. Nền tảng 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 pháp tạo ảnh động cho các 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 các khu vực truy cập và hơn thế nữa.

Để xem cách hoạt động của trống và trống, hãy bắt đầu với JAM của riêng bạn rồi chọn Standard trống hoặc Standard Electric Guitar.

Biểu trưng Jam