Jak stworzyliśmy UI
Wstęp
JAM with Chrome to internetowy projekt muzyczny stworzony przez Google. JAM with Chrome pozwala ludziom z całego świata tworzyć zespoły i JAM w czasie rzeczywistym z poziomu przeglądarki. DinahMoe przesunęła granice tego, co było możliwe dzięki interfejsowi Web Audio API w Chrome. Nasz zespół z Tool of North America opracował interfejs do grania na komputerze, grania na bębnach i gier tak, jakby był to instrument muzyczny.
Zgodnie z kierunkiem kreatywnym Google Creative Lab ilustrator Rob Bailey stworzył skomplikowane ilustracje do każdego z 19 instrumentów dostępnych dla JAM. Opierając się na tych zadaniach, dyrektor ds. interaktywnego, Ben Tricklebank, i nasz zespół projektowy z narzędzia Tool stworzyli łatwy i profesjonalny interfejs dla każdego instrumentu.
Ponieważ każdy instrument jest wyjątkowy pod względem wizualnym, Bartek Drozdz, dyrektor techniczny firmy Tool, połączyłem je ze sobą, używając kombinacji obrazów PNG, CSS, SVG i Canvas.
Wiele instrumentów musiało obsługiwać różne metody interakcji (takie jak kliknięcia, przeciąganie czy struny – wszystkie czynności, jakie można by wykonać na instrumencie), nie zmieniając przy tym interfejsu z silnikiem dźwięku DinahMoe. Aby zapewnić komfortową rozgrywkę, potrzebujemy czegoś więcej niż tylko obsługi kursora myszy w górę i w dół w JavaScript.
Aby uwzględnić tę zmianę, stworzyliśmy element „Stage” obejmujący obszar gry, a także obsługę kliknięć, przeciągania i strumień w przypadku różnych instrumentów.
Scena
Stage to nasz kontroler, za pomocą którego konfigurujemy funkcje na instrumencie. Może to być na przykład dodawanie różnych części instrumentów, z którymi użytkownik będzie wchodzić w interakcje. W miarę dodawania kolejnych interakcji (np. „działania”), możemy je dodawać do prototypu Etapu.
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
};
Określanie pozycji i położenia myszy
Pierwsze zadanie polega na przetłumaczeniu współrzędnych myszy w oknie przeglądarki w odniesieniu do elementu Stage. Musieliśmy więc wziąć pod uwagę umiejscowienie naszego etapu na stronie.
Musimy ustalić położenie elementu względem całego okna, a nie tylko jego elementu nadrzędnego, dlatego czynność ta jest nieco bardziej skomplikowana niż przyglądanie się tylko elementom shiftTop i offLeft. Najłatwiejszym sposobem jest użycie metody getBoundingClientRect, która określa pozycję okna (tak jak w przypadku zdarzeń myszy), która jest również obsługiwana w nowszych przeglądarkach.
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};
}
};
Jeśli getBoundingClientRect nie istnieje, mamy prostą funkcję, która dodaje jedynie przesunięcia, przenosząc w górę łańcuch elementów nadrzędnych, aż dotrze do treści. Następnie odejmujemy zakres przewinięcia okna, aby uzyskać jego położenie względem okna. Jeśli korzystasz z biblioteki jQuery, funkcja shift() dobrze radzi sobie ze złożonością określania lokalizacji na różnych platformach, ale i tak trzeba odjąć liczbę przewiniętych elementów.
Po każdym przewinięciu strony lub zmianie rozmiaru strony możliwe jest, że pozycja elementu zmieniła się. Możemy nasłuchiwać tych zdarzeń i ponownie sprawdzić pozycję. Te zdarzenia są wywoływane wiele razy podczas typowego przewijania lub zmiany rozmiaru, więc w rzeczywistej aplikacji najlepiej jest ograniczyć częstotliwość ponownego sprawdzania pozycji. Można to zrobić na wiele sposobów, ale w ofercie HTML5 Rocks znajdziesz artykuł o odbijaniu zdarzeń przewijania za pomocą metody requestAnimationFrame.
Zanim zajmiemy się wykrywaniem trafień, w tym pierwszym przykładzie przy każdym ustawieniu kursora myszy w obszarze Stage zwracane są wartości x i 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);
};
Aby zacząć obserwować ruch myszy, tworzymy nowy obiekt Stage i przekazujemy do niego identyfikator elementu div, którego chcemy użyć jako sceny.
//-- Create a new Stage object, for a div with id of "stage"
var stage = new Stage("stage");
Proste wykrywanie trafień
W JAM z Chrome nie wszystkie interfejsy instrumentu są złożone. Są to proste prostokąty, dzięki czemu łatwo jest wykryć, czy kliknięcie mieści się w ich granicach.
Zaczynając od prostokątów, ustawimy kilka rodzajów kształtów podstawowych. Każdy obiekt kształtu musi znać swoje granice i mieć możliwość sprawdzenia, czy znajduje się w nim punkt.
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;
};
Każdy nowy typ kształtu, który dodamy, będzie wymagał funkcji w obiekcie Stage, która zarejestruje go jako strefę trafienia.
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;
};
W przypadku zdarzeń myszy każde wystąpienie kształtu będzie obsługiwać sprawdzanie, czy przekazane wartości x i y myszy są jego trafieniami, i zwrócą wartość prawda lub fałsz.
Do elementu sceny możemy też dodać klasę „active”, która zmieni kursor myszy w taki sposób, że będzie się pojawiać po najechaniu na niego kursorem.
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);
Więcej kształtów
W miarę jak kształty stają się coraz bardziej złożone, matematyka, aby sprawdzić, czy jakiś punkt znajduje się w nich, staje się coraz bardziej skomplikowana. Jednak te równania są już dobrze znane i bardzo szczegółowo udokumentowane w wielu miejscach w internecie. Niektóre z najlepszych przykładów JavaScriptu, jakie widziałem, pochodzą z biblioteki geometrii Kevina Lindseya.
Na szczęście przy tworzeniu JAM w Chrome nigdy nie musieliśmy wykroczyć poza okręgi i prostokąty, wykorzystując kombinacje kształtów i warstw, które radziły sobie z nadmierną złożonością.
Kółka
Aby sprawdzić, czy punkt znajduje się w okrągłym bębnie, trzeba utworzyć okrągły kształt podstawy. Chociaż jest on podobny do prostokąta, ma własne metody wyznaczania granic i sprawdzania, czy punkt znajduje się wewnątrz okręgu.
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;
};
Zamiast zmieniać kolor, dodanie klasy działania uruchamia animację CSS3. Rozmiar tła to dobry sposób na szybkie skalowanie obrazu bębna bez wpływu na jego pozycję. Aby korzystać z tej funkcji, musisz dodać prefiksy innych przeglądarek (-moz, -o i -ms). Możesz też dodać wersję bez prefiksu.
#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%;}
}
Ciąg znaków
Funkcja GuitarString pobierze identyfikator canvas i obiekt Rect, a następnie narysuje linię przez środek tego prostokąta.
function GuitarString(rect) {
this.x = rect.x;
this.y = rect.y + rect.height / 2;
this.width = rect.width;
this._strumForce = 0;
this.a = 0;
}
Jeśli ma on wibrować, wywołujemy funkcję strum, aby nadać ciągowi ruch. Każda wyrenderowana klatka zmniejszy siłę uderzenia i zwiększy licznik, który będzie powodować drgania struny.
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;
};
Skrzyżowania i instrumenty kosmiczne
Obszarem działania, w którym struny będzie struny, znów będzie pudło. Kliknięcie go powinno aktywować animację ciągu znaków. Ale kto by chciał kliknąć gitarę?
Aby dodać podział, należy zaznaczyć pole przecięcia tekstu z linią, którą porusza mysz użytkownika.
Aby uzyskać odpowiednią odległość między poprzednią a bieżącą pozycją myszy, musimy spowolnić tempo, z jakim są rejestrowane zdarzenia ruchu myszy. W tym przykładzie ustawimy po prostu flagę ignorowania zdarzeń ruchu kursora myszy przez 50 milisekund.
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);
};
Następnie będziemy polegać na kodzie przecięcia napisanym przez Kevina Lindseya, aby sprawdzić, czy linia ruchu myszy przecina środek prostokąta.
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;
};
Na koniec dodamy nową funkcję tworzącą instrument strunowy. Spowoduje to utworzenie nowego etapu, ustawienie kilku ciągów znaków i pozyskanie kontekstu obszaru roboczego, który będzie rysowany.
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;
}
Następnie ustawimy obszary działań struny i dodamy je do elementu Stage.
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);
}
};
Na koniec funkcja renderowania StringInstrument dokona pętli przez wszystkie ciągi i wywoła ich metody renderowania. Działa przez cały czas, szybko, gdy element requestAnimationFrame zostanie uznany za odpowiedni. Więcej informacji o requestAnimationFrame znajdziesz w artykule Paula Irlandii o requestAnimationFrame for smart animating.
W rzeczywistej aplikacji możesz ustawić flagę, gdy żadna animacja nie jest wyświetlana, aby zatrzymać rysowanie nowej ramki canvas.
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);
}
};
Podsumowanie
Wspólny element etapu, który obsługuje wszystkie nasze interakcje, musi też mieć swoje wady. Jest to bardziej złożone, a zdarzenia dotyczące wskaźnika kursora są ograniczone i można je zmieniać bez dodatkowego kodu. Jednak w przypadku JAM z Chrome korzyści płynące z odróżniania zdarzeń myszy od poszczególnych elementów sprawdzały się bardzo dobrze. Dzięki temu mogliśmy więcej eksperymentować z projektem interfejsu, przełączać się między metodami animowania elementów, używać SVG do zastępowania obrazów podstawowych kształtów, łatwego wyłączania obszarów działań itp.
Aby zobaczyć, jak działają perkusje i stingi, załóż własne urządzenie JAM i wybierz perkusję standardową lub klasyczną gitarę elektryczną.