Kategorien
JavaScript & jQuery Programmierung Sonstige Programmiersprachen

ECMAScript 6 besser verstehen: Template Strings

Willkommen zum zweiten Teil unserer Artikelserie zu ECMAScript 6. Eines der Features, das ich am neuen Microsoft Edge, der neuen Browser Rendering Engine von Microsoft, am meisten mag, ist ihre umfangreiche Unterstützung von ECMAScript 6. In dieser Serie stelle ich einige der coolen Dinge vor, die man mit ECMAScript 6 tun kann, vor allem, wenn man große Web-Anwendungen schreibt.

ECMAScript 6 besser verstehen: Template Strings

Im ersten Teil habe ich euch in das Thema Klassen und Vererbung eingeführt. In diesem Artikel konzentriere ich mich auf Template Strings, ausgehend von meinen eigenen Erfahrungen beim Erstellen von Embedded Expressions.

Eine Lösung für das „Line Return“-Problem

Wenn ich an babylon.js arbeite, habe ich mit Code für Shader zu tun. Für ein besseres Verständnis würde ich sagen, man könnte diesen Code am ehesten als einen Haufen Text beschreiben, der wie C aussieht.

Ein Beispiel dafür findet sich hier.

Das Problem bei sehr langen Texten in JavaScript ist der Zeilenwechsel. Wie oft haben wir Dinge wie diese hier geschrieben?

var myTooLongString = "A long time ago, in a galaxy far," +
                      "far away...." +
                      "It is a period of civil war." +
                      "Rebel spaceships, striking" +
                      "from a hidden base, have won" +
                      "their first victory against" + 
                      "the evil Galactic Empire.";

Bei mehr als 200 Zeilen langem Code für Shaders wird das richtig anstrengend.

Glücklicherweise besitzt ECMAScript 6 das neue Template Strings-Feature. Neben anderen schönen Erleichterungen, die dieses Feature bietet, unterstützt ein Template String vor allem direkt mehrzeilige Strings:

var myTooLongString = `A long time ago, in a galaxy far
far away....
It is a period of civil war.
Rebel spaceships, striking
from a hidden base, have won
their first victory against
the evil Galactic Empire`;

Weil innerhalb eines Template String alle Zeichen von Bedeutung sind, kann man keine Leerzeichen an den Anfang der Zeilen einfügen.

Als JavaScript-Entwickler haben wir drei Arten, um Strings zu definieren:

Und was ist daran nun ein Template?

Die Unterstützung mehrzeiligen Codes ist nicht das einzige Feature von Template Strings. So kann man Template Strings auch dafür nutzen, um Platzhalter mit Variablenwerten zu ersetzen, wie ihr es z.B. mit printf in C/C++ oder string.Format in C# sicher schon getan habt:

var items = [];
items.push("banana");
items.push("tomato");
items.push("light saber");

var total = 100.5;

result.innerHTML = `You have ${items.length} item(s) in your basket for a total of $${total}`;

Dieser Code führt zu folgender Ausgabe:

You have 3 item(s) in your basket for a total of $100.5

Ganz praktisch oder?
Hier zur Erinnerung, wie das mit ECMAScript 5 ging:

result.innerHTML = "You have " + items.length + " item(s) in your basket for a total of $" + total;

Mit Tags noch einen Schritt weiter gehen

Im letzten Schritt der Spezifikation von Template Strings geht es darum, vor dem String selbst eine eigene Funktion einzufügen (custom function), um einen Template String mit einem „Tag“ zu erstellen:

var myUselessFunction = function (strings,...values) {
    var output = "";
    for (var index = 0; index < values.length; index++) {
        output += strings[index] + values[index];
    }

    output += strings[index]
    return output;
}

result.innerHTML = myUselessFunction `You have ${items.length} item(s) in your basket for a total of $${total}`;

Die Funktion dient hier dazu, sowohl Zugang zu den konstanten Teilen des Strings als auch zu den Variablen-Werten zu bekommen.
Im vorhergehenden Beispiel sehen Strings und Werte (values) wie folgt aus:

  • strings[0] = “You have “
  • values[0] = 3
  • strings[1] = “items in your basket for a total of $”
  • values[1] = 100.5
  • strings[2] = “”

Wie wir sehen, ist jeder Value[n] umgeben von konstanten Strings (strings[n] und strings[n + 1]).

Das gibt uns Kontrolle darüber, wie der endgültige Output-String aufgebaut ist. Im vorigen Beispiel haben wir nur das grundsätzliche Verhalten von Template Strings reproduziert. Wir können aber auch noch weiter gehen und dem String eine coole Verarbeitung (processing) mitgeben.

Hier zum Beispiel ist ein Stückchen Code, welches Strings blockiert, die versuchen, benutzerdefinierte DOM-Elemente einzuschleusen:

var items = [];
items.push("banana");
items.push("tomato");
items.push("light saber");

var total = "Trying to hijack your site 
"; var myTagFunction = function (strings,...values) { var output = ""; for (var index = 0; index < values.length; index++) { var valueString = values[index].toString(); if (valueString.indexOf("<") !== -1) { // Far more complex tests can be implemented here :) return "String analyzed and refused!"; } output += strings[index] + values[index]; } output += strings[index] return output; } result.innerHTML = myTagFunction `You have ${items.length} item(s) in your basket for a total of $${total}`;

"Getaggte" Template Strings sind für viele Anwendungen geeignet, wie z.B. Sicherheit, Ortsbestimmung, das Erstellen einer eigenen, domain-spezifischen Sprache und mehr.

Raw Strings

Tagging-Funktionen haben eine besondere Option, wenn sie auf konstante Strings zugreifen: Sie nutzen strings.raw um "unescaped" String-Values zu erhalten. Das heißt z.B. im Fall von “\n” wird das nicht als einzelnes Zeichen betrachtet, sondern tatsächlich als zwei Zeichen “\” und “n”.

Hauptziel ist es, uns zu ermöglichen, auf den String zugreifen zu können, so wie er eingegeben wurde:

var myRawFunction = function (strings,...values) {
    var output = "";
    for (var index = 0; index < values.length; index++) {
        output += strings.raw[index] + values[index];
    }

    output += strings.raw[index]

    console.log(strings.length, values.length);
    return output;
}

result.innerHTML = myRawFunction `You have ${items.length} item(s) in your basket\n.For a total of $${total}`;

Dieser Code führt zu folgender Ausgabe:

You have 3 item(s) in your basket\n. For a total of $100.5

Man kann auch eine neue String-Funktion nutzen: String.raw(). Diese bereits eingebaute Funktion führt genau das aus, was unser voriges Beispiel bereits getan hat:

result.innerHTML = String.raw `You have ${items.length} item(s) in your basket\n.For a total of $${total}`;

Fazit

Microsoft Edge und die aktuellen Versionen von Chrome (41+), Opera(28+) and Firefox (35+) unterstützen Template Strings. In welcher Breite ECMAScript 6 allgemein unterstützt wird, kannst du hier nachschlagen. Wer Anwendungen für das moderne Web erstellen will, für den sind Template Strings eine sinnvolle Sache.

Einen vollständigen Überblick zu den neuen Web-Standards und Features, die Microsoft Edge mitbringt – wie z.B. WebAudio – bietet die Liste von http://dev.modern.ie/platform/status.

Mehr Tipps, Tricks und Hilfen rund um JavaScript

Microsoft hält eine ganze Reihe kostenloser Lehrangebote zu zahlreichen Open Source JavaScript-Themen bereit – und seit der Veröffentlichung von Microsoft Edge tun wir in diesem Bereich noch sehr viel mehr. Auch von mir gibt es Tutorials:

Außerdem haben wir da noch die Schulungsreihe unseres Teams:

Praktisch sind zudem die folgenden kostenlosen Tools: Visual Studio Community, Azure Trial und Cross Browser Testing Tools für Mac, Linux oder Windows.

Dieser Artikel ist Teil der Web-Dev Tech-Series von Microsoft. Wir freuen uns Microsoft Edge (früher Project Spartan genannt) und seine neue Rendering Engine mit euch zu teilen. Kostenlose Virtual Machines oder Remote Testings für Mac, iOS, Android oder Windows gibt es hier: @ dev.modern.IE.

(dpe)

Kategorien
JavaScript & jQuery Programmierung

JavaScript: Mit Closure Space private Attribute erstellen

Vor einiger Zeit habe ich einen Angular Cloud Data Connector entwickelt, der es Angular-Entwicklern erlaubt, mithilfe von Webstandards, wie indexierten DB, Cloud-Daten zu nutzen, und zwar konkret Azure Mobile Service. Ich wollte Java-Entwicklern die Möglichkeit geben, private Attribute (private members) in ein Objekt einzubetten. Die Technik, die ich für diesen speziellen Fall benutze, nenne ich “closure space”. In diesem Tutorial zeige ich, wie man das für verschiedene Projekte nutzen kann und inwiefern Performance und Speicher der gängigen Browser davon beeinflusst werden.

Aber bevor wir richtig anfangen, werde ich noch kurz erklären, wozu man private Attribute braucht und auf welche Weise man sie auch “simulieren” kann.

Wozu brauchen wir private Attribute?

Wenn wir ein Objekt mit JavaScript erstellen, können wir Wertattribute (value members) festlegen. Um den Lese-/Schreibzugriff darauf zu steuern, müssen wir Zugriffsberechtigte wie folgt definieren:

var entity = {};

entity._property = "hello world";
Object.defineProperty(entity, "property", {
    get: function () { return this._property; },
    set: function (value) {
        this._property = value;
    },
    enumerable: true,
    configurable: true
});

Auf diese Weise haben wir die volle Kontrolle über den Lese-/Schreibzugriff. Problematisch ist jedoch, dass das _property Attribut noch zugänglich ist und direkt verändert werden kann.

Genau deshalb brauchen wir einen solideren Weg, um private Attribute festzulegen, die nur mithilfe der Funktionen des Objekts zugänglich sind.

Die Nutzung von Closure Space

Die Lösung heißt: Closure Space. Dieser Speicherraum wird vom Browser bereitgestellt, und zwar jedes Mal, wenn eine innere Funktion Zugang zu Variablen aus dem Bereich einer äußeren Funktion besitzt. Das ist manchmal eine ziemlich knifflige Angelegenheit, aber für unsere Zwecke ist die Lösung perfekt.

Um diese Funktion zu nutzen, ändern wir also den vorherigen Code in diesen hier:

var createProperty = function (obj, prop, currentValue) {
    Object.defineProperty(obj, prop, {
        get: function () { return currentValue; },
        set: function (value) {
            currentValue = value;
        },
        enumerable: true,
        configurable: true
    });
}

var entity = {};

var myVar = "hello world";
createProperty(entity, "property", myVar);

In diesem Beispiel hat die Funktion createProperty die Variable currentValue, welche von Get/Set-Funktionen erkannt wird. Diese Variable wird im Closure Space der Get- und Set-Funktionen gespeichert. Ausschließlich diese beiden Funktionen können nun die Variable currentValue sehen und aktualisieren! Mission erfüllt!

Fast zumindest. Der einzige Vorbehalt: Der Quellenwert (myVar) ist nach wie vor zugänglich. Deshalb zeige ich hier eine Version mit noch besserem Schutz:

var createProperty = function (obj, prop) {
    var currentValue = obj[prop];
    Object.defineProperty(obj, prop, {
        get: function () { return currentValue; },
        set: function (value) {
            currentValue = value;
        },
        enumerable: true,
        configurable: true
    });
}

var entity = {
    property: "hello world"
};

createProperty(entity, "property");

Mit dieser Methode wird sogar der Quellenwert unsichtbar. Nun haben wir die Aufgabe aber wirklich erfüllt!

Ein Blick auf die Performance

Jetzt schauen wir mal wie sich das Ganze auf die Performance auswirkt.

Ganz klar, Closure Space oder sogar Eigenschaften sind langsamer und aufwändiger als reine Variablen. Deshalb will ich mich in diesem Artikel auf den Unterschied zwischen dem üblichen Weg und der Closure Space-Technik konzentrieren.

Um nachzuweisen, dass die Nutzung von Closure Space verhältnismäßig effizienter ist als der herkömmliche Weg, habe ich diesen kleinen Benchmark-Code geschrieben:

<!DOCTYPE html>
  <html xmlns="http://www.w3.org/1999/xhtml">
  <head>
  <title></title>
  </head>
  <style>
  html {
  font-family: "Helvetica Neue", Helvetica;
  }
  </style>
  <body>
  <div id="results">Computing...</div>
  <script>
  var results = document.getElementById("results");
  var sampleSize = 1000000;
  var opCounts = 1000000;
  
 var entities = [];

 setTimeout(function () {
  // Creating entities
  for (var index = 0; index < sampleSize; index++) {
  entities.push({
  property: "hello world (" + index + ")"
  });
  }

 // Random reads
  var start = new Date().getTime();
  for (index = 0; index < opCounts; index++) {
  var position = Math.floor(Math.random() * entities.length);
  var temp = entities[position].property;
  }
  var end = new Date().getTime();

 results.innerHTML = "<strong>Results:</strong><br>Using member access: <strong>" + (end - start) + "</strong> ms";
  }, 0);

 setTimeout(function () {
  // Closure space =======================================
  var createProperty = function (obj, prop, currentValue) {
  Object.defineProperty(obj, prop, {
  get: function () { return currentValue; },
  set: function (value) {
  currentValue = value;
  },
  enumerable: true,
  configurable: true
  });
  }
  // Adding property and using closure space to save private value
  for (var index = 0; index < sampleSize; index++) {
  var entity = entities[index];

 var currentValue = entity.property;
  createProperty(entity, "property", currentValue);
  }

 // Random reads
  var start = new Date().getTime();
  for (index = 0; index < opCounts; index++) {
  var position = Math.floor(Math.random() * entities.length);
  var temp = entities[position].property;
  }
  var end = new Date().getTime();

 results.innerHTML += "<br>Using closure space: <strong>" + (end - start) + "</strong> ms";
  }, 0);

 setTimeout(function () {
  // Using local member =======================================
  // Adding property and using local member to save private value
  for (var index = 0; index < sampleSize; index++) {
  var entity = entities[index];

 entity._property = entity.property;
  Object.defineProperty(entity, "property", {
  get: function () { return this._property; },
  set: function (value) {
  this._property = value;
  },
  enumerable: true,
  configurable: true
  });
  }

 // Random reads
  var start = new Date().getTime();
  for (index = 0; index < opCounts; index++) {
  var position = Math.floor(Math.random() * entities.length);
  var temp = entities[position].property;
  }
  var end = new Date().getTime();

 results.innerHTML += "<br>Using local member: <strong>" + (end - start) + "</strong> ms";
  }, 0);

 </script>
  </body>
  </html>

Ich erstelle eine Million Objekte, jeweils mit einem Eigenschaftsattribut (property member). Dann lasse ich drei Tests laufen:

  • Führe 1 Million Direktzugriffe auf die Eigenschaft aus
  • Führe 1 Million Direktzugriffe auf die “Closure Space” Version aus
  • Führe 1 Million Direktzugriffe auf die reguläre Get/Set Version aus

Hier eine Tabelle und eine Grafik von den Ergebnissen:


Deutlich zu sehen ist, dass die Closure Space-Version in allen Fällen schneller arbeitet als die reguläre Version. Abhängig vom Browser kann die Optimierung sehr eindrucksvoll ausfallen.

Die Performance von Chrome war weniger gut, als ich sie erwartet hatte. Um einen Bug auszuschließen, fragte ich daher bei Google nach, was die Ursache dafür sein könnte. Nebenbei bemerkt: Wer ausprobieren will, wie dieser Test für Microsoft Edge ausgeht – der neue Browser von Microsoft, der in Windows 10 enthalten ist – der kann ihn hier herunterladen.

Wie auch immer, wenn wir genau hinschauen, sehen wir, dass die Nutzung von Closure Space oder sogar einer Eigenschaft zehnmal langsamer funktioniert, als der direkte Zugriff auf ein Attribut. Das nur als Warnung, das Ganze mit Bedacht anzuwenden.

Speicherbedarf

Wir sollten auch prüfen, ob diese Technik nicht zu viel Speicherkapazität bindet. Um also auch dafür einen Benchmark-Test durchzuführen, habe ich diese drei Stückchen Code geschrieben:

Referenz Code

var sampleSize = 1000000;

var entities = [];

// Creating entities
for (var index = 0; index < sampleSize; index++) {
    entities.push({
        property: "hello world (" + index + ")"
    });
}

Der übliche Weg

var sampleSize = 1000000;

var entities = [];

// Adding property and using local member to save private value
for (var index = 0; index < sampleSize; index++) {
    var entity = {};

    entity._property = "hello world (" + index + ")";
    Object.defineProperty(entity, "property", {
        get: function () { return this._property; },
        set: function (value) {
            this._property = value;
        },
        enumerable: true,
        configurable: true
    });

    entities.push(entity);
}

Closure Space-Version

var sampleSize = 1000000;

var entities = [];

var createProperty = function (obj, prop, currentValue) {
    Object.defineProperty(obj, prop, {
        get: function () { return currentValue; },
        set: function (value) {
            currentValue = value;
        },
        enumerable: true,
        configurable: true
    });
}

// Adding property and using closure space to save private value
for (var index = 0; index < sampleSize; index++) {
    var entity = {};

    var currentValue = "hello world (" + index + ")";
    createProperty(entity, "property", currentValue);

    entities.push(entity);

Dann ließ ich alle drei Codes durchlaufen und startete den integrierten Memory Profiler (als Beispiel nutzte ich hier die F12 Tools):

Auf meinem Computer erhielt ich daraufhin folgende Ergebnisse:

Im Vergleich von Closure Space und dem üblichen Weg hatte nur Chrome leicht bessere Werte für die Closure Space-Version. IE11 und Firefox brauchten ein bisschen mehr Speicher, aber generell waren alle Browser relativ vergleichbar. Als User wird man da kaum einen Unterschied bemerken.

Mehr Tipps, Tricks und Hilfen rund um JavaScript

Microsoft hält eine ganze Reihe kostenloser Lehrangebote zu zahlreichen Open Source JavaScript-Themen bereit – und seit der Veröffentlichung von Microsoft Edge tun wir in diesem Bereich noch sehr viel mehr. Auch von mir gibt es Tutorials:

Außerdem haben wir noch die Schulungsreihe unseres Teams:

Praktisch sind zudem die folgenden kostenlosen Tools: Visual Studio Community, Azure Testversion und Cross Browser Testing Tools für Mac, Linux oder Windows.

Fazit

Wie wir gesehen haben, können wir mit Closure Space sehr gut wirklich private Daten erstellen. Das kostet uns ein bisschen Speicherplatz, aber meiner Meinung nach ist das vertretbar (und bringt uns schließlich gleichzeitig eine bessere Performance, als wenn wir den üblichen Weg nutzen würden).

Wer Fragen zu diesem Artikel hat, kann mich gern auf Twitter kontaktieren: @deltakosh

Und wer es gleich selbst versuchen will, kann den gesamten verwendeten Code hier finden. Es gibt außerdem eine gute Kurzeinweisung in die Azure Mobile Services, und zwar hier.

Dieser Artikel ist Teil der Web-Dev Tech-Series von Microsoft. Wir freuen uns Microsoft Edge (früher Project Spartan genannt) und seine neue Rendering Engine mit euch zu teilen. Kostenlose Virtual Machines oder Remote Testings für Mac, iOS, Android oder Windows gibt es hier: @ dev.modern.IE.

(dpe)

Kategorien
JavaScript & jQuery Programmierung

JavaScript: Bessere Auswertung der Performance durch User Marks

Wer an fortgeschrittenem JavaScript-Code arbeitet, wie zum Beispiel einer 3D-Engine, wird sich früher oder später fragen, wie sich dieser Code optimieren lässt und wie viel Zeit man mit bestimmten Abschnitten des Codes verbringen sollte. In diesem Tutorial stelle ich einige Tools vor, die uns sagen, wie gut die Performance unseres Codes tatsächlich ist und wie wir Anwendermarkierungen (User Marks) im Memory Graph am besten nutzen, um die Leistungsfähigkeit des Codes auszuwerten.

JavaScript: Bessere Auswertung der Performance durch User Marks

Sie wollen einen schnellen Überlick zum Thema? Dann gleich mal dieses Video ansehen.

Wer Fragen zu diesem Artikel hat, kann mich gern auf Twitter kontaktieren (@deltakosh)

Profiler gesucht?

Dazu fällt mir gleich der integrierte Profiler ein, der in den Updates zu den Internet Explorer F12 Dev Tools enthalten ist — dessen Weiterentwicklung, die auch für Microsoft Edge verfügbar sind. Aber natürlich sind andere, ähnlich funktionale Tools ebenso geeignet.

Wer das mal mit Android, iOS oder Mac OS ausprobieren will, kann auch remote.IE nutzen, um eine Windows 10 Technical Preview-Instanz in wenigen Minuten zum Laufen zu bringen. Dann einfach den Internet Explorer “e” öffnen (eine temporäre Client Shell mit konfigurierter neuer Rendering Engine von Microsoft Edge), “F12” klicken und daraufhin sollte das Folgende zu sehen sein:

Hierbei ist zu beachten, dass bei den neuen F12 Tools, die Teil des Windows 10 Technical Previews sind, der Profiler im Fenster „UI responsiveness“ zu finden ist:

Schauen wir uns nun andere Optionen an, die uns Erkenntnisse über die Performance unseres Codes vermitteln können.

console.time

Hier muss man einfach nur console.time() und console.timeEnd() vor bzw. nach dem Teil des Codes aufrufen, der getestet werden soll. Als Ergebnis erhält man einen String in der Konsole, der die Zeit zwischen time und timeEnd anzeigt.

Natürlich ist das recht einfach und kann auch leicht emuliert werden, aber mir persönlich gefällt besonders die Unkompliziertheit dieser Funktion.

Und es kommt noch besser: Man kann einen String angeben, um eine Bezeichnung für die Messwerte zu bekommen.

Dies habe ich zum Beispiel für Babylon.js getan:

console.time("Active meshes evaluation");
this._evaluateActiveMeshes();
console.timeEnd("Active meshes evaluation");

Diese Art von Code ist in allen wichtigen Funktionen zu finden und wenn man Performance Logging aktiviert hat, erhält man jede Menge interessanter Informationen:

Hinweis: Das Rendering von Text in die Konsole kann eine Menge CPU-Power beanspruchen.

Da diese Funktion an sich keine Standardfunktion ist, hat mich die Kompatibilität mit unterschiedlichen Browsern umso mehr überzeugt. Sie wird von Chrome, Firefox, IE, Opera und Safari unterstützt.

Performance Object

Wer es gern etwas visueller mag, dem sei performance object empfohlen. Neben anderen interessanten Features, um die Performance einer Webseite zu messen, gibt es die Funktion mark, die eine Anwendermarkierung (user mark) aussendet.

Eine Anwendermarkierung verbindet einen Namen mit einem Zeitwert. Teile des Codes können mit dieser API gemessen werden, um genaue Werte zu erhalten. Es gibt einen tollen Artikel über diese API von Aurelio de Rosa bei SitePoint.

An dieser Stelle hier geht es mir nur darum, diese API zu benutzen, um konkrete Anwendermarkierungen im UI Responsiveness Screen bildlich darzustellen:

Dieses Tool ermöglicht es, eine Session zu erfassen und festzustellen, wie stark dabei die CPU beansprucht wird:

Wir können dann einen konkreten Frame vergrößert darstellen, indem wir den Eintrag “Animation frame callback” auswählen und dort nach einem Rechtsklick “filter to event” wählen.

Der gewählte Frame wird daraufhin gefiltert:

Dank des neuen F12 Tools können wir nun zu den JavaScript Call Stacks wechseln, um mehr Details zu diesem Event zu bekommen:

Es ist an dieser Stelle jedoch etwas schwierig, zu erkennen, wie der Code während des Events tatsächlich umgesetzt wurde.

Hier jedoch helfen uns die User Marks aus der Klemme. Wir können unsere eigenen Marker hinzufügen und sind dann in der Lage einen Frame aufzuteilen. Dort können wir dann analysieren, welches Feature am aufwändigsten ist.

performance.mark("Begin of something…just now!");

Wer ein eigenes Framework erstellt, erhält daduch außerdem die sehr praktische Möglichkeit, den eigenen Code mit Messwerten zu belegen:

performance.mark("Active meshes evaluation-Begin");
this._evaluateActiveMeshes();
performance.mark("Active meshes evaluation-End");
performance.measure("Active meshes evaluation", "Active meshes evaluation-Begin", "Active meshes evaluation-End");

Schauen wir uns doch mal an, was uns das bei babylon.js bringt, hier für die Instanz mit der “V8”-Szene:

Wir können nun babylon.js auffordern, für uns User Marks und Messwerte auszusenden. Dazu benutzen wir den debug layer:

Mithilfe des UI Responsiveness Analyzer erhalten wir diese Ansicht:

Deutlich zu sehen: Die User Marks werden oberhalb des Events angezeigt (die orangefarbenen Dreiecke), ebenso die Bereiche für alle Messungen:

Auf diese Weise lässt sich in diesem Beispiel sehr leicht bestimmen, dass die Abschnitte [Render targets] und [Main render] am aufwändigsten operieren.

Der vollständige Code, den babylon.js nutzt, um den Usern zu ermöglichen, die Performance verschiedener Funktionen zu testen, sieht so aus:

  Tools._StartUserMark = function (counterName, condition) {
    if (typeof condition === "undefined") { condition = true; }
    if (!condition || !Tools._performance.mark) {
        return;
    }
    Tools._performance.mark(counterName + "-Begin");
};

Tools._EndUserMark = function (counterName, condition) {
    if (typeof condition === "undefined") { condition = true; }
    if (!condition || !Tools._performance.mark) {
        return;
    }
    Tools._performance.mark(counterName + "-End");
    Tools._performance.measure(counterName, counterName + "-Begin", counterName + "-End");
};

Tools._StartPerformanceConsole = function (counterName, condition) {
    if (typeof condition === "undefined") { condition = true; }
    if (!condition) {
        return;
    }

    Tools._StartUserMark(counterName, condition);

    if (console.time) {
        console.time(counterName);
    }
};

Tools._EndPerformanceConsole = function (counterName, condition) {
    if (typeof condition === "undefined") { condition = true; }
    if (!condition) {
        return;
    }

    Tools._EndUserMark(counterName, condition);

    if (console.time) {
        console.timeEnd(counterName);
    }
};

Dank der F12 Tools und den User Marks erhalten wir also ein sehr nützliches Dashboard, das uns zeigt, wie effizient verschiedene Teile des Codes zusammenarbeiten.

Mehr Tipps, Tricks und Hilfen rund um JavaScript

Microsoft hält eine ganze Reihe kostenloser Lehrangebote zu zahlreichen Open Source JavaScript-Themen bereit – und seit der Veröffentlichung von Microsoft Edge tun wir in diesem Bereich noch sehr viel mehr. Auch von mir gibt es Tutorials:

Außerdem haben wir noch die Schulungsreihe unseres Teams:

Praktisch sind zudem die folgenden kostenlosen Tools: Visual Studio Community, Azure Testversion sowie Cross Browser Testing Tools für Mac, Linux oder Windows.

Dieser Artikel ist Teil der Web-Dev Tech-Series von Microsoft. Wir freuen uns Microsoft Edge (früher Project Spartan genannt) und seine neue Rendering Engine mit euch zu teilen. Kostenlose Virtual Machines oder Remote Testings für Mac, iOS, Android oder Windows gibt es hier: @ dev.modern.IE.

(dpe)

Kategorien
JavaScript & jQuery Programmierung

Licht und Schatten: Shader gestalten mit HTML5 und WebGL

In letzter Zeit war viel von babylon.js die Rede und vor kurzem haben wir babylon.js v2.0 veröffentlicht, das eine 3D Sound-Positionierung (mit WebAudio) sowie volumetrische Lichtstreuungen ermöglicht.

shaders-teaser_DE

Wer die Bekanntgabe von Version 1.0 verpasst hat, kann sich die Keynote dazu hier noch mal ansehen und direkt zum Abschnitt 2:24-2:28 gehen.

In dieser Keynote zeigen Steven Guggenheimer und John Shewchuk in einer Demo, wie die Unterstützung für Oculus Rift in Babylon.js eingebaut wurde. Und eines der wichtigsten Elemente dieser Demo war wiederum unsere Arbeit an einem speziellen Shader, der Objektive simulieren kann, wie in diesem Bild zu sehen ist:

image

Zusammen mit Frank Olivier und Ben Constable sprach ich auf der Build selbst in einer Session über die Grafik im Internet Explorer und in Babylon.js.

Daraus ergibt sich für mich auch gleich die Frage, die bei Diskussionen um babylon.js immer wieder auftaucht: Was ist mit Shadern eigentlich genau gemeint? Ein guter Grund also, heute mal zu zeigen, wie Shader funktionieren.

Die Theorie

Bevor wir mit dem Ausprobieren anfangen, sollten wir erstmal versuchen zu verstehen, was dabei im System genau passiert.

Wenn wir über mittels Hardware beschleunigtes 3D reden, geht es um zwei CPUs: die zentrale CPU und die GPU. Die GPU ist letztlich eine Art extrem spezialisierte CPU.

Die GPU ist eine Zustandsmaschine, die man mit Hilfe der CPU einrichtet. So wird die CPU zum Beispiel die GPU so konfigurieren, dass diese Linien rendert statt Dreiecke. Oder sie wird den Grad der Transparenz festlegen usw.

Wenn alle Zustände definiert sind, wird die CPU bestimmen, was genau wiedergegeben wird (die Geometrie, welche aus einer Liste von Punkten besteht (genauer: Scheitel- oder Eckpunkte, die im sogenannten Vertex Buffer gespeichert werden) und einer Index-Liste (die Flächen (oder Dreiecke), die im sogenannten Index Buffer gespeichert sind)).

Im letzten Schritt wird die CPU dann festlegen, wie die Geometrie schließlich genau gerendert wird. Und für diese spezielle Aufgabe definiert die CPU bestimmte Shaders für die GPU. Shaders sind letztlich ein Stück Code, das die GPU für alle Scheitel- bzw. Eckpunkte und für jeden Pixel ausführt, den es zu rendern gilt.

Zuerst ein paar Begriffserklärungen: Einen Scheitel- oder Eckpunkt (Englisch: Vertex) kann man sich als “Punkt” in einer 3D Umgebung vorstellen im Unterschied zu diesem Punkt in einer 2D Umgebung.

Es gibt zwei Arten von Shadern: den Vertex Shader und den Pixel (oder Fragment) Shader.

Grafik-Pipeline

Bevor wir richtig ins Thema Shaders einsteigen, noch kurz ein paar Grundlagen. Um Pixel darzustellen, nutzt die GPU die von der CPU vorgegebene Geometrie und tut das Folgende:

  • Mithilfe des Index Buffer werden drei Eckpunkte erfasst, um ein Dreieck festzulegen: Der Index Buffer enthält eine Liste von Vertex Indizes. Das heißt, jeder Eintrag im Index Buffer ist die Nummer eines Eckpunkts im Vertex Buffer. Auf diese Weise werden Duplikate von Eckpunkten vermieden. Im folgenden Beispiel besteht der Index Buffer aus einer Liste mit 2 Flächen: [1 2 3 1 3 4]. Die erste Fläche enthält Eckpunkt 1, Eckpunkt 2 und Eckpunkt 3. Die zweite Fläche enthält Eckpunkt 1, Eckpunkt 3 und Eckpunkt 4. In dieser Geometrie gibt es also vier Eckpunkte:

image

  • Der Vertex Shader wird auf jeden Eckpunkt des Dreiecks angewendet. Wichtigste Aufgabe des Vertex Shaders ist die Erstellung eines Pixels für jeden Eckpunkt (die Projektion des 3D Eckpunkts auf einem 2D Bildschirm):

image

  • Die GPU benutzt diese drei Pixel (die ein zweidimensionales Dreieck auf dem Bildschirm definieren), um alle diesem Pixel zugeordneten Werte zu interpolieren (zumindest die Position). Der Pixel Shader wird dann auf jeden Pixel der im 2D-Dreieck enthalten ist angewendet, um für jeden Pixel eine Farbe zu erstellen:

image

  • Dieser Prozess wird für jede Fläche durchgeführt, die der Index Buffer vorgibt.

Aufgrund seiner parallelen Eigenschaften kann die GPU diesen Schritt für viele Flächen gleichzeitig durchführen und dadurch entsprechend gute Ergebnisse hervorbringen.

GLSL

Wie wir gerade gesehen haben, benötigt die GPU zwei Shader, um Dreiecke darstellen zu können: den Vertex Shader und den Pixel Shader. Diese Shader werden mit GLSL (Graphics Library Shader Language) geschrieben. Sie sieht so aus wie C.

Für den Internet Explorer 11 haben wir ein Compiler-Programm entwickelt, um GLSL in HLSL (High Level Shader Language) umzuwandeln, was die Programmiersprache für Shader in DirectX 11 ist. Auf diese Weise gewährleistet der IE11 die Sicherheit des Shader Codes (keiner will schließlich seinen Rechner neu starten, um WebGL zu nutzen):

image

Hier ist ein Beispiel für einen häufig verwendeten Vertex Shader:

precision highp float;

// Attributes
attribute vec3 position;
attribute vec2 uv;

// Uniforms
uniform mat4 worldViewProjection;

// Varying
varying vec2 vUV;

void main(void) {
    gl_Position = worldViewProjection * vec4(position, 1.0);

    vUV = uv;
}

Struktur eines Vertex Shader

Ein Vertex Shader enthält Folgendes:

  • Attributes: Ein Attribut definiert einen Teil eines Eckpunkts. Standardmäßig sollte ein Eckpunkt zumindest die Position beinhalten (a vector3:x, y, z). Aber als Entwickler kann man auch weitere Daten hinzufügen. So gibt es im vorherigen Beispiel den vector2 namens uv (Koordinaten für eine Textur, mit der eine 2D-Textur auf ein 3D-Objekt angewendet werden kann.)
  • Uniforms: Das sind Variablen, die vom Shader benutzt und von der CPU festgelegt werden. Im Beispiel gibt es davon nur eine und zwar eine Matrix, welche die Position des Eckpunkts (x, y, z) auf den Bildschirm projiziert (x, y)
  • Varying: Varying-Variablen sind Werte, die vom Vertex Shader generiert und an den Pixel Shader übermittelt werden. Im Beispiel überträgt der Vertex Shader einen vUV-Wert (eine einfache Kopie von uv) an den Pixel Shader. Konkret bedeutet das, der Pixel hat eine definierte Position und Koordinaten für die Textur. Diese Werte werden von der GPU interpoliert und vom Pixel Shader verwendet.
  • Main: Diese Funktion ist der Code, den die GPU für jeden Eckpunkt ausführt. Er muss mindestens einen Wert für die gl_position erzeugen (die Position des aktuellen Eckpunkts auf dem Bildschirm).

Der Vertex Shader in unserem Beispiel ist ziemlich einfach aufgebaut. Er erzeugt eine Systemvariable (beginnend mit gl_) namens gl_position, um die Position des damit verbundenen Pixels festzulegen und er legt die Varying-Variable vUV fest.

Die Magie einer Matrix

In unserem Shader-Beispiel gibt es die Matrix worldViewProjection. Wir brauchen sie, um die Position des Eckpunkts auf die gl_position-Variable zu projizieren. Schön und gut, aber woher bekommen wir den Wert für diese Matrix? Nun, es ist eine Uniform-Variable, man muss sie also von der CPU aus definieren (mithilfe von JavaScript).

Das gehört zweifellos zu den eher komplizierten Aufgaben wenn man sich mit 3D beschäftigt. Man muss etwas von komplexer Mathematik verstehen (oder eine 3D Engine wie babylon.js benutzen, mehr dazu später).
Die Matrix worldViewProjection ist eine Kombination von drei verschiedenen Matrizen:

image

Mit der entstandenen Matrix können 3D-Eckpunkte in 2D-Pixel umgewandelt werden, unter Berücksichtigung der Perspektive und allen Werten bezüglich der Position/des Maßstabs/einer Rotation des Objekts.

Diese Matrix zu erstellen und sie aktuell zu halten – das ist deine Verantwortung als-3D Entwickler.

Zurück zu den Shadern

Nachdem der Vertex Shader auf alle Eckpunkte angewendet wurde (also dreimal), haben wir drei Pixel mit der korrekten gl_position und einem vUV-Wert. Die GPU wird daraufhin diese Werte auf alle Pixel, die das Dreieck enthält (und aus denen es besteht), interpolieren.

Danach wird sie bei jedem Pixel den Pixel Shader anwenden:

precision highp float;
varying vec2 vUV;
uniform sampler2D textureSampler;

void main(void) {
    gl_FragColor = texture2D(textureSampler, vUV);
}

Die Struktur von Pixel (oder Fragment) Shader

Die Struktur eines Pixel Shaders gleicht der eines Vertex Shaders:

  • Varying: Varying Variablen sind Werte, die vom Vertex Shader generiert und an den Pixel Shader übermittelt werden. Im Beispiel erhält der Pixel Shader einen vUV-Wert vom Vertex Shader.
  • Uniforms: Das sind Variablen, die vom Shader benutzt und von der CPU festgelegt werden. Im Beispiel gibt es nur einen Sampler, ein Werkzeug um die Farben von Texturen zu erkennen.
  • Main: Diese Funktion ist der Code, den die GPU für jeden Eckpunkt ausführt. Er muss mindestens einen Wert für die gl_FragColor erzeugen (die Farbe des aktuellen Pixels).

Dieser Pixel Shader ist sehr einfach: Er erkennt die Farbe der Textur mithilfe der Texturkoordinaten des Vertex Shaders (der sie von den Eckpunkten übermittelt bekam).
Und so sieht der Shader dann aus:

(Der vollständige Code ist auf meinem Blog nachzulesen)

Um dieses Ergebnis zu erzielen, muss man sich mit EINER MENGE WebGL Code herumschlagen. WebGL ist auf der einen Seite ein echt leistungsstarkes Tool, andererseits aber ein API auf niedriger Stufe, bei dem alles selbst gemacht werden muss: Von der Erzeugung der Buffer bis zur Definition der Strukturen von Eckpunkten. Dazu kommen die Berechnung, die Festlegung der Zustände, das Laden der Texturen usw…

Zu schwer? BABYLON.ShaderMaterial kommt zu Hilfe

An dieser Stelle denken sicher viele: Klar, Shader sind echt cool, aber WebGL und der ganze mathematische Hintergrund – dazu habe ich keine Lust, das ist mir echt zu komplex.

Auf jeden Fall! Das ist nur zu verständlich und genau deshalb habe ich ja Babylon.js gebaut.

Was nun folgt, ist der Code auf dem die vorherige Demo mit der rollenden Kugel läuft. Als erstes brauchen wir eine einfache Webseite:

<!DOCTYPE html>
  <html>
  <head>
  <title>Babylon.js</title>
  <script src="Babylon.js"></script>

  <script type="application/vertexShader" id="vertexShaderCode">
  precision highp float; 

  // Attributes
  attribute vec3 position;
  attribute vec2 uv;
        // Uniforms
  uniform mat4 worldViewProjection;
        // Normal
  varying vec2 vUV;
        void main(void) {
  gl_Position = worldViewProjection * vec4(position, 1.0);
        vUV = uv;
  }
  </script>

  <script type="application/fragmentShader" id="fragmentShaderCode">
  precision highp float; 
  varying vec2 vUV;
        uniform sampler2D textureSampler;
        void main(void) {
  gl_FragColor = texture2D(textureSampler, vUV);
  }
  </script>
    <script src="index.js"></script>
  <style>
  html, body {
  width: 100%;
  height: 100%;
  padding: 0;
  margin: 0;
  overflow: hidden;
  margin: 0px;
  overflow: hidden;
  }
        #renderCanvas {
  width: 100%;
  height: 100%;
  touch-action: none;
  -ms-touch-action: none;
  }
  </style>
  </head>
  <body>
  <canvas id="renderCanvas"></canvas>
  </body>
  </html>

Hinweis: Die Shader werden hier durch <script> Tags definiert. In Babylon.js können sie zudem in unterschiedlichen Dateien definiert werden (.fx files).

Babylon.js kann hier heruntergeladen werden oder auf unserem GitHub Repository. Um Zugang zum BABYLON.StandardMaterial zubekommen, wird die Version 1.11 oder höher benötigt.

Der primäre JavaScript Code sieht dann wie folgt aus:

"use strict";

document.addEventListener("DOMContentLoaded", startGame, false);

function startGame() {
    if (BABYLON.Engine.isSupported()) {
        var canvas = document.getElementById("renderCanvas");
        var engine = new BABYLON.Engine(canvas, false);
        var scene = new BABYLON.Scene(engine);
        var camera = new BABYLON.ArcRotateCamera("Camera", 0, Math.PI / 2, 10, BABYLON.Vector3.Zero(), scene);

        camera.attachControl(canvas);

        // Creating sphere
        var sphere = BABYLON.Mesh.CreateSphere("Sphere", 16, 5, scene);

        var amigaMaterial = new BABYLON.ShaderMaterial("amiga", scene, {
            vertexElement: "vertexShaderCode",
            fragmentElement: "fragmentShaderCode",
        },
        {
            attributes: ["position", "uv"],
            uniforms: ["worldViewProjection"]
        });
        amigaMaterial.setTexture("textureSampler", new BABYLON.Texture("amiga.jpg", scene));

        sphere.material = amigaMaterial;

        engine.runRenderLoop(function () {
            sphere.rotation.y += 0.05;
            scene.render();
        });
    }
};

Ich benutze hier BABYLON.ShaderMaterial, um mich weder mit dem Erstellen, dem Verlinken noch der Bearbeitung der Shader direkt beschäftigen zu müssen.

Wer ein BABYLON.ShaderMaterial erstellt, muss das DOM-Element angeben, wo die Shader abgelegt sind oder den Basisnamen der Dateien, in denen sich die Shader befinden. Bei der Nutzung von Dateien muss für jeden Shader nach folgendem Muster eine eigene Datei erzeugt werden: basename.vertex.fx und basename.fragment,.fx. Dann wird das Material wie folgt erstellt:

var cloudMaterial = new BABYLON.ShaderMaterial("cloud", scene, "./myShader",
        {
            attributes: ["position", "uv"],
            uniforms: ["worldViewProjection"]
        });

Außerdem müssen die Namen der benutzten Attributes und Uniforms angegeben werden.
Anschließend können die Werte der Uniforms und Sampler mithilfe der Funktionen setTexture, setFloat, setFloats, setColor3, setColor4, setVector2, setVector3, setVector4, setMatrix  direkt eingestellt werden.

Klingt einfach, oder?

Wer will, kann sich ja noch mal die worldViewProjection-Matrix weiter oben anschauen. Im Vergleich dazu ist die jetzt mit Babylon.js und BABYLON.ShaderMaterial erstellte Version doch das reinste Kinderspiel stimmt´s?! BABYLON.ShaderMaterial berechnet alles automatisch, weil wir es in der Liste der Uniforms so angegeben haben.

BABYLON.ShaderMaterial kann außerdem diese Matrix-Arten verarbeiten:

  • world
  • view
  • projection
  • worldView
  • worldViewProjection

Nein, Mathe braucht man hier nicht mehr. Jedes Mal wenn man zum Beispiel eine sphere.rotation.y += 0.05 veranlasst, wird die world-Matrix der Kugel erstellt und an die GPU übermittelt.

CYOS: Shader selber bauen

Jetzt können wir uns getrost auf den nächsten Level trauen. Wir werden eine Seite bauen, auf der man eigene Shader dynamisch erstellen und das Ergebnis sofort anschauen kann. Dafür nehmen wir den gerade besprochenen Code, die Seite nutzt das BABYLON.ShaderMaterial Objekt um die von uns erstellten Shader zu kompilieren und auszuführen.

Ich habe mit dem ACE Code Editor für CYOS gearbeitet – ein hervorragender Code Editor mit Syntaxmarkierern. Lohnt auf jeden Fall, sich ihn mal hier anzuschauen. Und CYOS gibt es hier.

Mit dem ersten Kombinationsfeld wählt man vorgegebene Shader aus. Welche das sind, sehen wir gleich.

Im zweiten Kombinationsfeld kann man das Gitter (das 3D Objekt) verändern, das uns eine Vorschau der Shader gibt.

Mit dem Compile Button wiederum erstellt man ein neues BABYLON.ShaderMaterial von den Shadern. Dieser Button nutzt folgenden Code:

// Compile
shaderMaterial = new BABYLON.ShaderMaterial("shader", scene, {
    vertexElement: "vertexShaderCode",
    fragmentElement: "fragmentShaderCode",
},
    {
        attributes: ["position", "normal", "uv"],
        uniforms: ["world", "worldView", "worldViewProjection"]
    });

var refTexture = new BABYLON.Texture("ref.jpg", scene);
refTexture.wrapU = BABYLON.Texture.CLAMP_ADDRESSMODE;
refTexture.wrapV = BABYLON.Texture.CLAMP_ADDRESSMODE;

var amigaTexture = new BABYLON.Texture("amiga.jpg", scene);

shaderMaterial.setTexture("textureSampler", amigaTexture);
shaderMaterial.setTexture("refSampler", refTexture);
shaderMaterial.setFloat("time", 0);
shaderMaterial.setVector3("cameraPosition", BABYLON.Vector3.Zero());
shaderMaterial.backFaceCulling = false;

mesh.material = shaderMaterial;

Unglaublich einfach, oder? Das Material ist nun in der Lage drei vorberechnete Matrizen zu übergeben: (world, worldView und worldViewProjection). Den Eckpunkten werden zugleich Position, Normalkoordinaten und Textur-Koordinaten zugeordnet. Zwei Texturen sind dabei bereits geladen:

amiga.jpg:

amiga

  • ref.jpg:

ref

Und zu guter Letzt kommt dann noch der renderLoop. Dort aktualisiere ich zwei praktische Uniforms:

  • Eine heißt time und erlaubt uns ein paar lustige Animationen.
  • Die andere ist cameraPosition, um die Kameraposition in die Shader zu bekommen (sehr sinnvoll bei Lichtberechnungen).
engine.runRenderLoop(function () {
    mesh.rotation.y += 0.001;

    if (shaderMaterial) {
        shaderMaterial.setFloat("time", time);
        time += 0.02;

        shaderMaterial.setVector3("cameraPosition", camera.position);
    }

    scene.render();
});

Dank unserer fleißigen Arbeit an Windows Phone 8.1, kann man CYOS auch auf einem Windows Phone benutzen (So kann man Shader erstellen, wo und wann es gerade passt.):

wp_ss_20140417_0001

Basic Shader

Beginnen wir also mit dem ersten Shader, der auf CYOS definiert wird: Der Basic Shader.

Den kennen wir schon. Er berechnet die gl_position und benutzt Texturkoordinaten, um für jeden Pixel eine Farbe abzurufen.

Um die Pixelposition zu berechnen brauchen wir die worldViewProjectionMatrix und die Position des Eckpunkts:

precision highp float;

// Attributes
attribute vec3 position;
attribute vec2 uv;

// Uniforms
uniform mat4 worldViewProjection;

// Varying
varying vec2 vUV;

void main(void) {
    gl_Position = worldViewProjection * vec4(position, 1.0);

    vUV = uv;
}

Texturkoordinaten (uv) werden unverändert an den Pixel Shader übermittelt.

Hinweis: Wir müssen “precision mediump float;” in der ersten Zeile hinzufügen, sowohl für den Vertex Shader als auch für den Pixel Shader; das ist für Chrome erforderlich. Um eine bessere Performance zu erreichen, wird damit festgelegt, dass keine Gleitkommawerte mit vollständiger Präzision benutzt werden.

Der Pixel Shader ist sogar noch einfacher, weil wir nur die Texturkoordinaten brauchen und die Texturfarbe abrufen:

precision highp float;

varying vec2 vUV;

uniform sampler2D textureSampler;

void main(void) {
    gl_FragColor = texture2D(textureSampler, vUV);
}

Wir haben bereits gesehen, dass das uniform-Element textureSampler mit der Textur “amiga” gefüllt ist. Daher sieht das Ganze dann so aus:

image

Schwarz/Weiß Shader

Nun zum nächsten Shader: den Schwarz/Weiß Shader.

Dieser Shader basiert auf dem vorhergehenden, soll jedoch ausschließlich im Schwarz/Weiß Rendering Modus arbeiten.
Wir nehmen also den Vertex Shader, den wir schon haben. Nur den Pixel Shader werden wir leicht modifizieren.

Zunächst beschränken wir uns auf nur eine Komponente, in diesem Fall hier die grüne:

precision highp float;
varying vec2 vUV;
uniform sampler2D textureSampler;
void main(void) {
    gl_FragColor = vec4(texture2D(textureSampler, vUV).ggg, 1.0);
}

Deutlich zu sehen: Statt .rgb (diese Funktion nennt man Swizzle) haben wir .ggg benutzt.

Um aber einen wirklich korrekten Schwarz/Weiß-Effekt zu erzielen, sollten wir außerdem die Helligkeit berechnen (dabei alle Komponenten in Betracht ziehend):

precision highp float;

varying vec2 vUV;

uniform sampler2D textureSampler;

void main(void) {
    float luminance = dot(texture2D(textureSampler, vUV).rgb, vec3(0.3, 0.59, 0.11));
    gl_FragColor = vec4(luminance, luminance, luminance, 1.0);
}

Die Punktfunktion (oder das Punktprodukt) wird folgendermaßen berechnet:

result = v0.x * v1.x + v0.y * v1.y + v0.z * v1.z

In unserem Beispiel also:

luminance = r * 0.3 + g * 0.59 + b * 0.11 (Diese Werte berücksichtigen, dass das menschliche Auge Grüntöne besser wahrnimmt.)

Na wenn das nicht ziemlich cool klingt, oder?

image

Cel Shading Shader

Kommen wir jetzt zu einem etwas komplexeren Shader, dem Cel Shading Shader (fälschlicherweise oft als „cell shading“ geschrieben).

Bei diesem brauchen wir die Normalkoordinaten und die Positionen der Eckpunkte im Pixel Shader. Demzufolge sieht der Vertex Shader so aus:

precision highp float;

// Attributes
attribute vec3 position;
attribute vec3 normal;
attribute vec2 uv;

// Uniforms
uniform mat4 world;
uniform mat4 worldViewProjection;

// Varying
varying vec3 vPositionW;
varying vec3 vNormalW;
varying vec2 vUV;

void main(void) {
    vec4 outPosition = worldViewProjection * vec4(position, 1.0);
    gl_Position = outPosition;

    vPositionW = vec3(world * vec4(position, 1.0));
    vNormalW = normalize(vec3(world * vec4(normal, 0.0)));

    vUV = uv;
}

Hinweis: Wir benutzen hier auch die world-Matrix, weil Position und Normalkoordinaten ohne Umwandlung gespeichert sind. Die Anwendung der world-Matrix erlaubt es uns, die Rotation des Objekts mit einzuberechnen.

Der Pixel Shader sieht dann wie folgt aus:

precision highp float;

// Lights
varying vec3 vPositionW;
varying vec3 vNormalW;
varying vec2 vUV;

// Refs
uniform sampler2D textureSampler;

void main(void) {
    float ToonThresholds[4];
    ToonThresholds[0] = 0.95;
    ToonThresholds[1] = 0.5;
    ToonThresholds[2] = 0.2;
    ToonThresholds[3] = 0.03;

    float ToonBrightnessLevels[5];
    ToonBrightnessLevels[0] = 1.0;
    ToonBrightnessLevels[1] = 0.8;
    ToonBrightnessLevels[2] = 0.6;
    ToonBrightnessLevels[3] = 0.35;
    ToonBrightnessLevels[4] = 0.2;

    vec3 vLightPosition = vec3(0, 20, 10);

    // Light
    vec3 lightVectorW = normalize(vLightPosition - vPositionW);

    // diffuse
    float ndl = max(0., dot(vNormalW, lightVectorW));

    vec3 color = texture2D(textureSampler, vUV).rgb;

    if (ndl > ToonThresholds[0])
    {
        color *= ToonBrightnessLevels[0];
    }
    else if (ndl > ToonThresholds[1])
    {
        color *= ToonBrightnessLevels[1];
    }
    else if (ndl > ToonThresholds[2])
    {
        color *= ToonBrightnessLevels[2];
    }
    else if (ndl > ToonThresholds[3])
    {
        color *= ToonBrightnessLevels[3];
    }
    else
    {
        color *= ToonBrightnessLevels[4];
    }

    gl_FragColor = vec4(color, 1.);
}

Aufgabe dieses Shaders ist es, eine Lichtquelle zu simulieren. Dabei wird auf weich verlaufende Schattierungen verzichtet, stattdessen kommen Helligkeitsstufen zum Einsatz. Ist die Lichtintensität zum Beispiel zwischen 1 (Maximum) und 0,95, wird die Farbe des Objekts (abgerufen von der Textur) direkt angewendet. Liegt die Intensität zwischen 0,95 und 0,5, wird die Farbe um den Faktor 0,8 abgeschwächt usw.

Diesen Shader erhalten wir in vier Schritten:

  • Zuerst geben wir Helligkeitsstufen und Levelkonstanten an.
  • Dann berechnen wir mithilfe der Phong-Gleichung die Lichtwerte (Wir gehen dabei davon aus, dass sich die Lichtquelle nicht bewegt.):
vec3 vLightPosition = vec3(0, 20, 10);

// Light
vec3 lightVectorW = normalize(vLightPosition - vPositionW);

// diffuse
float ndl = max(0., dot(vNormalW, lightVectorW));

Die Lichtintensität pro Pixel ist abhängig vom Winkel zwischen den Normalkoordinaten und der Lichtrichtung.

  • Anschließend erhalten wir die Texturfarbe für die Pixel
  • Zum Schluss kontrollieren wir die Helligkeitsstufen und wenden den jeweiligen Level auf die Farbe an.

Das sieht dann ein bisschen aus wie ein Cartoon-Objekt:

image

Phong Shader

Im vorherigen Shader haben wir einen Teil der Phong-Gleichung benutzt. Jetzt nehmen wir einfach mal die ganze Gleichung.

Der Vertex Shader ist leicht, weil die Action ausschließlich im Pixel Shader stattfindet:

precision highp float;

// Attributes
attribute vec3 position;
attribute vec3 normal;
attribute vec2 uv;

// Uniforms
uniform mat4 worldViewProjection;

// Varying
varying vec3 vPosition;
varying vec3 vNormal;
varying vec2 vUV;

void main(void) {
    vec4 outPosition = worldViewProjection * vec4(position, 1.0);
    gl_Position = outPosition;

    vUV = uv;
    vPosition = position;
    vNormal = normal;
}

Gemäß der Gleichung müssen wir die diffuse Reflexion und die Spekularität mithilfe der Lichtrichtung und den Normalkoordinaten der Eckpunkte berechnen:

precision highp float;

// Varying
varying vec3 vPosition;
varying vec3 vNormal;
varying vec2 vUV;

// Uniforms
uniform mat4 world;

// Refs
uniform vec3 cameraPosition;
uniform sampler2D textureSampler;

void main(void) {
    vec3 vLightPosition = vec3(0, 20, 10);

    // World values
    vec3 vPositionW = vec3(world * vec4(vPosition, 1.0));
    vec3 vNormalW = normalize(vec3(world * vec4(vNormal, 0.0)));
    vec3 viewDirectionW = normalize(cameraPosition - vPositionW);

    // Light
    vec3 lightVectorW = normalize(vLightPosition - vPositionW);
    vec3 color = texture2D(textureSampler, vUV).rgb;

    // diffuse
    float ndl = max(0., dot(vNormalW, lightVectorW));

    // Specular
    vec3 angleW = normalize(viewDirectionW + lightVectorW);
    float specComp = max(0., dot(vNormalW, angleW));
    specComp = pow(specComp, max(1., 64.)) * 2.;

    gl_FragColor = vec4(color * ndl + vec3(specComp), 1.);
}

Die diffuse Reflexion hatten wir schon im Shader davor, wir müssen also nur noch den Spiegeleffekt hinzufügen. Diese Illustration eines Wikipedia-Artikels zeigt sehr gut, wie der Shader letztlich funktioniert:

image

Bei unserer Kugel sieht das so aus:

image

Discard Shader

Für den Discard Shader stelle ich an dieser Stelle ein neues Konzept vor: Das Discard Keyword.

Dieser Shader wird jeden nicht-roten Pixel verwerfen und ein Dug-Objekt vortäuschen.

Der Vertex Shader ist der gleiche wie am Anfang beim Basic Shader:

precision highp float;

// Attributes
attribute vec3 position;
attribute vec3 normal;
attribute vec2 uv;

// Uniforms
uniform mat4 worldViewProjection;

// Varying
varying vec2 vUV;

void main(void) {
    gl_Position = worldViewProjection * vec4(position, 1.0);

    vUV = uv;
}

Der Pixel Shader prüft die Farbe und nutzt die Discard-Funktion, wenn zum Beispiel die grüne Komponente zu hoch ist:

precision highp float;

varying vec2 vUV;

// Refs
uniform sampler2D textureSampler;

void main(void) {
    vec3 color = texture2D(textureSampler, vUV).rgb;

    if (color.g > 0.5) {
        discard;
    }

    gl_FragColor = vec4(color, 1.);
}

Das sieht dann etwas seltsam aus:

image

Wave Shader

Nachdem wir mit dem Pixel Shader unseren Spaß hatten, will ich nun zeigen, dass auch der Vertex Shader eine Menge kann.

Für den Wave Shader werden wir den Phong Pixel Shader wiederverwenden.

Der Vertex Shader benutzt das uniform-Element time um einige Animationswerte zu erhalten. Dadurch wird der Shader eine Welle mit den Positionswerten der Eckpunkte erzeugen:

precision highp float;

// Attributes
attribute vec3 position;
attribute vec3 normal;
attribute vec2 uv;

// Uniforms
uniform mat4 worldViewProjection;
uniform float time;

// Varying
varying vec3 vPosition;
varying vec3 vNormal;
varying vec2 vUV;

void main(void) {
    vec3 v = position;
    v.x += sin(2.0 * position.y + (time)) * 0.5;

    gl_Position = worldViewProjection * vec4(v, 1.0);

    vPosition = position;
    vNormal = normal;
    vUV = uv;
}

Nachdem eine Sinuskurve auf die position.y angewendet wird, erhalten wir dieses Ergebnis:

image

Spherical Environment Mapping

Diese Variante wurde größtenteils von diesem Tutorial inspiriert. Ein wirklich exzellenter Artikel, den es sich zu lesen lohnt, genau wie das Experimentieren mit dem dazugehörigen Shader.

image

Fresnel Shader

Zum Abschluss will ich auch noch meinen Favoriten vorstellen: den Fresnel Shader.
Dieser Shader variiert die Intensität abhängig vom Winkel zwischen der Blickrichtung und den Normalkoordinaten der Eckpunkte.

Der Vertex Shader ist der gleiche, den wir für den Cel Shading Shader benutzt haben und es nicht schwer, die Fresnel Reflexion in unseren Pixel Shader hineinzurechnen (Wir haben schließlich die Normalkoordinaten und die Kameraposition, um die Blickrichtung zu beurteilen):

precision highp float;

// Lights
varying vec3 vPositionW;
varying vec3 vNormalW;

// Refs
uniform vec3 cameraPosition;
uniform sampler2D textureSampler;

void main(void) {
    vec3 color = vec3(1., 1., 1.);
    vec3 viewDirectionW = normalize(cameraPosition - vPositionW);

    // Fresnel
    float fresnelTerm = dot(viewDirectionW, vNormalW);
    fresnelTerm = clamp(1.0 - fresnelTerm, 0., 1.);

    gl_FragColor = vec4(color * fresnelTerm, 1.);
}

image

Oder doch einen ganz anderen Shader?

Genug der Vorbereitung, jetzt kann jeder daran gehen, einen eigenen Shader zu bauen. Eigene Erfahrungen können gern hier im Kommentarfeld oder im babylon.js Forum geteilt werden (Link s. unten).

Wer sich noch intensiver mit dem Thema beschäftigen will, hier ein paar nützliche Links:

Und hier noch ein bisschen Lehrmaterial von meiner Seite:

Oder, wer noch einen Schritt zurückgehen will – die Schulungsreihe über JavaScript von unserem Team:

Und jeder ist natürlich eingeladen, unsere kostenlosen Werkzeuge für sein nächstes Web-Projekt zu nutzen: Visual Studio Community, Azure Trial und Cross-Browser Testwerkzeuge für Mac, Linux oder Windows.

Dieser Artikel ist Teil der Web Dev Tech Series von Microsoft. Wir freuen uns, das Microsoft Edge und seine neue EdgeHTML Rendering Engine mit euch zu teilen. Kostenlose Virtual Machines oder Remote Testings für Mac, iOS, Android oder Windows gibt es hier: dev.modern.IE.

Kategorien
JavaScript & jQuery Programmierung

Nicht OOP, aber nah dran: Einfache Vererbung mit JavaScript

Viele meiner Freunde sind C#- oder C++-Entwickler und sind daran gewöhnt, in ihren Projekten mit Vererbung zu arbeiten. Wenn der eine oder die andere darüber nachdenkt, es auch mal mit JavaScript zu versuchen, ist eine der ersten Fragen: “Kann ich denn in JavaScript eigentlich auch die Vererbung nutzen?”

simpleinheritance-teaser_DE

JavaScript benutzt einen anderen Ansatz als C# oder C++, um objektorientierte Programmiermuster zu ermöglichen: Es ist eine prototypenbasierte Sprache. Die Nutzung von Prototypen bedeutet, dass das Verhalten (Behaviors) von Objekten wiederverwendet werden kann, indem man bereits vorhandene Objekte klont (also nachbildet), die als Prototypen dienen. Jedes Objekt in JavaScript ist von einem Prototypen abhängig, der einen Satz von Funktionen und Attributen (Members) vorgibt, den das Objekt nutzen kann. Klassen gibt es nicht. Nur die Objekte. Und jedes Objekt kann dann als Prototyp für ein weiteres Objekt dienen.

Dieses Konzept ist außerordentlich flexibel und wir können damit OOP-Paradigmen, wie eben auch Vererbung, simulieren.

Vererbung umsetzen

Am besten, wir visualisieren erst einmal, was wir genau erreichen wollen – mithilfe einer Hierarchie:

image

Zunächst erzeugen wir ClassA, das geht leicht Weil es wie gesagt keine eindeutigen Klassen gibt, können wir eine Reihe von Behaviors festlegen (A class so…), indem wir einfach folgende Funktion erstellen:

var ClassA = function() {
this.name = „class A“;
}

Diese “Klasse” kann nun mit dem new-Keyword instanziiert werden:

var a = new ClassA();
ClassA.prototype.print = function() {
    console.log(this.name);
}

Und sie wird unser Objekt nutzen:

a.print();

Eigentlich ganz einfach, oder?
Das gesamte Beispiel ist gerade mal acht Zeilen lang:

var ClassA = function() {
    this.name = "class A";
}

ClassA.prototype.print = function() {
    console.log(this.name);
}

var a = new ClassA();

a.print();

Jetzt werden wir ein Tool hinzufügen, mit dem wir eine Art Vererbung zwischen Klassen erzeugen können. Es hat nur eine Aufgabe – den Prototypen zu klonen:

var inheritsFrom = function (child, parent) { child.prototype = Object.create(parent.prototype); };

Wenn das nicht fast nach Zauberei aussieht! Mit dem Kopieren des Prototypen übertragen wir alle Attribute und Funktionen auf die neue Klasse.

Wenn wir also eine zweite Klasse als abgeleitete Klasse der ersten hinzufügen wollen, brauchen wir nichts weiter als diesen Code:

var ClassB = function() {
    this.name = "class B";
    this.surname = "I'm the child";
}

inheritsFrom(ClassB, ClassA);

Weil ClassB nun die print Funktion von ClassA geerbt hat, funktioniert dann auch der folgende Code:

var b = new ClassB();
b.print();

Und führt zu dem folgenden Ergebnis:

class B

Wir können die print-Funktion für ClassB aufheben:

ClassB.prototype.print = function() {
    ClassA.prototype.print.call(this);
    console.log(this.surname);
}

In diesem Fall erhalten wir dieses Resultat:

class B
I’m the child

Der Trick besteht darin, ClassA.prototype aufzurufen um die print-Basisfunktion zu erhalten. Und mithilfe der call-Funktion können wir dann die Basisfunktion des aktuellen Objekts (this) aufrufen. Wie wir ClassC erstellen ist nun offensichtlich:

var ClassC = function () {
    this.name = "class C";
    this.surname = "I'm the grandchild";
}

inheritsFrom(ClassC, ClassB);

ClassC.prototype.foo = function() {
    // Do some funky stuff here...
}

ClassC.prototype.print = function () {
    ClassB.prototype.print.call(this);
    console.log("Sounds like this is working!");
}

var c = new ClassC();
c.print();

Und das kommt dabei heraus:

class C
I’m the grandchild

Das scheint doch zu funktionieren!

Noch mehr Tipps, Tricks und Hilfen rund um JavaScript

Es klingt vielleicht seltsam, aber Microsoft bietet eine ganze Reihe kostenloser Lehrangebote zu zahlreichen Open Source JavaScript-Themen an – und mit der anstehenden Veröffentlichung von Microsoft Edge werden wir in diesem Bereich noch sehr viel mehr tun. Auch von mir gibt es Tutorials:

Außerdem haben wir noch die Schulungsreihe unseres Teams:

Praktisch sind zudem die folgenden kostenlosen Tools: Visual Studio Community, Azure Trial, und Cross-Browser Testing Tools für Mac, Linux, oder Windows.

Und das noch mit auf den Weg…

Um es abschließend noch mal klar zu sagen: JavaScript ist nicht C# oder C++. Diese Skriptsprache hat ihre eigene Philosophie. Für alle C++- oder C#-Entwickler, die sich die ganze Leistung von JavaScript zu Eigen machen wollen, habe ich diesen wichtigen Tipp: Versucht nicht eure Lieblingssprache in JavaScript nachzubilden. Es gibt nicht die beste oder die schlechteste Sprache. Nur unterschiedliche Ansätze und Philosophien!

Dieser Artikel ist Teil der Web Dev Tech Series von Microsoft. Wir freuen uns, Microsoft Edge und seine neue EdgeHTML Rendering Engine mit euch zu teilen. Kostenlose Virtual Machines oder Remote Testings für Mac, iOS, Android oder Windows gibt es hier: dev.modern.IE.

Kategorien
JavaScript & jQuery Programmierung Sonstige Programmiersprachen

ECMAScript 6 besser verstehen: Klassen und Vererbung

Hiermit möchte ich Sie zu einer Serie von Artikeln über ECMAScript 6 einladen, mit der ich nicht nur meine Begeisterung darüber teilen, sondern vor allem erklären will, wie man ECMAScript 6 am besten verwenden kann. Ich hoffe, Sie haben mindestens soviel Spaß beim Lesen, wie ich beim Schreiben hatte.

ecmascript01-teaser_DE

Zu meinem Hintergrund: Ich arbeite bei Microsoft an der Browser Rendering Engine für das Microsoft Edge, welche gegenüber der bisherigen Internet Explorer Engine, die wir im Lauf der Jahre ausgiebig kennen (und lieben?) gelernt haben, ein gewaltiger Schritt nach vorn ist. Mein persönliches Lieblings-Feature dabei ist, dass sie jede Menge ECMAScript 6 unterstützt. Das erleichtert das Schreiben von komplexen Web-Applikationen ungemein.

Laut http://kangax.github.io/compat-table/es6/ und ES6 on dev.modern.ie/platform/status haben wir derzeit fast 70% der ECMAScript 6 Features in Microsoft Edge.

Keine Frage, ich mag es mit JavaScript zu arbeiten, aber wenn ich an großen Projekten wie Babylon.js sitze, greife ich lieber auf TypeScript zurück, das jetzt auch Angular 2 antreibt. Der Grund ist, dass ich bei JavaScript (auch bekannt als ECMAScript 5) einige Syntax-Funktionen vermisse, die andere Programmiersprachen, mit denen ich große Projekte schreibe, mitbringen. Zum Beispiel fehlen mir Klassen und Vererbungen.

Wie Sie von JavaScript auf TypeScript umsteigen, erfahren Sie in diesem Artikel. Eine ausführliche Einführung in TypeScript erhalten Sie in diesem kostenlosen Online-Kurs der Microsoft Virtual Academy (MVA). Den kostenlosen Download zu TypeScript 1.4 für Visual Studio 2013 finden Sie hier.

Eine Klasse erstellen

JavaScript ist eine prototypen-basierte Sprache und mit ECMAScript 5 kann man Klassen und Vererbungen simulieren. Sie möchten die Performance Ihrer Webanwendung verbessern? Hier finden Sie praktische Tipps zur Verbesserung Ihres HTML/JavaScript Codes.

Die flexiblen Funktionen von JavaScript erlauben uns, eine Datenkapselung zu simulieren, als ob wir mit Klassen arbeiten würden. Um dies zu erreichen, erweitern wir den Prototyp eines Objekts:

var Animal = (function () {
    function Animal(name) {
        this.name = name;
    }
    // Methods
    Animal.prototype.doSomething = function () {
        console.log("I'm a " + this.name);
    };
    return Animal;
})();


var lion = new Animal("Lion");
lion.doSomething();

Deutlich wird hier, dass wir eine „Klasse“ mit „Eigenschaften“ und „Methoden“ definiert haben.

Der Konstruktor ist dabei durch die Funktion selbst definiert (function Animal). Dies ermöglicht uns die Instanziierung von Eigenschaften. Durch die Nutzung des Prototyps können wir Funktionen definieren, die wie Instanzmethoden behandelt werden.

Man kann es also durchaus so machen, muss aber Einiges von prototypischer Vererbung verstehen. Und wer an klassenbasierte Sprachen gewöhnt ist, findet das sicher alles etwas verwirrend. Etwas seltsam ist natürlich auch, dass in JavaScript zwar das Schlüsselwort Klasse existiert, es aber nichts bewirkt. ECMAScript 6 hingegen bringt dieses jetzt zum Laufen, und das Ganze auch noch mit einem einfacheren Code:

class AnimalES6 {
    constructor(name) {
        this.name = name;
    }

    doSomething() {
        console.log("I'm a " + this.name);
    }
}

var lionES6 = new AnimalES6("Lion");
lionES6.doSomething();

Am Ende haben wir das gleiche Ergebnis. Aber für Entwickler, die gewohnt sind, mit Klassen zu arbeiten, ist das Ganze weitaus leichter zu schreiben und zu lesen. Ein Prototyp ist dazu hier nicht nötig und und man nutzt einfach das Keyword „Constructor“ um den Konstruktor zu definieren.

Darüber hinaus bringen Klassen viel neue Semantik mit, die in ECMAScript 5 nicht vorhanden war. So kann ein Konstruktor nicht ohne das Keyword „new“ aufgerufen oder Methoden mit „new“ konstruiert werden. Eine weitere Veränderung: Methoden sind nicht aufzählbar.

Interessant ist jedoch: Beide Versionen können parallel genutzt werden.

Denn trotz der neuen Keywords, am Ende des Tages landen wir bei einer Funktion mit einem Prototyp, dem eine Funktion hinzugefügt wurde. Eine „Methode“ bezeichnet hier einfach eine Funktionseigenschaft des Objekts.

Eine weitere Kernfunktion klassenbasierter Entwicklung, "getter and setter", wird ebenfalls von ES6 unterstützt. Dadurch wird schnell deutlich, was der Sinn von Methoden ist und was sie tun sollen:

class AnimalES6 {
    constructor(name) {
        this.name = name;
        this._age = 0;
    }

    get age() {
        return this._age;
    }

    set age(value) {
        if (value < 0) {
            console.log("We do not support undead animals");
        }

        this._age = value;
    }

    doSomething() {
        console.log("I'm a " + this.name);
    }
}

var lionES6 = new AnimalES6("Lion");
lionES6.doSomething();
lionES6.age = 5;

Ganz praktisch, oder?

Gleichzeitig kommt hier einer der typischen Vorbehalte gegenüber JavaScript zum Vorschein: der gar nicht so vertrauliche „private Member“ (_age). Mehr zu diesem Thema schrieb ich in diesem Artikel.

Mit einer neuen Funktion von ECMAScript 6 haben wir nun einen eleganteren Weg, dies umzusetzen und zwar mithilfe von „Symbolen“:

var ageSymbol = Symbol();

class AnimalES6 {
    constructor(name) {
        this.name = name;
        this[ageSymbol] = 0;
    }

    get age() {
        return this[ageSymbol];
    }

    set age(value) {
        if (value < 0) {
            console.log("We do not support undead animals");
        }

        this[ageSymbol] = value;
    }

    doSomething() {
        console.log("I'm a " + this.name);
    }
}

var lionES6 = new AnimalES6("Lion");
lionES6.doSomething();
lionES6.age = 5;

Was also ist ein Symbol in diesem Zusammenhang? Es ist ein einzigartiger, unveränderlicher Datentyp, der es ermöglicht, eindeutige Objekteigenschaften zu definieren. Ohne das Symbol besteht kein Zugang zu den Eigenschaften.

Die Folge ist ein „persönlicherer“ Zugriff auf die Member. Oder zumindest ist dieser Zugriff jetzt nicht mehr so einfach. Symbole helfen zwar bei der Einzigartigkeit von Namen, aber diese Einzigartigkeit allein garantiert noch keinen Datenschutz. Es bedeutet nur: Wenn ein Schlüssel nötig ist, der mit einem anderen nicht kollidieren darf, erzeugen Sie ein neues Symbol.

Dass auch hier noch niemand so ganz "privat" ist, liegt an Object.getOwnPropertySymbols. Damit können andere auf die Symboleigenschaften zugreifen.

Mit Vererbungen arbeiten

Sobald wir Klassen haben, erwarten wir auch Möglichkeiten der Vererbung. Auch hier war es bisher schon möglich, Vererbung in ES5 zu simulieren, aber es war schon eine recht komplexe Angelegenheit.

So sah es zum Beispiel aus, wenn TypeScript Vererbung nachbildete:

var __extends = this.__extends || function (d, b) {
    for (var p in b) if (b.hasOwnProperty(p)) d[p] = b[p];
    function __() { this.constructor = d; }
    __.prototype = b.prototype;
    d.prototype = new __();
};
var SwitchBooleanAction = (function (_super) {
     __extends(SwitchBooleanAction, _super);
     function SwitchBooleanAction(triggerOptions, target, propertyPath, condition) {
        _super.call(this, triggerOptions, condition);
        this.propertyPath = propertyPath;
        this._target = target;
     }
     SwitchBooleanAction.prototype.execute = function () {
        this._target[this._property] = !this._target[this._property];
     };
     return SwitchBooleanAction;
})(BABYLON.Action);

Nicht unbedingt sehr leicht zu lesen, oder? Die Alternative von ECMAScript 6 sieht da schon besser aus:

var legsCountSymbol = Symbol();
class InsectES6 extends AnimalES6 {
    constructor(name) {
        super(name);
        this[legsCountSymbol] = 0;
    }

    get legsCount() {
        return this[legsCountSymbol];
    }

    set legsCount(value) {
        if (value < 0) {
            console.log("We do not support nether or interstellar insects");
        }

        this[legsCountSymbol] = value;
    }

    doSomething() {
        super.doSomething();
        console.log("And I have " + this[legsCountSymbol] + " legs!");
    }
}

var spiderES6 = new InsectES6("Spider");
spiderES6.legsCount = 8;
spiderES6.doSomething();

Mithilfe des Keywords „extends“ lassen sich Klassen von einer anderen ableiten, also spezialisierte Klassen erzeugen („child“). Das Keyword „super“ stellt dann die Beziehung zur Basisklasse her.

Dank all dieser neuen Features können wir nun Klassen erzeugen und mit Vererbungen arbeiten, ohne uns Hilfe in Hogwarts holen zu müssen, um das Gleiche mit Prototypen zu erreichen.

Warum TypeScript noch relevanter ist als zuvor…

Nachdem wir nun die beschriebenen neuen Funktionen in unserem Browser haben, ist es meiner Meinung nach sogar noch sinnvoller als vorher, TypeScript zu nutzen, um JavaScript-Code zu erstellen.

Schließlich unterstützt TypeScript (1.4) jetzt ECMAScript 6 Code (mit let und const Keywords). Bereits existierender TypeScript-Code kann also weiter verwendet werden, man muss nur noch diese neue Option aktivieren, um ECMAScript 6 Code zu erstellen.

Und wer sich ein paar Zeilen TypeScript genauer anschaut, wird feststellen, dass sie aussehen wie ECMAScript 6 ohne die Typen. Wer heute TypeScript beherrscht, wird morgen also auch ECMAScript 6 schneller verstehen.

Daniel Meixner und Chris Heilmann sprechen in dieser Ausgabe des TechTalk ausführlich über die Neuerungen bei Edge, ECMAScript 2015 und TypeScript.

Fazit

Wer mit TypeScript arbeitet, kann die Funktionen in allen Browsern nutzen, der Code wird in ECMAScript 5 umgewandelt. Wer ECMAScript 6 direkt im Browser nutzen will, muss auf Windows 10 aktualisieren und dort die Rendering Engine von Microsoft Edge testen. Wem das jedoch zuviel Aufwand ist, weil er nur mal eben die neuen Browser-Funktionen ansehen will: remote.modern.ie bietet den Zugriff auf einen Windows 10 Computer mit Microsoft Edge. Das klappt auch mit MacOS oder Linux-Maschinen.

Klar, Microsoft Edge ist nicht der einzige Browser, der den offenen Standard ES6 unterstützt. Wie weit der Support anderer Browser inzwischen reicht, ist hier verzeichnet: http://kangax.github.io/compat-table/es6/

Und um noch einmal euphorisch zu werden: Ich denke die Zukunftsaussichten für JavaScript mit ECMAScript 6 sind wirklich glänzend und ich kann es kaum erwarten, dass es von möglichst vielen Browsern unterstützt wird.

Dieser Artikel ist Teil der Web Dev Tech Series von Microsoft. Wir freuen uns, Microsoft Edge und seine neue EdgeHTML Rendering Engine mit Ihnen zu teilen. Kostenlose Virtual Machines oder Remote Testings für Mac, iOS, Android oder Windows gibt es hier: dev.modern.IE.