우수사례 - JAM with Chrome

Google Play가 만든 UI

소개

JAM with Chrome은 Google에서 만든 웹 기반 음악 프로젝트입니다. JAM with Chrome을 사용하면 전 세계 사람들이 브라우저 내에서 실시간으로 밴드와 JAM을 결성할 수 있습니다. DinahMoeChrome의 Web Audio API로 가능성의 한계를 뛰어넘었습니다. Tool of North America의 Google 팀은 컴퓨터를 마치 악기처럼 연주하고, 드럼을 치고, 연주할 수 있는 인터페이스를 만들었습니다.

일러스트레이터 롭 베일리는 Google Creative Lab의 창작 디렉션에 따라 JAM에서 사용할 수 있는 19가지 악기에 대해 각각 정교한 일러스트레이션을 만들었습니다. 이 과정을 통해 인터랙티브 디렉터 Ben Tricklebank와 Tool의 디자인팀은 각 악기에 대해 쉽고 효과적인 인터페이스를 만들었습니다.

풀 잼 몽타주

각 악기는 시각적으로 독특하기 때문에, 툴의 기술 이사인 Bartek Drozdz와 저는 PNG 이미지, CSS, SVG, Canvas 요소를 조합하여 이러한 도구를 결합했습니다.

많은 악기가 DinahMoe의 사운드 엔진과의 인터페이스를 동일하게 유지하면서 다양한 상호작용 방법 (예: 클릭, 드래그, 스트럼 등)을 처리해야 했습니다. 우리는 멋진 플레이 환경을 제공하려면 JavaScript의 마우스 업과 마우스다운 외에 다른 요소가 필요하다는 것을 알게 되었습니다.

이러한 모든 변형을 처리하기 위해 우리는 연주 가능한 영역을 포함하는 '스테이지' 요소를 만들어 다양한 악기의 클릭, 드래그, 스트럼을 처리합니다.

무대

스테이지는 악기 전반에 걸쳐 기능을 설정하는 데 사용하는 컨트롤러입니다. 예를 들어 사용자가 상호작용할 결제 수단의 여러 부분을 추가할 수 있습니다. '조회'와 같은 상호작용을 더 추가하면 스테이지의 프로토타입에 추가할 수 있습니다.

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 요소를 기준으로 변환시키는 것입니다. 이를 위해서는 페이지에서 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의 ID를 전달합니다.

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

간단한 적중 감지

JAM with 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.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를 반환하는지 확인을 처리합니다.

또한 정사각형 요소에 마우스를 가져가면 마우스 커서가 포인터가 되도록 변경하는 'active' 클래스를 스테이지 요소에 추가할 수도 있습니다.

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

도형 더보기

도형이 더 복잡해짐에 따라 도형 안에 점이 있는지 알아내는 계산도 더 복잡해집니다. 그러나 이러한 공식은 잘 확립되어 있고 온라인상의 많은 곳에 상세하게 문서화되어 있습니다. 제가 본 자바스크립트 예제 중 몇 가지를 예로 들면 Kevin Lindsey의 도형 라이브러리가 있습니다.

다행히 Chrome으로 JAM을 빌드할 때 우리는 원과 직사각형을 넘어서서 추가적인 복잡성을 처리하기 위해 도형과 레이어의 조합에 의존할 필요가 없었습니다.

드럼 모양

점이 원형 드럼 내에 있는지 확인하려면 원형 기본 모양을 만들어야 합니다. 직사각형과 상당히 비슷하지만 경계를 결정하고 점이 원 안에 있는지 확인하기 위한 자체 메서드가 있습니다.

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 함수는 캔버스 ID와 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);
};

다음으로 Kevin Lindsey가 작성한 교차로 코드를 사용하여 마우스 움직임 선이 직사각형의 중간과 교차하는지 확인해야 합니다.

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

그런 다음 문자열의 적중 영역을 배치한 다음 스테이지 요소에 추가합니다.

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 with Chrome의 경우 개별 요소에서 마우스 이벤트를 추상화할 수 있다는 이점이 무척 잘 작동했습니다. 이를 통해 인터페이스 디자인을 더 실험하고, 요소에 애니메이션을 적용하는 방법 간에 전환하고, SVG를 사용하여 기본 도형의 이미지를 대체하고, 적중 영역을 쉽게 사용 중지하는 등의 작업을 수행할 수 있습니다.

드럼과 스팅이 작동하는 모습을 보려면 내 JAM을 시작하고 Standard Drum이나 Classic Clean Electric Guitar를 선택하세요.

Jam 로고