Fallstudie – JAM mit Chrome

So haben wir die Benutzeroberfläche zum Fels geschlagen

Fred Chasen
Fred Chasen

Einleitung

JAM mit Chrome ist ein webbasiertes Musikprojekt von Google. Dank JAM mit Chrome können Nutzer aus aller Welt eine Band gründen und JAM in Echtzeit im Browser starten. DinahMoe hat mit der Web Audio API von Chrome die Grenzen des Möglichen erweitert. Unser Team bei Tool of North America hat die Benutzeroberfläche für das Strumming, das Schlagen und das Spielen Ihres Computers wie ein Musikinstrument entwickelt.

Dank der kreativen Ausrichtung von Google Creative Lab schuf der Illustrator Rob Bailey für jedes der 19 Instrumente, die JAM zur Verfügung stehen, aufwendige Illustrationen. Auf Grundlage dieser Anforderungen erstellten der Interactive Director Ben Tricklebank und unser Designteam bei Tool eine einfache und professionelle Benutzeroberfläche für jedes Instrument.

Komplette Jammontage

Da jedes Instrument visuell einzigartig ist, haben der Technical Director Bartek Drozdz von Tool und ich sie mithilfe von Kombinationen aus PNG-Bildern, CSS, SVG und Canvas-Elementen zusammengefügt.

Viele der Instrumente mussten verschiedene Interaktionsmethoden bewältigen (wie Klicken, Ziehen und Strums – alles, was man von einem Instrument erwarten würde), während die Schnittstelle zur Sound-Engine von DinahMoe gleich blieb. Wir haben festgestellt, dass wir mehr als nur Mouse-up und Mouse-Down von JavaScript benötigen, um ein schönes Spielerlebnis zu bieten.

Um diese Variation zu bewältigen, erstellten wir ein Stage-Element, das den spielbaren Bereich abdeckte und Klicks, Drags und Strums über die verschiedenen Instrumente hinweg handhabt.

Die Bühne

Die Stage ist unser Controller, mit dem wir die Funktionen eines Instruments einrichten. Zum Beispiel das Hinzufügen verschiedener Teile der Instrumente, mit denen die Nutzenden interagieren. Wenn wir weitere Interaktionen hinzufügen (z. B. einen „Treffer“), können wir sie dem Prototyp der Phase hinzufügen.

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

Element und Mausposition abrufen

Unsere erste Aufgabe besteht darin, die Mauskoordinaten im Browserfenster so zu übersetzen, dass sie relativ zu unserem Stage-Element sind. Dazu mussten wir berücksichtigen, wo sich unsere Stage auf der Seite befindet.

Da wir herausfinden müssen, wo das Element relativ zum gesamten Fenster und nicht nur zum übergeordneten Element ist, ist es etwas komplizierter, als nur die Elemente „offsetTop“ und „offsetLeft“ zu betrachten. Die einfachste Möglichkeit ist getBoundingClientRect, das die Position relativ zum Fenster angibt, genau wie Mausereignisse, und es wird in neueren Browsern gut unterstützt.

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

Ist getBoundingClientRect nicht vorhanden, haben wir eine einfache Funktion, die lediglich die Offsets addiert und die Kette der übergeordneten Elemente nach oben bewegt, bis sie den Textkörper erreicht. Dann subtrahieren wir, wie weit das Fenster gescrollt wurde, um die relative Position zum Fenster zu erhalten. Wenn Sie jQuery verwenden, ist die Funktion offset() eine hervorragende Methode, um den Standort plattformübergreifend zu ermitteln. Sie müssen jedoch trotzdem den gescrollten Betrag abziehen.

Wenn Sie auf der Seite scrollen oder die Größe ändern, kann es sein, dass sich die Position des Elements geändert hat. Wir können auf diese Ereignisse warten und die Position noch einmal prüfen. Diese Ereignisse werden beim Scrollen oder bei der Größe der Größe mehrfach ausgelöst. In einer echten Anwendung sollten Sie daher die Häufigkeit begrenzen, mit der Sie die Position noch einmal überprüfen. Dafür gibt es viele Möglichkeiten. In HTML5 Rocks gibt es jedoch einen Artikel zum Entprellen von Scroll-Ereignissen mithilfe von requestAnimationFrame.

Bevor wir mit der Treffererkennung beginnen, werden in diesem ersten Beispiel einfach die relativen Werte x und y ausgegeben, wenn die Maus im Anzeigebereich bewegt wird.

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

Um die Mausbewegung zu beobachten, erstellen wir ein neues Stage-Objekt und übergeben ihm die ID des div-Elements, das wir als Stage verwenden möchten.

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

Einfache Treffererkennung

In JAM mit Chrome sind nicht alle Benutzeroberflächen komplex. Unsere Drumcomputer-Pads sind einfache Rechtecke, mit denen Sie leicht erkennen können, ob ein Klick in deren Grenzen liegt.

Trommelmaschine

Wir beginnen mit Rechtecken und richten einige Basistypen von Formen ein. Jedes Formobjekt muss seine Grenzen kennen und in der Lage sein zu prüfen, ob sich ein Punkt darin befindet.

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

Für jeden neu hinzugefügten Formtyp ist eine Funktion in unserem Stage-Objekt erforderlich, um ihn als Trefferzone zu registrieren.

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

Bei Mausereignissen prüft jede Forminstanz, ob die übergebenen Maus-X- und -Y-Werte ein Treffer für sie sind, und geben „true“ oder „false“ zurück.

Wir können dem Element des Anzeigebereichs auch eine "aktive" Klasse hinzufügen, die den Mauszeiger in einen Zeiger verwandelt, wenn er über das Quadrat fährt.

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

Weitere Formen

Je komplizierter die Formen werden, desto komplexer wird die Berechnung, um zu ermitteln, ob sich ein Punkt darin befindet. Diese Gleichungen sind jedoch an vielen Stellen im Internet gut etabliert und ausführlich dokumentiert. Einige der besten JavaScript-Beispiele, die ich gesehen habe, stammen aus der Geometriebibliothek von Kevin Lindsey.

Glücklicherweise mussten wir bei der Entwicklung von JAM mit Chrome nie mehr als Kreise und Rechtecke verwenden und uns auf Kombinationen aus Formen und Ebenen setzen, um zusätzliche Komplexität zu bewältigen.

Trommelformen

Kreise

Um zu überprüfen, ob sich ein Punkt innerhalb einer kreisförmigen Trommel befindet, müssen wir eine kreisförmige Grundform erstellen. Obwohl es dem Rechteck sehr ähnlich ist, verfügt es über eigene Methoden zum Bestimmen der Grenzen und zum Überprüfen, ob sich der Punkt innerhalb des Kreises befindet.

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

Anstatt die Farbe zu ändern, wird durch Hinzufügen der Trefferklasse eine CSS3-Animation ausgelöst. Dank der Hintergrundgröße lässt sich das Bild der Trommel schnell skalieren, ohne ihre Position zu beeinflussen. Dazu müssen Sie die Präfixe anderer Browser hinzufügen (-moz, -o und -ms). Möglicherweise möchten Sie auch eine Version ohne Präfix hinzufügen.

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

Strings

Unsere GuitarString-Funktion nimmt eine Canvas-ID und ein Rect-Objekt und zeichnet eine Linie über die Mitte des Rechtecks.

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

Wenn es vibrieren soll, rufen wir die Strum-Funktion auf, um die Zeichenfolge in Bewegung zu setzen. Jeder Frame, den wir rendern, reduziert die Kraft, mit der er angespielt wurde, und erhöht den Zähler, der dazu führt, dass die Zeichenfolge hin- und herschwingt.

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

Schnitte und Anschläge

Unser Hit-Bereich für den String ist wieder eine Box. Durch Klicken in dieses Feld sollte die Zeichenfolgenanimation ausgelöst werden. Aber wer möchte schon auf eine Gitarre klicken?

Um Strumming hinzuzufügen, müssen wir die Schnittmenge der Zeichenfolgen und der Linie, die sich die Maus des Nutzers bewegt, überprüfen.

Um einen ausreichenden Abstand zwischen der vorherigen und der aktuellen Position der Maus zu erhalten, muss die Geschwindigkeit, mit der Mausbewegungsereignisse erfasst werden, verlangsamt werden. In diesem Beispiel setzen wir einfach eine Markierung, um Mausmove-Ereignisse 50 Millisekunden lang zu ignorieren.

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

Als Nächstes brauchen wir einen Schnittpunkt-Code, den Kevin Lindsey geschrieben hat, um zu sehen, ob sich die Mausbewegungslinie mit der Mitte unseres Rechtecks schneidet.

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

Schließlich fügen wir eine neue Funktion zum Erstellen eines Streichinstruments hinzu. Dadurch wird der neue Stage erstellt, mehrere Zeichenfolgen eingerichtet und der Kontext des Canvas ermittelt, auf dem der Canvas aufgebaut werden soll.

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

Als Nächstes positionieren wir die Trefferbereiche der Zeichenfolgen und fügen sie dem Stage-Element hinzu.

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

Schließlich durchläuft die Rendering-Funktion des StringInstrument alle unsere Zeichenfolgen und ruft deren Rendering-Methoden auf. Sie wird ständig ausgeführt, und zwar schnell, wie requestAnimationFrame es für angemessen hält. Weitere Informationen zu requestAnimationFrame finden Sie im Artikel requestAnimationFrame for Smart Animating von Paul Irish.

In einer echten Anwendung kann es sinnvoll sein, eine Markierung zu setzen, wenn keine Animation stattfindet, um das Zeichnen eines neuen Canvas-Frames zu stoppen.

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

Zusammenfassung

Ein gemeinsames Stage-Element für alle unsere Interaktionen zu haben, hat aber auch Nachteile. Diese Methode ist rechenintensiver und Cursorzeigerereignisse sind begrenzt, da für deren Änderung zusätzlicher Code erforderlich ist. Bei JAM mit Chrome funktionierten jedoch sehr gut die Vorteile der Möglichkeit, Mausereignisse von den einzelnen Elementen wegzu abstrahieren. So konnten wir mehr mit dem Design der Benutzeroberfläche experimentieren, zwischen Methoden zum Animieren von Elementen wechseln, Bilder mit einfachen Formen mit SVG ersetzen und die Trefferbereiche einfach deaktivieren.

Um die Drums and Stings in Aktion zu sehen, starte deine eigene JAM und wähle Standard Drums oder Classic Clean Electric Guitar aus.

Jam-Logo