Video-Manipulation mit Canvas Schritt für Schritt erklärt

Da wir ja kürzlich im Zuge des in JavaScript geschriebenen MP3-Decoders die These „das muss doch auch mit Video gehen“ aufgestellt hatten, dachte ich, dass eine kleine Canvas-Video-Demo nebst Erklärungen angebracht wäre. Das was wir in diesem Artikel durchexerzieren ist nur das simpelste aller Beispiele, sollte aber für einen grundsätzlichen Überblick reichen; wir basteln uns ein kleines Script, das einen Videostream von einem Video-Element auf ein Canvas-Element kopiert und mit einen Hipster-Effekt versieht:

Der fertige Videoeffekt in Aktion

Grundlagen

Für unsere Effektmaschine brauchen wir HTML-seitig nichts weiter als ein Video-Element, zwei Canvas-Elemente und einen Script-Block:

<!doctype html>
<video id="film" width="320" height="180" controls>
    <source src="video.mp4" type="video/mp4">
    <source src="video.ogv" type="video/ogg">
</video>
<canvas id="zwischenablage" width="320" height="180" style="display:none"></canvas>
<canvas id="ziel" width="320" height="180"></canvas>
<script></script>

Warum zwei Canvas-Elemente? Unsere Video-Bearbeitung muss in zwei Schritten erfolgen, denn wir können nicht auf die einzelnen Pixel auf einem Video-Element zugreifen – das geht nur bei Canvas-Elementen. Also müssen wir unsere Video-Frames vom Canvas-Element auf Canvas 1 kopieren und von dort auf Canvas 2. Und weil Canvas 1 zu wirklich nichts weiter gebraucht wird, können wir sie auch mit display:none unsichtbar machen. Jetzt fehlt nur noch DOM-Zugriff auf die drei Elemente …

// Elemente in der Seite
var film           = document.getElementById('film'),
    ziel           = document.getElementById('ziel').getContext('2d');
    zwischenablage = document.getElementById('zwischenablage').getContext('2d');

… und schon kann es losgehen.

Die richtige Framerate abpassen

Das Ziel ist also nun, jedes Frame des Videos abzupassen, zu kopieren und zu modifizieren. Das ist schwieriger als man vielleicht zunächst meint, denn herkömmliche JavaScript-Timing-Mechanismen (setTimeout() und setInterval()) sind nicht besonders exakt; mit ihnen kann man nicht garantieren, dass man jedes Frame erwischt oder dass man nicht vielleicht unnötigerweise manche Frames zweimal kopiert. Die richtige Pfad führt über eine Funktion namens requestAnimationFrame() (Specs), die auf das nächste vom Browser gerenderte Frame wartet und dann einen Callback ausführt. Dieses Werkzeug sorgt, rekursiv angewendet, für einen präzisen Animations-Loop.

Der Haken an der Sache ist, dass requestAnimationFrame() eine sehr sehr neue Erfindung ist und in den diversen Browsern nur mit Vendor-Prefix zu finden ist – wenn überhaupt. Damit wir uns mit all diesen Varianten keinen Wolf programmieren setzen wir eine kleine Hilfsfunktion ein (adaptiert von Paul Irish) die uns eine verzweiflungsfreie API bereitstellt:

// Hack für requestAnimationFrame in allen Browsern
var animate = (function(){
    return window.requestAnimationFrame    ||
        window.webkitRequestAnimationFrame ||
        window.mozRequestAnimationFrame    ||
        window.oRequestAnimationFrame      ||
        window.msRequestAnimationFrame     ||
        function(callback){
            setTimeout(callback, 1000 / 60);
        };
})();

Hier wird dafür gesorgt, dass wir unter animate die bestmögliche Implementierung von requestAnimationFrame() vorfinden, die unser Browser zu bieten hat; zur Not auch eine Variante mit Vendor-Prefix oder wenn alle Stricke reißen auch ein herkömmlicher setTimeout().

Damit wir auch eine Animationsschleife erhalten, müssen wir animate wieder und wieder aufrufen, wobei jeder Aufruf der Funktion ein einzelnes Frame abhandelt. Auch diese Funktionalität stecken wir wieder in eine Hilfsfunktion:

// Die Kopier-Schleife
function loop(){
    if(!film.paused){
        animate(loop);
    }
}

Hier wird loop() immer wieder von sich selbst aufgerufen – jeweils durch unseren requestAnimationFrame() getunnelt und nur so lange, wie das Video auch läuft (film.paused nicht true ist). Fehlt nur noch der Startschuss für die Kopier-Schleife, wofür uns praktischerweise das Video-Element ein passendes Event bereitstellt:

// Die Kopier-Schleife starten
film.addEventListener('play', function(){
	loop();
}, false);

Zu diesem Zeitpunkt können wir unsere Animationsschleife starten, laufen lassen und stoppen, wobei freilich noch nicht wirklich etwas passiert – dazu müssen wir noch ein paar weitere Funktionen programmieren.

Pixel kopieren …

Auf ein Canvas-Element kann man mit der drawImage() ganz einfach fertige Grafiken zeichen – einfach neben den Ziel-Koordinaten ein HTML-Element angeben, das als Datenquelle dienen soll und fertig! Als Quelle können neben <img>-Elementen auch Canvas- oder Video-Elemente fungieren, so dass es ein leichtes ist, die Videoframes auf die Zwischenablage zu kopieren. Um an die Pixeldaten des Frames zu kopieren, müssen wir die Methode getImageData() auf der Zwischenablage bemühen. Diese gibt ein so geanntes ImageData-Objekt zurück, dass die Farbwerte jedes einzelnen Pixels enthält. Wenn wir dieses Objekt nun mit getImageData() auf die Ziel-Canvas schreiben …

// Diese Funktion kopiert Pixel von "film" auf "zwischenablage", dann auf "ziel"
function copy(){
    zwischenablage.drawImage(film, 0, 0);
    var bilddaten = zwischenablage.getImageData(0, 0, 320, 180);
    ziel.putImageData(bilddaten, 0, 0);
}

… und den Aufruf der copy()-Funktion in den Animationsloop einbauen …

// Die Kopier-Schleife
function loop(){
    if(!film.paused){
        copy(); // Frames kopieren
        animate(loop);
    }
}

… haben wie schon mal zumindest einen schönen sauberen Klon des Videos erzeugt. Wegen des Doppelschritts beim Kopieren haben wir durch das ImageData-Objekt Zugriff auf die Farbwerte jedes einzelnen Pixels in jedem Videoframe, so dass es uns ein leichtes ist, ein bisschen mit den Farben zu spielen.

… und verändern

Ein ImageData-Objekt enthält neben den Angaben width und height (geben die Maße des Ausschnitts an) ein Array, das der Reihe nach alle Farbwerte aller Pixel enthält. Der erste Eintrag im Array ist der Rot-Wert des ersten Pixels, der zweite Eintrag ist der Grün-Wert des ersten Pixels, an dritter Stelle folgt der Blau-Wert des ersten Pixels, dann kommt der Alpha-Wert des ersten Pixels und dann der Rot-Wert des zweiten Pixels … und so weiter. Diese Werte effektvoll zu manipulieren ist natürlich sehr einfach:

// Wendet den Effekt an
function effekt(bilddaten){
    var pixel = bilddaten.data;
    var i = 0;
    var r, g, b, new_r, new_g, new_b;
    while(i < pixel.length){
        // R, G und b holen...
        r = pixel[i],
        g = pixel[i + 1],
        b = pixel[i + 2];
        // Manipulieren...
        new_r = Math.min(255, r * 0.393 + g * 0.769 + b * 0.189),
        new_g = Math.min(255, r * 0.349 + g * 0.686 + b * 0.168),
        new_b = Math.min(255, r * 0.272 + g * 0.534 + b * 0.131);
        // ... und speichern!
        pixel[i] = new_r;
        pixel[i + 1] = new_g;
        pixel[i + 2] = new_b;
        // Auf zum nächsten Pixel - den Alphawert einfach überspringen
        i += 4;
    }
    return bilddaten;
}

Die Funktion rattert einmal durch das Bilddaten-Array, spielt an den RGB-Werten der Pixel herum, überspringt den Alpha-Wert und gibt am Ende ein komplett überarbeitetes ImageData-Objekt zurück. Dieses müssen wir dann nur noch abbilden, d.h. wir müssen die Funktion effekt() in copy() einmal auf die Bilddaten anwenden

// Diese Funktion kopiert Pixel von "film" auf "zwischenablage", dann auf "ziel"
function copy(){
    zwischenablage.drawImage(film, 0, 0);
    var bilddaten = zwischenablage.getImageData(0, 0, 320, 180);
    bilddaten = effekt(bilddaten); // Effekt anwenden
    ziel.putImageData(bilddaten, 0, 0);
}

Und das war es dann auch schon! Das Ganze läuft halbwegs performant in jedem Browser, der Canvas- und Video-Element unterstützt, was heutzutage ja auf alles jenseits der Kreidezeit-Fraktion aus dem Hause Microsoft (IE 6 - 8) zutrifft.

Ausblick

Ein paar Pixel zu verdrehen ist also augenscheinlich nicht besonders schwer. Wollte man wirklich Videodaten erst im Browser generieren, wäre das aber im Prinzip auch kein Hexenwerk; mit der createImageData()-Methode des 2D-Kontext (Spezifikationen) kann man leere Bilddatensätze erstellen und diese dann mit Farbwerten für die diversen Pixel befüllen. Die Herausforderung liegt tatsächlich eher in der Codierung und Decodierung von Bilddaten, denn man will schließlich in Sachen Wiedergabe weder von dem löchrigen Codec-Support der Browser abhängig sein, noch möchte man darauf verzichten, bei Effektgeneratoren wie unserer kleinen Demo am Ende fertige Filme abzuspeichern. In diesem Bereich liegen die wahren Herausforderungen. Die Werkzeuge sind jedoch alle da. Es müsste nur mal jemand etwas daraus machen.

Aufstand der Webworker: In JavaScript geschriebener Codec bringt Firefox MP3 bei

Eins der schönsten Features von HTML5 könnten die Audio- und Videoelemente sein. Mit <audio> und <video> ist es kinderleicht Ton und Bewegtbild in Webseiten einzubetten und die sehr durchdachte API lädt zum Programmieren eigener Player-Interfaces geradezu ein. Leider gibt es das Codec-Problem: kein einziger Audio- oder Videocodec wird von allen Browsern unterstützt. Die beteiligten Parteien (die Browserhersteller) haben auch sehr gute Gründe – wirtschaftliche Gründe – sich dem jeweiligen Feindes-Codec zu verweigern und es ist nicht wirklich abzusehen, wo in nächster Zeit eine Einigung unter den Browserherstellern herkommen soll. Fakt ist also: <audio> und <video> sind bis auf weiteres praktisch unbrauchbar.

Weil ich zum Glück keinen Browserhersteller gehöre, sondern unabhängiger HTML5-Erklärbär bin, darf ich das am Ende eines Workshops auch immer so offen formulieren. Meine Vorschläge für den Umgang mit dieser Situation sind immer die gleichen: entweder weiterhin das bewärhte Flash benutzen oder den fehlenden Codec in JavaScript nachbauen. Letzteres wird dabei meist nicht ganz ernst genommen, aber wenn es Dinge wie einen in JS geschriebenen PC-Emulator gibt, sehe ich nicht ein, dass es nicht Möglich sein soll, seinen eigenen Decoder im Browser zu programmieren. Dank Audio Data API und <canvas> ist es schließlich möglich, jedwede Ton- oder Bildinformation in einem modernen Browser abzubilden – es müsste halt nur mal jemand wagen.

Und nun hat es endlich mal jemand gewagt: JSmad ist das Script, das dem Firefox 4 MP3-Support beibringt. Das Projekt ist noch recht jung und es hat so seine Performance-Probleme, aber in diesem Fall zählt wirklich vor allem erst mal der Grundgedanke: es gibt keinen Grund, sich HTML5 von den Browserherstellern kaputt machen zu lassen! Sie geben uns ein kaputtes DOM und wir werfen so lange jQuery darauf, bis es funktioniert. Sie liefern uns keine Codecs, wir bauen sie. Das ist genau die richtige Einstellung für ein entspannt-produktives Verhältnis zu HTML5 – denn von Klagen und Jammern allein wird es nicht besser.

Langfristig gesehen wäre es vermutlich rechtlich unbedenklicher, die freien Codecs für die proprietären Browser nachzubauen statt wie im Fall von JSmad umgekehrt. Dazu müssten sich Safari und Internet Explorer zwar erst mal die Audio Data API zulegen, aber auch dieser Tag kann so fern nicht sein. Das wird schon.

Warum man die neuen Formularelemente von HTML5 nicht (kaum) mit CSS stylen kann

Wenn ich auf meinen HTML5-Seminaren die neuen Formularelemente vorstelle, währt die allgemeine Begeisterung in der Regel exakt so lange, bis einer fragt, wie man die tollen neuen Dinger denn gestalten könnte. Denn das ist praktisch nicht möglich. Nimmt man etwa einen Datumspicker, wie er uns in Opera dargestellt wird, und verpasst ihm via CSS einen roten Hintergrund, so ist das Ergebnis etwas unbefriedigend:

HTML5-Datumspicker + CSS = Desaster

Zwar ist das nicht im engeren Sinne falsch (ein roter Hintergrund ist ja vorhanden), aber auch nicht wirklich hilfreich – die Sonntage sind unsichtbar und die Buttons noch ohne jedes Design. Das Interface des Datumspickers ist einfach eine zu komplexe Konstruktion, als dass man ihr durch so einfache Anweisungen wie „roter Hintergrund“ gerecht werden könnte. Unter der Haube baut der Browser den Datumspicker aus HTML zusammen, so genanntem Shadow DOM, das gegen Stylingmaßnahmen von außen abgeschirmt ist. Theoretisch könnten es uns die Browserhersteller durch Pseudoelemente ermöglichen, die Einzelteile des Datumspickers zu gestalten, doch auch auch dann würde sich der Vorgang zwischen den verschiedenen Browsern extrem unterscheiden. Bei praktischer Betrachtungsweise muss man also sagen: die neuen Formularelemente von HTML5 lassen sich kaum bis gar nicht gestalten.

Der Grund hierfür ist einfach: der Standard legt überhaupt nicht fest, wie die neuen Formularelemente auszusehen haben. So verlieren die Spezifikationen z.B. über die Natur des <input type="date"> nicht mehr als die folgenden Worte:

The input element represents a control for setting the element's value to a string representing a specific date.

Das ist ein Freibrief für die Browserhersteller, ihr Eingabefeld so zu gestalten wie es ihnen gerade passt. Daraus folgen die unterschiedlichsten Umsetzungen der Browser und die praktisch nicht gegebenen Gestaltungsmöglichkeiten. Und das ist kein Versehen, sondern durchaus ein Feature, denn wie ein ein Eingabefeld idealerweise auszusehen hat, ist von dem Kontext abhängig, in dem es verwendet wird. So sportet das Number-Input in Desktopbrowsern in aller Regel zwei kleine Buttons zum hoch- und runterzählen:

Number-Input in einem Desktopbrowser

Das ist eine absolut sinnvolle Umsetzung eine solchen Zahlen-Eingabefeldes, doch es sind durchaus Umstände denkbar, unter denen andere Gestaltungsansätze besser wären. Das gleiche Zahlen-Eingabefeld sieht in einem iPhone so aus:

Number-Input von HTML5 im iPhone-Browser

Das Eingabefeld sieht aus wie jedes andere Feld auch; die Umsetzung des Elements erfolgt ausschließlich über die Bildschirmtastatur. Denn mit den winzigen Buttons zum hoch- und runterzählen möchte man sich in einem Touch-Interface sicher nicht herumschlagen und sich die Möglichkeiten einer angepassten Bildschirmtastatur entgehen zu lassen wäre nicht sonderlich clever. So geht der iPhone-Browser seinen eigenen Weg, den er aber auch nur gehen kann, weil eben die Spezifikationen die genaue Umsetzung der neuen HTML5-Formularelemente offenlassen. Dass dabei die Gestaltungsfreiheit leidet, ist eine unschöne, aber wohl nicht vermeidbare Nebenwirkung.

ECMAScript 5, die nächste Version von JavaScript – Teil 5: Kleine Helferlein für Arrays

Auf unserer Tour durch die neuen Features von ES5 wird es Zeit, den letzten großen Themenkomplex in Angriff zu nehmen: Arrays. Arrays in JavaScript sind seltsame Zeitgenossen. Sie sind kein eigener Datentyp, sondern spezielle Objekte, die dem Programmierer viele Möglichkeiten bieten, sich in den eigenen Fuß zu schießen (Stichwort Array-Constructor, length-Eigenschaft). Auch in ES5 ändert sich daran nicht viel, denn anstelle größerer Umbauten es gibt „nur“ ein paar neue Funktionen, die das Arbeiten mit Arrays erleichtern.

Array oder nicht?

Dass der typeof-Operator mit schlafwandlerischer Sicherheit jedes Array als object identifiziert, dürfte einer der Gründe dafür sein, dass JavaScript nicht ernstgenommen wird. Dabei liegt typeof nicht falsch – JavaScript-Arrays sind nun mal Objekte. Das ändert natürlich nichts daran, dass typeof an dieser Stelle nicht sonderlich hilfreich ist. ES5 hilft, indem es die Funktion Array.isArray() einführt, die genau das macht, was man von ihr erwartet:

var foo = [];
typeof foo;         // "object"
Array.isArray(foo); // true

Warum „repariert“ man nicht einfach typeof? Das hat zwei Gründe: erstens hat, wie erwähnt, typeof nicht unrecht wenn es Arrays als Objekte identifiziert, zweitens ist das Web voll mit Scripts die sich auf das alte Verhalten von typeof verlassen. Würden das alle Browser von heute auf morgen ändern, wären die Auswirkungen so verheerend, dass kein Weg an einer neuen Funktion vorbeiführt. Wenn man bedenkt, wie kompliziert die Identifizierung von Arrays ist, sollte man einfach froh sein, dass es Array.isArray() gibt.

ForEach, Filter und Map

ForEach-Schleifen sollte jedem JavaScript-Programmierer aus seinem liebsten Framework bekannt sein und werden in der bekannten Form in ES5 fest eingeführt. Das Array wird einmal durchlaufen und eine Callback-Funktion wird der Reihe nach auf die Array-Elemente angewendet:

// Drei Alerts für drei Zahlen
var zahlen = [6, 9, 12];
zahlen.forEach(function(zahl){
    alert(zahl);
});

Während forEach() nichts zurückgibt und nur mit den einzelnen Array-Elementen arbeitet, produzieren map() und filter() neue Arrays. Dabei filtert mittels filter() eines Callbacks ein Array; gibt der Callback true zurück, wird das Element in das neue Array gepackt, bei false nicht:

// Sortiert ungerade Zahlen aus
var zahlen = [6, 9, 12];
var gerade_zahlen = zahlen.filter(function(zahl){
    if(zahl % 2 == 0){
        return true;
    }
    else {
        return false;
    }
});

Der dritte Kandidat im Bunde, map(), wendet einen Callback auf alle Array-Elemente an. Die vom Callback zurückgegebenen Werte bilden dann ein neues Array:

// Verdoppelt die Zahlen im Array
var zahlen = [6, 9, 12];
var verdoppelte_zahlen = zahlen.map(function(zahl){
    return zahl * 2;
});

Vorsicht Falle: Die Callbacks von forEach(), filter() und map() bekommen drei Parameter übergeben! Neben dem akuellen Array-Element wird auch der aktuelle Index sowie das Array an sich übergeben. Das kann nützlich sein, kann aber zur Falle werden:

["6", "9", "12"].map(parseInt);

Hier sollen die Strings im Array in Integer verwandelt werden; man würde erwarten, dass man ein Array mit dem Inhalt [6, 9, 12] erhält. Das tatsächliche Ergebnis ist aber [6, NaN, 1]. Wie das sein kann? Ganz einfach: Wenn parseInt() ein zweites Argument übergeben bekommt, behandelt es diesen als Basis und produziert entsprechende Ergebnisse. Das ist zwar logisch, aber nicht gerade intuitiv – also immer schön aufpassen.

Alle drei neuen Funktionen nehmen neben dem Callback auch noch einen zweiten Parameter an, der bestimmt, welches Objekt im Callback für this verwendet wird. Wird hier nichts oder null angegeben, ist this das globale Objekt (es sei denn man befindet sich im Strict Mode, wo dies bekanntlich nicht mehr möglich ist).

Every und Some

Die beiden Array-Methoden every() und some() wenden einen Prüf-Callback auf die Elemente eines Arrays an. Der Callback prüft, ob die Elemente einer gewissen Bedingung entsprechen und gibt true oder false zurück. Der Unterschied zwischen every() und some(): ersteres gibt true zurück, wenn alle Array-Elemente den Test bestanden haben, letzteres auch dann, wenn nur ein einziges Element die Bedingungen erfüllt.

var arr = [2, 4, 6, 7, 11];

// False - es sind nicht ALLE Elemente gerade Zahlen
arr.every(function(element){
    return (element % 2 === 0);
});

// True - Einige Elemente SIND gerade Zahlen
arr.some(function(element){
    return (element % 2 === 0);
});

Wie auch bei forEach(), filter() und map() bekommt der Callback drei Argumente übergeben – neben dem zu prüfenden Element auch seinen Index und das gesamte Array. Auch das this des Callbacks kann über ein zweites Argument für every() und some() bestimmt werden.

Reduce und ReduceRight

Wenn es darum geht, ein Array auf einen einzigen Wert einzudampfen, sind reduce() und reduceRight() die Mittel der Wahl. Beide gehen ein Array Element für Element durch (leere Elemente werden übersprungen) und wenden einen Callback auf die Elemente an. Dem Callback wird dabei einerseits der Wert des aktuellen Elements übergeben, andererseits auf der Rückgabewert des vorherigen Callback-Ausrufs. So kann man zum Beispiel bequem die Zahlen in einem Array aufsummieren:

var arr = [1, 2, 3];

// Summiert alle Elemente des Arrays auf (Resultat: 6)
arr.reduce(function(prev, curr){
    return prev + curr;
});

Der Unterschied zwischen reduce() und reduceRight() ist, dass ersteres das Array von links nach rechts durchgeht, letzteres von rechts nach links:

var arr = ["A", "B", "C"];

// Ergebnis: ABC
arr.reduce(function(prev, curr){
    return prev + curr;
});

// Ergebnis: CBA
arr.reduceRight(function(prev, curr){
    return prev + curr;
});

Der Callback erhält wie üblich neben dem vorherigen Rückgabewert und dem aktuellen Elemente auch den Index des aktuellen Elements und das gesamte Array. Über ein zweites Argument von reduce() bzw. reduceRight() kann man den Startwert für den ersten Callback-Aufruf festlegen:

var arr = [1, 2, 3];

// Summiert alle Elemente des Arrays und den Startwert auf (Resultat: 10)
arr.reduce(function(prev, curr){
    return prev + curr;
}, 4);

IndexOf und LastIndexOf

Schon gewusst, dass indexOf() ein Teil von ES5 und damit eine so eine Art Neuheit ist? Zusammen mit lastIndexOf() dient es bei der Positionsbestimmung eines Elements in einem Arrray, wobei indexOf() den ersten Index und lastIndexOf() den letzten Index zurückgibt.

var arr = ["a", "b", "c", "a", "d"];
arr.indexOf("a");     // 0
arr.lastIndexOf("a"); // 3

Neben dem Element, nach dem in dem Array gesucht werden soll, kann auch ein Startindex für die Suche angegeben werden. Dabei sucht indexOf() von diesem Startindex aus vorwärts und lastIndexOf() rückwärts.

var arr = ["a", "b", "c", "a", "d"];
arr.indexOf("a", 1);     // 3
arr.lastIndexOf("a", 2); // 0

Wie geht es weiter?

Nachdem, wie zu erwarten war, uns der Blick auf die allmächtige Kompatibilitätstabelle freudig stimmt (außer in ältere IE funktioniert der Array-Teil von ES5 überall) bleibt die Frage wie es denn jetzt weitergeht. In Sachen ES5 gibt es nicht mehr viel zu berichten, denn alles wirklich neue haben wir bereits abgearbeitet. Dinge wie JSON und String.prototype.trim sind zwar streng genommen auch ES5, sind aber auch bereits allgemein bekannt und von den Browsern gut unterstützt.

In den folgenden Teilen der Serie werden wir daher noch weiter in die Zukunft vorstoßen. ECMAScript 5 ist ja streng genommen schon ein altes Eisen – immerhin datieren die Spezifikationen vom Dezember 2009. Zeit also, sich mit dem wirklich Neuen zu befassen, das zur Zeit noch den Arbeitstitel „ECMAScript Harmony“ trägt. Das wenige davon, das man tatsächlich schon anfassen kann, ist nur punktuell in Browsern implementiert und es ist nicht gesagt, dass es irgendwann in seiner heutigen Form auch Standard wird, aber zum experimentieren reicht allemal. Themen wie Traceur, Node.js und CoffeeScript werden wir sicher auch mal anschneiden können.

Folgt mir

Kauft mein Zeug!

Cover des HTML5-Buchs
Infos auf html5-buch.de, kaufen bei Amazon

Cover der HTML5-DVD
Infos bei Galileo Press, kaufen bei Amazon

Promote JS

Learning JavaScript