Пример использования: JAM с Chrome

Как мы сделали пользовательский интерфейс потрясающим

Введение

JAM with Chrome — это музыкальный веб-проект, созданный Google. JAM с Chrome позволяет людям со всего мира создавать группы и JAM в режиме реального времени внутри браузера. DinahMoe раздвинула границы возможного с помощью Chrome Web Audio API . Наша команда Tool of North America создала интерфейс для игры на барабанах, игре на компьютере, как если бы это был музыкальный инструмент.

Под творческим руководством Google Creative Lab иллюстратор Роб Бэйли создал сложные иллюстрации для каждого из 19 инструментов, доступных для JAM. Работая над этим, интерактивный директор Бен Триклбэнк и наша команда дизайнеров Tool создали простой и профессиональный интерфейс для каждого инструмента.

Полный монтаж варенья

Поскольку каждый инструмент визуально уникален, мы с техническим директором Tool Бартеком Дроздзом соединили их вместе, используя комбинации изображений PNG, элементов CSS, SVG и Canvas.

Многие инструменты должны были поддерживать различные методы взаимодействия (такие как щелчки, перетаскивание и удары - все, что вы ожидаете делать с инструментом), сохраняя при этом интерфейс со звуковым движком DinahMoe одинаковым. Мы обнаружили, что нам нужно нечто большее, чем просто JavaScript mouseup и mousedown, чтобы обеспечить красивый игровой процесс.

Чтобы справиться со всеми этими вариациями, мы создали элемент «Сцена», который покрывал игровую область, обрабатывая щелчки, перетаскивания и удары по всем различным инструментам.

Сцена

Сцена — это наш контроллер, который мы используем для настройки функций инструмента. Например, добавление различных частей инструментов, с которыми будет взаимодействовать пользователь. По мере добавления новых взаимодействий (например, «попаданий») мы можем добавлять их в прототип сцены.

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 и передадим ему идентификатор элемента div, который мы хотим использовать в качестве нашей сцены.

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

Простое обнаружение попаданий

В JAM с Chrome не все интерфейсы инструментов сложны. Наши пэды драм-машины представляют собой простые прямоугольники, что позволяет легко определить, попадает ли щелчок в их пределы.

Драм-машина

Начав с прямоугольников, мы настроим некоторые базовые типы фигур. Каждый объект-фигура должен знать свои границы и иметь возможность проверять, находится ли точка внутри него.

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, чтобы зарегистрировать его как зону попадания.

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.

Мы также можем добавить к элементу сцены «активный» класс, который будет превращать курсор мыши в указатель при наведении на квадрат.

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, которые я видел, взяты из библиотеки геометрии Кевина Линдси.

К счастью, при создании JAM с помощью Chrome нам никогда не приходилось выходить за рамки кругов и прямоугольников, полагаясь на комбинации фигур и слоев для решения любой дополнительной сложности.

Формы барабанов

Круги

Чтобы проверить, находится ли точка внутри круглого барабана, нам нужно создать базовую форму круга. Хотя он очень похож на прямоугольник, у него будут свои собственные методы для определения границ и проверки, находится ли точка внутри круга.

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 возьмет идентификатор холста и объект 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);
};

Далее нам нужно будет положиться на некоторый код пересечения, который написал Кевин Линдси, чтобы увидеть, пересекает ли линия движения мыши середину нашего прямоугольника.

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

Далее мы расположим области попадания строк, а затем добавим их в элемент 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);
  }
};

Наконец, функция рендеринга StringInstrument пройдёт через все наши строки и вызовет их методы рендеринга. Он выполняется постоянно и так быстро, как считает нужным requestAnimationFrame. Подробнее о requestAnimationFrame можно прочитать в статье Пола Айриша requestAnimationFrame для умной анимации .

В реальном приложении вам может потребоваться установить флаг, когда анимация не происходит, чтобы остановить рисование нового кадра холста.

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

Заворачивать

Наличие общего элемента Stage для управления всем нашим взаимодействием не лишено недостатков. Это более сложно в вычислительном отношении, и события указателя курсора ограничены без добавления дополнительного кода для их изменения. Однако для JAM с Chrome преимущества возможности абстрагировать события мыши от отдельных элементов сработали очень хорошо. Это позволило нам больше экспериментировать с дизайном интерфейса, переключаться между методами анимации элементов, использовать SVG для замены изображений основных фигур, легко отключать области попадания и многое другое.

Чтобы увидеть барабаны и стинги в действии, запустите свой собственный JAM и выберите стандартные барабаны или классическую чистую электрогитару .

Логотип Джема