Örnek olay - Chrome JAM

Kullanıcı arayüzünü nasıl mükemmel hale getirdik?

Fred Chasen
Fred Chasen

Giriş

Chrome JAM, Google tarafından oluşturulmuş, web tabanlı bir müzikal projedir. Chrome JAM, dünyanın her yerinden kullanıcıların tarayıcı içinde gerçek zamanlı olarak kayış oluşturup JAM oluşturmasını sağlar. DinahMoe, Chrome'un Web Audio API'siyle yapabileceklerinin sınırlarını zorladı. Tool of North America'daki ekibimiz, bilgisayarınızı bir müzik enstrümanı gibi çalma, davul çalma ve bilgisayarınızı çalmanız için tasarladı.

Google Creative Lab'in yaratıcı yönetiminden yararlanan illüstratör Rob Bailey, JAM'de kullanılabilen 19 enstrümanın her biri için karmaşık resimler oluşturdu. İnteraktif Direktör Ben Tricklebank ile Tool tasarım ekibimiz her enstrüman için kolay ve profesyonel bir arayüz oluşturdu.

Tam doğaçlama montajı

Her alet görsel olarak benzersiz olduğundan, Aracın Teknik Direktörü Bartek Drozdz ve ben PNG resimleri, CSS, SVG ve Tuval öğelerinin kombinasyonlarını kullanarak bunları birleştirdik.

Çoğu enstrüman, DinahMoe'nun ses motoruyla arayüzü aynı tutarken farklı etkileşim yöntemlerini (tıklamalar, sürükle ve çalgılar gibi, bir enstrümanla yapmayı bekleyeceğiniz her şey) işlemek zorunda kaldı. Güzel bir oyun deneyimi sağlayabilmek için JavaScript’in fareyi yukarı ve aşağı hareket ettirmeden daha fazlasına ihtiyacımız olduğunu fark ettik.

Tüm bu varyasyonlarla başa çıkmak için oynanabilir alanı kapsayan bir "Sahne" öğesi oluşturduk.

Sahne

Sahne, bir araç genelinde işlev ayarlamak için kullandığımız denetleyicimizdir. Örneğin, kullanıcıların etkileşimde bulunacağı enstrümanların farklı parçalarını eklemek gibi. Daha fazla etkileşim ("isabet" gibi) ekledikçe bunları Sahnenin prototipine ekleyebiliriz.

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

Öğeyi ve fare konumunu alma

İlk görevimiz, tarayıcı penceresindeki fare koordinatlarını Stage öğemize göre çevirmektir. Bunu yapmak için Sahnemizin sayfada bulunduğu yeri göz önünde bulundurmamız gerekiyordu.

Öğenin yalnızca üst öğesine değil, tüm pencereye göre nerede olduğunu bulmamız gerektiğinden, bu durum, sadece OffTop ve OffLeft öğelerine bakmaktan biraz daha karmaşıktır. En kolay seçenek, fare etkinliklerinde olduğu gibi pencereye göre konum veren ve yeni tarayıcılarda iyi desteklenen getBoundingClientRect kullanmaktır.

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 yoksa, ofsetleri toplayacak, öğe üst öğeleri zincirini gövdeye ulaşana kadar yukarı taşıyacak basit bir işlevimiz vardır. Ardından, pencereyle göreli konumu elde etmek için pencerenin ne kadar kaydırıldığını çıkarırız. jQuery kullanıyorsanız, ofset() işlevi farklı platformlar arasında konumu saptamanın karmaşıklığını başarıyla üstlenebilir, ancak yine de kaydırılan miktarı çıkarmanız gerekir.

Sayfa kaydırıldığında veya yeniden boyutlandırıldığında öğenin konumu değişmiş olabilir. Bu etkinlikleri dinleyebilir ve konumu tekrar kontrol edebiliriz. Bu etkinlikler, tipik bir kaydırma veya yeniden boyutlandırma sırasında birçok kez tetiklenir. Bu nedenle, gerçek bir uygulamada konumu tekrar kontrol etme sıklığınızı sınırlamak en iyisidir. Bunu yapmanın pek çok yolu vardır ancak HTML5 Rocks'ın, requestAnimationFrame kullanarak kaydırma etkinliklerini tekrar eski haline getirme ile ilgili bir makalesi vardır. Bu makale burada iyi sonuç verecektir.

Herhangi bir isabet algılaması işlemeden önce, bu ilk örnekte fare Sahne alanında hareket ettirildiğinde göreli x ve y çıktısı alınacaktır.

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

Fare hareketini izlemeye başlamak için yeni bir Aşama nesnesi oluşturacağız ve bu nesneye, Sahnemiz olarak kullanmak istediğimiz div'in kimliğini ileteceğiz.

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

Basit isabet algılama

Chrome JAM'de tüm araç arayüzleri karmaşık değildir. Davul makinesi pedlerimiz basit dikdörtgenlerden oluşur. Böylece tıklama sınırlarının dışına çıkıp çıkmadığını kolayca tespit edebiliriz.

Davul makinesi

Dikdörtgenlerden başlayarak bazı temel şekil türleri oluşturacağız. Her şekil nesnesinin, sınırlarını bilmesi ve içinde bir nokta olup olmadığını kontrol edebilmesi gerekir.

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

Eklediğimiz her yeni şekil türünün isabet bölgesi olarak kaydedilmesi için Aşama nesnemizde bir işlev olması gerekir.

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

Fare etkinliklerinde, her şekil örneği, iletilen fare x ve y'nin kendisi için bir isabet olup olmadığını kontrol eder ve true veya false döndürür.

Ayrıca, sahne öğesine, fareyle karenin üzerine gelindiğinde fare imlecini bir işaretçi haline getirecek bir "etkin" sınıfı da ekleyebiliriz.

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

Diğer şekiller

Şekiller daha karmaşık hale geldikçe, içlerinde bir nokta olup olmadığını anlamak için yapılan hesaplama da daha karmaşık hale gelir. Ancak, bu denklemler birçok yerde yerleşik ve ayrıntılı olarak açıklanmıştır. Gördüğüm en iyi JavaScript örneklerinden bazıları Kevin Lindsey’in geometri kitaplığına aittir.

Neyse ki Chrome JAM geliştirirken hiç ekstra karmaşıklıklarla başa çıkmak için şekil ve katman kombinasyonlarından yararlanarak, dairelerin ve dikdörtgenlerin ötesine geçmek zorunda kalmadık.

Davul şekilleri

Daireler

Bir noktanın dairesel bir tambur içinde olup olmadığını kontrol etmek için daire taban şekli oluşturmamız gerekir. Dikdörtgene oldukça benzese de, sınırları belirlemek ve noktanın dairenin içinde olup olmadığını kontrol etmek için kendi yöntemleri vardır.

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

Rengi değiştirmek yerine, isabet sınıfının eklenmesi bir CSS3 animasyonunu tetikler. Arka plan boyutu, tamburun konumunu etkilemeden görüntüyü hızlı bir şekilde ölçeklendirmemizi sağlayan iyi bir yöntem sağlıyor. Bu çalışma için başka tarayıcıların öneklerini eklemeniz gerekir (-moz, -o ve -ms) ve öneksiz bir sürüm eklemek isteyebilirsiniz.

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

Dize

GuitarString işlevimiz bir tuval kimliği ve Dikdörtgen nesnesi alıp bu dikdörtgenin merkezi boyunca bir çizgi çizer.

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

Titreşim olmasını istediğimizde, ipi harekete geçirmek için strum işlevimizi çağırırız. Oluşturduğumuz her kare, biraz tıkanma kuvvetini azaltır ve dizenin ileri geri gitmesine neden olacak bir sayacı artırır.

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

Kavşaklar ve Çalmalar

Dize için isabet alanımız yine bir kutudur. Bu kutunun içini tıklamak, dize animasyonunu tetiklemelidir. Peki gitarı kim tıklamak ister?

Çalgı eklemek için dizeler kutusunun kesişimini ve kullanıcının faresinin gittiği çizgiyi kontrol etmemiz gerekir.

Farenin önceki ve şu anki konumu arasında yeterli mesafeyi sağlamak için fare hareketi etkinliklerini alma hızımızı yavaşlatmamız gerekir. Bu örnekte, mousemove etkinliklerini 50 milisaniye süreyle yoksaymak için bir işaret ayarlayacağız.

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

Ardından, fare hareketi çizgisinin dikdörtgenimizin ortasında kesişip kesişmediğini görmek için Kevin Lindsey tarafından yazılan kesişim kodunu kullanmamız gerekecektir.

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

Son olarak, Yaylı Enstrüman oluşturmak için yeni bir Function ekleyeceğiz. Bu işlem yeni Sahneyi oluşturur, birkaç dize oluşturur ve çizilecek Kanvasın bağlamını öğrenir.

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

Daha sonra, dizelerin isabet alanlarını konumlandıracak ve ardından bunları Stage öğesine ekleyeceğiz.

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

Son olarak, Araç Çubuğu'nun oluşturma işlevi tüm dizelerimizi döngüye alır ve bunların oluşturma yöntemlerini çağırır. Her zaman çalışır ve requestAnimationFrame uygun gördüğü şekilde hızla çalışır. requestAnimationFrame for smart animating başlıklı makalede, requestAnimationFrame hakkında daha fazla bilgi bulabilirsiniz.

Gerçek bir uygulamada, animasyon yokken yeni bir tuval çerçevesi çizmeyi durdurmak için bir bayrak ayarlamak isteyebilirsiniz.

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

Son adım

Tüm etkileşimimizle başa çıkabilecek ortak bir Aşama öğesine sahip olmanın dezavantajları da var. Bu işlem hesaplama açısından daha karmaşıktır ve imleç işaretçi etkinlikleri sınırlandırılarak, bunları değiştirmek için fazladan kod eklenmesine gerek kalmaz. Ancak, Chrome JAM için ayrı ayrı öğelerden uzaklaştırılan fare etkinliklerini soyutlayabilmenin faydaları gerçekten işe yaradı. Arayüz tasarımıyla daha fazla deneme yapmamıza, öğelere animasyon yöntemleri arasında geçiş yapmamıza, temel şekil resimlerini değiştirmek için SVG'yi kullanmamıza, isabet alanlarını kolayca devre dışı bırakmamıza ve daha pek çok işlem yapmamızı sağladı.

Davul ve Baget'ın nasıl çalıştığını görmek için kendi JAM'inizi başlatın ve Standart Davul veya Klasik Temiz Elektro Gitar'ı seçin.

Jam Logosu