So haben wir die Benutzeroberfläche optimiert
Einführung
JAM with Chrome ist ein webbasiertes Musikprojekt von Google. Mit JAM with Chrome können Nutzer aus aller Welt eine Band gründen und in Echtzeit im Browser jammen. DinahMoe hat die Grenzen dessen, was mit der Web Audio API von Chrome möglich ist, verschoben. Unser Team von Tool of North America hat die Benutzeroberfläche entwickelt, mit der Sie Ihren Computer wie ein Musikinstrument klimpern, trommeln und spielen können.
Unter der Creative Direction von Google Creative Lab hat der Illustrator Rob Bailey detaillierte Illustrationen für jedes der 19 Instrumente erstellt, mit denen JAM möglich ist. Anhand dieser Informationen haben der Interactive Director Ben Tricklebank und unser Designteam bei Tool eine einfache und professionelle Benutzeroberfläche für jedes Instrument erstellt.
Da jedes Instrument visuell einzigartig ist, haben der technische Leiter von Tool, Bartek Drozdz, und ich sie mithilfe von PNG-Bildern, CSS-, SVG- und Canvas-Elementen zusammengesetzt.
Viele der Instrumente mussten verschiedene Interaktionsmethoden verarbeiten (z. B. Klicks, Ziehen und Zupfen – alles, was man mit einem Instrument tun würde), während die Benutzeroberfläche mit der Sound-Engine von DinahMoe unverändert blieb. Wir haben festgestellt, dass wir mehr als nur die JavaScript-Funktionen „mouseup“ und „mousedown“ benötigen, um ein ansprechendes Spielerlebnis zu bieten.
Um mit all diesen Variationen zurechtzukommen, haben wir ein „Bühnenelement“ erstellt, das den spielbaren Bereich abdeckte und Klicks, Ziehen und Zupfen auf allen verschiedenen Instrumenten verarbeitete.
The Stage
Die Stage ist unser Controller, mit dem wir die Funktion für ein Instrument einrichten. Fügen Sie beispielsweise verschiedene Teile der Instrumente hinzu, mit denen die Nutzer interagieren werden. Wenn wir weitere Interaktionen hinzufügen (z. B. einen „Hit“), können wir sie dem Prototyp der Bühne 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
Als Erstes müssen wir die Mauskoordinaten im Browserfenster relativ zum Stage-Element übersetzen. Dazu mussten wir berücksichtigen, wo sich unsere Bühne auf der Seite befindet.
Da wir herausfinden müssen, wo sich das Element relativ zum gesamten Fenster befindet, nicht nur zum übergeordneten Element, ist es etwas komplizierter, als nur die offsetTop- und offsetLeft-Werte des Elements zu betrachten. Die einfachste Option ist die Verwendung von getBoundingClientRect, das die Position relativ zum Fenster angibt, genau wie Mausereignisse. 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};
}
};
Wenn getBoundingClientRect nicht vorhanden ist, haben wir eine einfache Funktion, die die Offsets einfach addiert und sich die Elementübergeordneten entlang der Kette bis zum Body hinaufarbeitet. Dann ziehen wir die Anzahl der Scroll-Vorgänge ab, um die Position relativ zum Fenster zu erhalten. Wenn Sie jQuery verwenden, ist die Funktion „offset()“ eine gute Lösung für die plattformübergreifende Ermittlung der Position. Sie müssen jedoch weiterhin den Wert abziehen, um den Text zu scrollen.
Wenn Sie auf der Seite scrollen oder die Größe ändern, kann sich die Position des Elements ändern. Wir können auf diese Ereignisse warten und die Position noch einmal prüfen. Diese Ereignisse werden bei einem normalen Scrollen oder Ändern der Größe häufig ausgelöst. In einer echten Anwendung sollten Sie daher die Häufigkeit der Überprüfung der Position begrenzen. Es gibt viele Möglichkeiten, dies zu tun. In HTML5 Rocks finden Sie einen Artikel zum Debouncing von Scroll-Ereignissen mit requestAnimationFrame, der hier gut funktioniert.
Bevor wir mit der Kollisionserkennung beginnen, gibt dieses erste Beispiel nur die relativen X- und Y-Werte aus, wenn die Maus im Bereich „Stage“ 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 Instrumentoberflächen komplex. Die Pads der Drum Machine sind nur einfache Rechtecke, sodass leicht erkannt werden kann, ob ein Klick innerhalb ihrer Grenzen liegt.
Beginnen wir mit Rechtecken und erstellen einige grundlegende Formen. Jedes Formobjekt muss seine Begrenzungen kennen und prüfen können, 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 neuen Formtyp, den wir hinzufügen, ist eine Funktion innerhalb unseres Stage-Objekts erforderlich, um es 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 X- und Y-Werte der Maus einen Treffer darstellen, und gibt „wahr“ oder „falsch“ zurück.
Wir können dem Bühnenelement auch die Klasse „aktiv“ hinzufügen, wodurch der Mauszeiger zu einem Zeiger wird, wenn der Mauszeiger auf das Quadrat bewegt wird.
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 sind, desto komplexer wird die Mathematik, um zu ermitteln, ob sich ein Punkt darin befindet. Diese Gleichungen sind jedoch gut etabliert und werden an vielen Stellen im Internet ausführlich dokumentiert. Einige der besten JavaScript-Beispiele, die ich gesehen habe, stammen aus der Geometriebibliothek von Kevin Lindsey.
Glücklicherweise mussten wir beim Erstellen von JAM mit Chrome nie über Kreise und Rechtecke hinausgehen. Wir haben Kombinationen von Formen und Ebenen verwendet, um zusätzliche Komplexität zu bewältigen.
Kreise
Um zu prüfen, ob sich ein Punkt innerhalb eines zylindrischen Körpers befindet, müssen wir eine kreisförmige Grundform erstellen. Obwohl er dem Rechteck sehr ähnlich ist, hat er eigene Methoden zum Bestimmen von Grenzen und zum Prü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 das Hinzufügen der Trefferklasse eine CSS3-Animation ausgelöst. Mit der Hintergrundgröße können wir das Bild der Trommel schnell skalieren, ohne die Position zu verändern. Sie müssen die Präfixe anderer Browser hinzufügen, damit sie damit funktionieren (-moz, -o und -ms). Sie können 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 Rechteckobjekt entgegen und zeichnet eine Linie durch die Mitte dieses 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 wir die Saite zum Vibrieren bringen möchten, rufen wir die Funktion „strum“ auf, um die Saite in Bewegung zu setzen. Bei jedem gerenderten Frame wird die Kraft, mit der die Saite angeschlagen wurde, leicht verringert und ein Zähler erhöht, der die Saite vor- und zurückschwingen lässt.
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;
};
Überschneidungen und Zupfmuster
Der Trefferbereich für den String wird wieder ein Rechteck sein. Wenn Sie in dieses Feld klicken, sollte die Stringanimation ausgelöst werden. Aber wer möchte auf eine Gitarre klicken?
Um das Zupfen hinzuzufügen, müssen wir die Schnittmenge zwischen dem Feld für die Saiten und der Linie prüfen, die die Maus des Nutzers zurücklegt.
Damit zwischen der vorherigen und der aktuellen Position der Maus genügend Abstand besteht, müssen wir die Häufigkeit der Mausbewegungsereignisse verlangsamen. In diesem Beispiel setzen wir einfach ein Flag, um mousemove-Ereignisse für 50 Millisekunden 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 benötigen wir den von Kevin Lindsey geschriebenen Code für Überschneidungen, um zu prüfen, ob die Mausbewegung die 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;
};
Zum Schluss fügen wir eine neue Funktion hinzu, um ein Streichinstrument zu erstellen. Es wird die neue Bühne erstellt, eine Reihe von Strings eingerichtet und der Kontext des Canvas abgerufen, auf dem gezeichnet wird.
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 Saiten 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 Renderfunktion des String-Instruments alle Strings und ruft ihre Rendermethoden auf. Es wird ständig ausgeführt, so schnell wie von requestAnimationFrame festgelegt. Weitere Informationen zu requestAnimationFrame finden Sie im Artikel requestAnimationFrame for smart animating von Paul Irish.
In einer echten Anwendung können Sie ein Flag setzen, wenn keine Animation ausgeführt wird, um das Zeichnen eines neuen Canvas-Frames zu beenden.
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 Interaktionen hat auch Nachteile. Die Berechnung ist komplexer und die Ereignisse des Cursors sind ohne zusätzlichen Code zur Änderung eingeschränkt. Bei JAM mit Chrome haben sich jedoch die Vorteile von Mausereignissen, die von den einzelnen Elementen abstrahiert werden können, sehr gut bewährt. So konnten wir mehr mit dem Interface-Design experimentieren, zwischen Methoden zum Animieren von Elementen wechseln, Bilder einfacher Formen durch SVG ersetzen und vieles mehr.
Wenn du die Drums und Stings in Aktion sehen möchtest, starte einen eigenen JAM und wähle die Standard-Drums oder die klassische cleane E-Gitarre aus.