Im Blick: Bubbles

IEBlog Deutsch

Blog des Internet Explorer-Entwicklerteams

Im Blick: Bubbles

  • Comments 0

In einem früheren Beitrag haben wir über die Leistungsverbesserungen von JavaScript gesprochen, die IE10 mit sich bringt. Heute haben wir Bubbles veröffentlicht. Eine ursprünglich von Alex Gavriolovs Simulation BubbleMark inspirierte Demo, um einige dieser Verbesserungen zu testen. Die aktuelle Version wurde stark erweitert, sodass sie von den neuen Funktionen der Webplattform profitiert und Merkmale aufweist, die man häufig bei HTML5-Spielen findet. In diesem Beitrag werfen wir einen Blick auf die zugrunde liegende Technik, um zu verstehen, wie die Demo aufgebaut ist, und untersuchen die Faktoren, die sich am stärksten auf ihre Leistung auswirken.

Screenshot der Bubbles-Demo, ausgeführt in IE10 im Metro-Stil unter der Windows 8 Release Preview
Bubbles-Demo, ausgeführt in IE10 im Metro-Stil unter der Windows 8 Release Preview

Aufbau der Demo

Die Demo besteht aus einer Anzahl animierter Blasen, die durch den Raum schweben. Die Grundlage bildet ein verhältnismäßig einfaches JavaScript-Physikmodul. In jedem Animationsframe berechnet das Physikmodul etwa 60-mal pro Sekunde die Position aller Blasen neu, passt die Geschwindigkeit jeder Blase anhand des Werts für die Schwerkraft an und berechnet Zusammenstöße. All diese Berechnungen beinhalten aufwändige Gleitkommamathematik. Jede Blase wird auf dem Bildschirm durch ein DOM-Bildelement dargestellt, auf das eine CSS-Transformation angewendet wird. Das Bild wird zunächst von seinem Ursprungsort aus verschoben und anschließend dynamisch skaliert, um den Effekt einer sich aufblähenden Blase zu erzeugen. In JavaScript wird jede Blase als Objekt mit Accessor-Eigenschaften dargestellt, wie in ECMAScript 5 eingeführt.

Hinter den schwebenden Blasen befindet sich ein großes Bild, das anfangs durch eine vollständig undurchsichtige Maske verdeckt wird, die in ein <Canvas>-Element gerendert wird. Bei jedem Zusammenstoß zweier Blasen wird ein Teil der verdeckenden Maske entfernt, indem ein Gaußfilter auf die Deckkraftkomponente der Maske angewendet wird, um einen diffusen Transparenzeffekt zu erzielen. Dieses Verfahren beinhaltet außerdem etliche Gleitkommamultiplikationen, die auf Elemente von typisierten Arrays angewendet werden, sofern dies vom Browser unterstützt wird.

Über den schwebenden Blasen liegt eine Touch-Oberfläche, die auf Fingereingabe reagiert, sofern dies vom Browser unterstützt wird, beziehungsweise auf Mausereignisse, wenn dies nicht der Fall ist. Als Reaktion auf eine Berührung wird eine simulierte magnetische Abstoßung auf die schwebenden Blasen angewendet, sodass diese auseinander getrieben werden.

Effiziente Animation

In IE10 haben wir Unterstützung für die requestAnimationFrame-API eingeführt. Für die Entwicklung von JavaScript-Apps mit Animation empfehlen wir weiterhin, unter Browsern, die dies unterstützen, diese API zu verwenden (statt SetTimeout oder SetInterval). Wie in einem früheren Beitrag über die effizienteste Nutzung Ihrer Hardware besprochen, ermöglicht Ihnen diese API, unter Vermeidung überflüssiger, für die Benutzer unsichtbarer Arbeit, die maximale vom verfügbaren Bildschirm unterstützte Framerate zu erzielen. Indem Sie mit der minimalen Anzahl von Rechenoperationen die optimale Anzeige für den Benutzer erzielen, sparen Sie außerdem Akkuleistung. Die IE10 Release Preview unterstützt diese API ohne Herstellerpräfix, jedoch wird die Version mit Präfix zugunsten der Kompatibilität mit früheren IE10 Preview-Versionen ebenfalls beibehalten. Die Bubbles-Demo verwendet diese API und greift auf SetTimeout zurück, wenn sie nicht verfügbar ist.

Demo.prototype.requestAnimationFrame = function () {

var that = this;

if (window.requestAnimationFrame)

this.animationFrameTimer =

window.requestAnimationFrame(function () { that.onAnimationFrame(); });

else

this.animationFrameTimer =

setTimeout(function () { that.onAnimationFrame(); }, this.animationFrameDuration);

}

 

Demo.prototype.cancelAnimationFrame = function () {

if (window.cancelRequestAnimationFrame)

window.cancelRequestAnimationFrame(this.animationFrameTimer);

else

clearTimeout(this.animationFrameTimer);

}

Konvertieren von DOM-Werten von Zeichenfolgen in Zahlen

JavaScript ist besonders flexibel und ermöglicht eine Reihe von automatischen Konvertierungen zwischen Werten unterschiedlichen Typs. Beispielsweise werden Zeichenfolgenwerte automatisch in Zahlenwerte konvertiert, wenn sie in Rechenoperationen verwendet werden. Überraschender Weise kann dies in modernen Browsern erstaunlich hohe Leistungseinbußen mit sich bringen. Typenspezifischer Code, der von aktuellen JavaScript-Compilern ausgegeben wird, ist sehr effizient bei der Berechnung von Werten bekannten Typs. Bei Werten mit unerwartetem Typ entsteht jedoch ein erheblicher Mehraufwand.

Wenn die Bubbles-Demo geladen wird, hat die numberOfBubbles-Eigenschaft einen Anfangswert von 100. In jedem Animationsframe wird die Position jeder Blase angepasst:

function Demo() {

this.numberOfBubbles = 100;

//...

}

 

Demo.prototype.moveBubbles = function(elapsedTime) {

for (var i = 0; i < this.numberOfBubbles; i++) {

this.bubbles[i].move(elapsedTime, this.gravity);

}

}

Wählt der Benutzer auf der Benutzeroberfläche einen anderen Wert aus, muss der Wert der numberOfBubbles-Eigenschaft entsprechend angepasst werden. Ein einfacher Eventhandler kann dies folgendermaßen erledigen:

Demo.prototype.onNumberOfBubblesChange = function () {

this.numberOfBubbles = document.getElementById("numberOfBubblesSelector").value;

//...

}

Aus dieser natürlich scheinenden Methode, die Benutzereingabe zu verarbeiten, ergibt sich im JavaScript-Anteil der Demo ein Mehraufwand von 10 %. Weil der Wert aus dem Dropdownmenü, der numberOfBubbles zugeordnet wird, eine Zeichenfolge (und keine Zahl) ist, muss er bei jeder Wiederholung der moveBubbles-Schleife für jeden Frame der Animation erneut in eine Zahl konvertiert werden.

Das zeigt, wie sinnvoll es ist, Werte aus dem DOM explizit in Zahlen zu konvertieren, bevor sie in Rechenoperationen verwendet werden. In JavaScript sind Werte von DOM-Eigenschaften in derRegel vom Typ „string“, und die automatische Konvertierung der Zeichenfolge in eine Zahl kann bei häufiger Wiederholung rasch zu Leistungseinbußen führen. Die folgende Methode zum Aktualisieren von numberOfBubbles entsprechend der Auswahl durch den Benutzer ist besser und wird in der Demo verwendet:

Demo.prototype.onNumberOfBubblesChange = function () {

this.numberOfBubbles = parseInt(document.getElementById("numberOfBubblesSelector").value);

//...

}

Arbeiten mit ES5-Accessor-Eigenschaften

ECMAScript 5-Accessor-Eigenschaften sind für Datenkapselung, berechnete Eigenschaften, Datenüberprüfung und Änderungsbenachrichtigung gut geeignet. In der Bubbles-Demo wird jedes Mal, wenn sich eine Blase aufbläht, der Radius angepasst und dadurch die berechnete radiusChanged-Eigenschaft festgelegt, die angibt, dass die Bildgröße der Blase geändert werden muss.

Object.defineProperties(Bubble.prototype, {

//...

radius: {

get: function () {

return this.mRadius;

},

set: function (value) {

if (this.mRadius != value) {

this.mRadius = value;

this.mRadiusChanged = true;

}

}

},

//...

});

In allen Browsern produzieren die Accessor-Eigenschaften verglichen mit den Dateneigenschaften Mehraufwand. Der genaue Mehraufwand variiert von Browser zu Browser.

Minimieren des Zugriffs auf das Canvas-Objekt ImageData

Es hat sich bewährt, die Zahl der DOM-Aufrufe in Schleifen des kritischen Leistungspfads zu minimieren. Wenn zum Beispiel die Bubbles-Demo die Position jeder Blase durch Suchen des entsprechenden Elements im Dokument aktualisieren würde (wie unten), hätte dies die Leistung beträchtliche Folgen für die Leistung.

Bubble.prototype.render = function () {

document.getElementById("bubble" + this.id).style.left = Math.round(this.x) + "px";

document.getElementById("bubble" + this.id).style.top = Math.round(this.y) + "px";

this.updateScale();

}

Stattdessen wird das entsprechende Element für jede Blase im „bubble“-Objekt in JavaScript einmal zwischengespeichert und dann in jedem Animationsframe direkt darauf zugegriffen.

Bubble.prototype.render = function () {

this.element.style.left = Math.round(this.x) + "px";

this.element.style.top = Math.round(this.y) + "px";

this.updateScale();

}

Weniger offensichtlich ist möglicherweise, dass bei der Verwendung von <canvas> beachtet werden muss, ähnlichen Mehraufwand zu vermeiden. Das durch das Aufrufen von canvas.getContext("2D").getImageData() erhaltene Objekt ist ebenfalls ein DOM-Objekt. Der unten gezeigte Code könnte in der Demo zum Zeichnen des Kollisionseffekts der Blasen auf der Canvas verwendet werden. In dieser Version wird imgData.data in jeder Wiederholung der Schleife gelesen, was einen DOM-Aufruf erfordert und einen erheblichen Mehraufwand erzeugt.

BubbleTank.prototype.renderCollisionEffectToCanvas = function(px, py) {

var imgData = this.canvasContext.getImageData(/*...*/)

//...

for (var my = myMin; my <= myMax; my++) {

for (var mx = mxMin; mx <= mxMax; mx++) {

var i = (mx + gaussianMaskRadius) + (my + gaussianMaskRadius) * gaussianMaskSize;

imgData.data[4 * i + 3] = 255 * occlusionMask[(px + mx) + (py + my) * canvasWidth];

}

}

this.canvasContext.putImageData(imgData, px - gaussianMaskRadius, py - gaussianMaskRadius);

}

Eine bessere Methode zum Aktualisieren der <canvas>-Bilddaten ist die Zwischenspeicherung der data-Eigenschaft wie im folgenden Codeausschnitt. Die data-Eigenschaft ist ein typisiertes Array (PixelArray), auf das mit JavaScript sehr effizient zugegriffen werden kann.

BubbleTank.prototype.renderCollisionEffectToCanvas = function(px, py) {

var imgData = this.canvasContext.getImageData(/*...*/)

var imgColorComponents = imgData.data;

//...

for (var my = myMin; my <= myMax; my++) {

for (var mx = mxMin; mx <= mxMax; mx++) {

var i = (mx + gaussianMaskRadius) + (my + gaussianMaskRadius) * gaussianMaskSize;

imgColorComponents[4 * i + 3] =

255 * occlusionMask[(px + mx) + (py + my) * canvasWidth];

}

}

this.canvasContext.putImageData(imgData, px - gaussianMaskRadius, py - gaussianMaskRadius);

}

Verwenden von typisierten Arrays zum Speichern von Gleitkommazahlen

Wir haben in IE10 Unterstützung für typisierte Arrays hinzugefügt. Bei der Verwendung von Gleitkommazahlen ist es von Vorteil, typisierte Arrays (Float32Array oder Float64Array) statt JavaScript-Arrays (Array) zu verwenden. JavaScript-Arrays können Elemente von jedem Typ enthalten, erfordern in der Regel jedoch, dass Gleitkommawerte dem Heap zugeordnet werden (Boxing), bevor sie dem Array hinzugefügt werden. Das wirkt sich auf die Leistung aus. Um in modernen Browsern eine gleichmäßig hohe Leistung zu erzielen, sollte Float32Array oder Float64Array zum Anzeigen der Absicht, Gleitkommawerte zu speichern, verwendet werden. Damit vermeiden Sie Heap-Boxing im JavaScript-Modul und können stattdessen auf andere Compiler-Optimierungen zurückgreifen, beispielsweise das Generieren von typenspezifizierten Verfahren.

BubbleTank.prototype.generateOcclusionMask = function() {

if (typeof Float64Array != "undefined") {

this.occlusionMask = new Float64Array(this.canvasWidth * this.canvasHeight);

} else {

this.occlusionMask = new Array(this.canvasWidth * this.canvasHeight);

}

this.resetOcclusionMask();

}

Das obige Beispiel zeigt, wie die Bubbles-Demo Float64Arrays zum Halten und Aktualisieren der Maske einsetzt, die zum Abdecken des Hintergrundbilds auf der Canvas angewendet wird. Wenn der Browser keine typisierten Arrays unterstützt, greift der Code auf normale Arrays zurück. Der Gewinn aus der Verwendung von typisierten Arrays in der Bubbles-Demo hängt von den Einstellungen ab. In einem mittelgroßen Fenster in IE10 verbessern typisierte Arrays die Gesamtframerate um etwa 10 %.

Zusammenfassung

In diesem Blogbeitrag haben wir die neue Bubbles-Demo erläutert und untersucht, wie diese von den deutlichen Verbesserungen der JavaScript-Ausführung in der IE10 Release Preview profitiert. Wir haben einige wichtige Techniken zum Erzielen guter Leistung bei animationsbasierten Apps hervorgehoben. Ausführliche technische Informationen zu den Änderungen an der JavaScript-Runtime (Chakra) in der IE10 Release Preview finden Sie in diesem früheren Beitrag. Wir freuen uns über die enorme Leistungssteigerung und die neuen Möglichkeiten der IE10 Release Preview. Wir hoffen, dass diese durch den Einsatz von Webstandards und -technologien die Entwicklung von noch faszinierenderen Apps ermöglichen.

– Andrew Miadowicz, Programmmanager, JavaScript