Örnek olay - Chrome JAM

Kullanıcı arayüzünü nasıl mükemmelleştirdik?

Fred Chasen
Fred Chasen

Giriş

JAM with Chrome, Google tarafından oluşturulan web tabanlı bir müzik projesidir. Chrome ile JAM, dünyanın her yerinden kullanıcıların grup oluşturmasına ve tarayıcıda gerçek zamanlı olarak JAM yapmasına olanak tanır. DinahMoe, Chrome'un Web Audio API'si ile mümkün olanın sınırlarını zorladı. Tool of North America ekibimiz ise bilgisayarınızı bir müzik aleti gibi çalmak, tıngırdatmak ve çalmak için arayüzü tasarladı.

Google Creative Lab'in yaratıcı yönüyle birlikte çalışan illüstratör Rob Bailey, JAM'de kullanılabilen 19 enstrümanın her biri için karmaşık illüstrasyonlar oluşturdu. Bu bilgilerden yola çıkarak Etkileşimli Yönetmen Ben Tricklebank ve Tool'daki tasarım ekibimiz her enstrüman için kolay ve profesyonel bir arayüz oluşturdu.

Tam jam montaj

Her enstrüman görsel olarak benzersiz olduğundan Tool'un Teknik Direktörü Bartek Drozdz ile birlikte PNG resimleri, CSS, SVG ve tuval öğelerini kullanarak bunları birleştirdik.

Enstrümanların çoğunda, DinahMoe'un ses motoruyla arayüzü aynı tutarken farklı etkileşim yöntemleri (ör. tıklama, sürükleme ve tıngırdatma gibi bir enstrümanla yapmayı beklediğiniz tüm işlemler) kullanıldı. Güzel bir oyun deneyimi sunabilmek için JavaScript'in mouseup ve mousedown özelliklerinden daha fazlasına ihtiyacımız olduğunu fark ettik.

Tüm bu çeşitlilikle başa çıkmak için, çalınabilir alanı kaplayan ve tüm farklı enstrümanlarda tıklamaları, sürüklemeleri ve tıngırtıları işleyen bir "Sahne" öğesi oluşturduk.

Sahne

Stage, bir enstrümanda işlevi ayarlamak için kullandığımız denetleyicimizdir. Örneğin, kullanıcının etkileşimde bulunacağı enstrümanların farklı bölümlerini ekleyebilirsiniz. Daha fazla etkileşim ekledikçe ("isaretçi" gibi) 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ı sahne öğemize göre olacak şekilde çevirmektir. Bunu yapmak için sahnemizin sayfadaki yerini dikkate almamız gerekiyordu.

Öğenin yalnızca üst öğesine göre değil, pencerenin tamamına göre nerede olduğunu bulmamız gerektiğinden, bu işlem yalnızca offsetTop ve offsetLeft öğelerine bakmaktan biraz daha karmaşıktır. En kolay seçenek, tıpkı fare etkinlikleri gibi pencereye göre konumu veren ve yeni tarayıcılarda iyi desteklenen getBoundingClientRect işlevini 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 mevcut değilse öğe üst öğelerinin zincirini body'ye ulaşana kadar yukarı doğru hareket ettirerek yalnızca ofsetleri toplayan basit bir işlevimiz vardır. Ardından, pencereye göre konumu elde etmek için pencerenin ne kadar kaydırıldığını çıkarırız. jQuery kullanıyorsanız offset() işlevi, platformlar arasında konumu bulmanın karmaşıklığını gidermede mükemmel bir iş çıkarır ancak yine de kaydırılan miktarı çıkarmanız gerekir.

Sayfa her 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, normal bir kaydırma veya yeniden boyutlandırma işleminde birçok kez tetiklenir. Bu nedenle, gerçek bir uygulamada konumu ne sıklıkta yeniden kontrol edeceğinizi sınırlamak muhtemelen en iyisidir. Bunu yapmanın birçok yolu vardır ancak HTML5 Rocks'ta, requestAnimationFrame kullanarak kaydırma etkinliklerini sarmalama hakkında bir makale vardır. Bu makale burada işe yarayacaktır.

İsabet algılamayı işlemeden önce bu ilk örnekte, fare Sahne alanında hareket ettirildiğinde yalnızca göreli x ve y değerleri gösterilir.

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 Stage nesnesi oluşturup bu nesneye Stage 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 ile JAM'de enstrümanların arayüzlerinin tümü karmaşık değildir. Davul makinesi pad'lerimiz basit dikdörtgenler olduğundan, bir tıklamanın sınırlarına düşüp düşmediğini tespit etmek kolaydır.

Davul makinesi

Dikdörtgenlerden başlayarak bazı temel şekil türlerini ayarlayacağız. Her şekil nesnesinin sınırlarını bilmesi ve bir noktanın içinde 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ü için, isabet bölgesi olarak kaydedilmesi amacıyla Stage nesnemizde bir işleve ihtiyaç vardır.

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 koordinatlarının kendisi için isabet olup olmadığını kontrol eder ve doğru veya yanlış değerini döndürür.

Ayrıca, sahne öğesine "etkin" sınıfı ekleyerek fare imlecini, fareyle karenin üzerine geldiğinizde işaretçi olarak değiştirebiliriz.

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 karmaşıklaştıkça, bir noktanın içinde olup olmadığını bulmak için gereken matematik de karmaşıklaşır. Ancak bu denklemler, internetteki birçok yerde ayrıntılı olarak açıklanmıştır. Gördüğüm en iyi JavaScript örneklerinden bazıları Kevin Lindsey'nin geometri kitaplığında yer alıyor.

Neyse ki JAM'i Chrome ile oluştururken hiçbir zaman daire ve dikdörtgenlerin ötesine geçmek zorunda kalmadık. Ek karmaşıklığı ele almak için şekil kombinasyonlarından ve katmanlardan yararlandık.

Davul şekilleri

Daireler

Bir noktanın dairesel bir tambur içinde olup olmadığını kontrol etmek için bir 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;
};

İsabet sınıfı eklemek, rengi değiştirmek yerine bir CSS3 animasyonu tetikler. Arka plan boyutu, tamburun resmini konumunu etkilemeden hızlı bir şekilde ölçeklendirmenin güzel bir yolunu sunar. Bu tarayıcılarla çalışmak için diğer tarayıcıların ön eklerini (-moz, -o ve -ms) eklemeniz gerekir. Ayrıca ön ek içermeyen bir sürüm de ekleyebilirsiniz.

#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 Rect nesnesi alır ve bu dikdörtgenin ortasına 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şime geçirmek istediğimizde, teli harekete geçirmek için tıngırdat işlevimizi çağırırız. Oluşturduğumuz her karede, teli çalarken uygulanan kuvvet biraz azaltılır ve teli ileri geri sallamaya neden olacak bir sayaç artırılı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;
};

Kesişimler ve Çırpışma

Dize için dokunma alanımız yine bir kutu olacak. Bu kutuyu tıkladığınızda dize animasyonu tetiklenir. Ama kim gitar tıklamak ister?

Çırpı çalmak için tellerin bulunduğu kutunun, kullanıcının faresinin hareket ettiği çizgiyle kesişim noktasını kontrol etmemiz gerekir.

Farenin önceki ve mevcut konumu arasında yeterli mesafe elde etmek için fare hareketi etkinliklerini aldığımız hızı yavaşlatmamız gerekir. Bu örnekte, mousemove etkinliklerini 50 milisaniye boyunca yok sayacak 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ını kesip kesmediğini görmek için Kevin Lindsey'nin yazdığı kesişim kodunu kullanmamız gerekir.

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, telli bir enstrüman oluşturmak için yeni bir işlev ekleyeceğiz. Yeni sahneyi oluşturur, bir dizi dize oluşturur ve üzerine çizilecek kanvasın bağlamını alır.

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

Ardından, dizelerin isabet alanlarını konumlandırıp bunları Sahne öğ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, StringInstrument'ın render işlevi tüm dizelerimizde döngü oluşturur ve render yöntemlerini çağırır. requestAnimationFrame'in uygun gördüğü hızda her zaman çalışır. requestAnimationFrame hakkında daha fazla bilgiyi Paul Irish'ın Akıllı animasyon için requestAnimationFrame makalesinde bulabilirsiniz.

Gerçek bir uygulamada, animasyon gerçekleşmediğinde yeni bir kanvas çerçevesinin çizilmesini durdurmak için bir işaret 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şimlerimizi yönetmek için ortak bir Stage öğesine sahip olmanın da dezavantajları vardır. Bu yöntem, hesaplama açısından daha karmaşıktır ve imleç işaretçisi etkinlikleri, bunları değiştirmek için ek kod eklemeden sınırlıdır. Ancak Chrome ile JAM için fare etkinliklerini tek tek öğelerden soyutlayabilmenin avantajları gerçekten çok işe yaradı. Bu sayede arayüz tasarımıyla daha fazla deneme yapmamıza, öğeleri animasyonlandırma yöntemleri arasında geçiş yapmamıza, temel şekillerin resimlerini SVG ile değiştirmemize, isabet alanlarını kolayca devre dışı bırakmamıza ve daha birçok işlem yapmamıza olanak tanıdı.

Davul ve Sting'in nasıl kullanıldığını görmek için kendi JAM'inizi başlatın ve Standart Davullar veya Klasik Temiz Elektrik Gitarı'nı seçin.

Jam Logosu