Die HTML5-Elemente <details> und <summary>

Veröffentlicht am 22. Juli 2011

Twitter- und G+-Gefolge bat mich, ein paar Zeilen über zwei neue HTML5-Elemente zu verlieren; also frisch ans Werk! Die Elemente <details> (Specs) und <summary> (Specs) gehören zu den wenigen HTML5-Neuheiten, mit denen sich auch ganz ohne JavaScript etwas spürbar neues in den Browser bringen lässt. Die Spezifikationen lassen uns wissen:

The details element represents a disclosure widget from which the user can obtain additional information or controls [...] The first summary element child of the element, if any, represents the summary or legend of the details. [...] The rest of the element's contents represents the additional information or controls.

Auf deutsch gesagt: der Inhalt eines <details>-Elements wird vor dem User so lange versteckt, bis der Surfer das im <details>-Element befindliche <summary>-Element anklickt oder anderweitig (z.B. via Tastaturnavigation) aktiviert. Als Inhalt gilt dabei alles bis auf das erste <summary>-Element – dieses bleibt immer sichtbar. Die beiden neuen Elemente bilden also zusammen ein interaktives Paket.

Nützlich kann dies zum Beispiel für das Verstecken von optionalen Interface-Elementen sein, die nicht ständig im Blickfeld des Nutzers sein müssen wie die Steuerung eines Videoplayers, Statusanzeigen oder Konfigurationsoptionen einer Webapp:

<video id="film" width="320" height="180" autoplay>
    <source src="video.mp4" type="video/mp4">
    <source src="video.webm" type="video/webm">
    <source src="video.ogv" type="video/ogg">
</video>

<details>
    <summary>Videosteuerung ein/ausblenden</summary>
    <p>
        <button id="start" onclick="start()">Start/Pause</button>
        <button id="stumm" onclick="stumm()">Stummschalten</button>
        <button id="lauter" onclick="lauter()">Lauter</button>
        <button id="leiser" onclick="leiser()">Leiser</button>
    </p>
</details>

In Browsern mit Unterstützung für <details> und <summary> (was Stand heute nur Chrome zu sein scheint) werden die Buttons erst sichtbar, nachdem man das <summary>-Element angeklickt hat:

Details und Summary für die Steuerung eines HTML5-Videoplayers

In Aktion (funktioniert nur in aktuellen Chrome-Versionen)

Wird kein <summary>-Element in dem <details>-Element gefunden, soll der Browser selbst eine <summary> mit einem Standardtext einfügen. Fügt man das open-Attribut ein (boolsches Attribut, in XHTML open="open", sonst einfach nur open), ist der Inhalt des <details>-Elements zu Beginn sichtbar. Chrome verhält sich auch bereits rundum standardkonform und implementiert sogar die korrekten User-Agent-Stylesheets.

In modernen Webapplikationen kann man die Funktionalität von <details> und <summary> sicher häufig gebrauchen, so dass man die Einführung der beiden neuen Elemente durchaus gutheißen kann – auch wenn man das Verhalten der beiden Neulinge mit wenigen Zeilen JavaScript nachbauen könnte. Die Unterscheid zwischen der <details>-<summary>-Kombination und einer JavaScript-Lösung sind im Einzelnen:

  • auch wenn die JavaScript-Lösung recht trivial wäre, sind <details> und <summary> noch einfacher einzubauen und über das open-Attribut zu steuern
  • <details> und <summary> funktionieren auch bei abgeschaltetem JavaScript
  • <details> und <summary> lasen sich, soweit ich das bisher beurteilen kann, nicht anpassen, Animationen und ähnliches sind nicht drin
  • Das <summary>-Element scheint zur Zeit noch nicht tastaturtauglich zu sein, weder lies es sich bei meinen Test antabben noch über einen Accesskey steuern

Angesichts der mangelnden Anpassbarkeit weiß ich nicht wirklich, ob sich <details> und <summary> außerhalb von Prototypen (wo die einfache Umsetzung von Vorteil ist) wirklich in naher Zukunft durchsetzen werden. Hinzu kommt die zur Zeit sehr sparsame Browserunterstützung. Zwar ließe sich ein JavaScript-Ersatz schnell programmieren, aber wenn man einen solchen Ersatz hat, der tastaturfreundlich und schick animiert ist, braucht man dann noch die Original-Elemente <details> und <summary>? Man wird sehen was zukünftige Entwicklungen mit sich bringen. Aber zumindest für Prototypen sind die beiden Elemente schon mal gut zu gebrauchen.

ECMAScript 5, die nächste Version von JavaScript – Teil 6: Function.prototype.bind

Veröffentlicht am 14. Juli 2011

Das letzte wirklich wichtige Feature von ES5, das wir noch nicht behandelt haben, ist die bind()-Methode von Function.prototype. Diese nützliche Funktion ist noch nicht in vielen Browsern nativ vorhanden, doch da der nötige Polyfill nur 20 Zeilen lang ist und ihn alle vernünftigen JavaScript-Bibliotheken (jQuery, MooTools und viele weitere) von Haus aus mitbringen, gibt es keinen Grund, bind() nicht einzusetzen.

Welches Problem löst bind()?

Innerhalb einer JavaScript-Funktion findet man immer (so gut wie immer – im Strict Mode nicht zwingend) ein Variable namens this vor, die das der Funktion zugehörige Objekt referenziert. Je nachdem wie man eine Funktion aufruft, ist das mal das eine, mal das andere Objekt:

var a = {
    foo: function(){
        console.log(this); // Hier ist "this" das Objekt "a"
    }
}

console.log(this); // Hier ist "this" das globale Objekt

function Make_B(){
    this.foo = function(){
        console.log(this); // Hier ist "this" das frisch erstellte Objekt "b"
    }
}
var b = new Make_B()
b.foo();

Wenn man eine Funktion via call() oder apply() aufruft, kann man bestimmen, was this sein soll und damit einige sehr clevere Dinge programmieren. Was aber, wenn wir bei der Erstellung der Funktion festlegen wollen, was this ist? Angenommen man wollte sich ein Widget programmieren, das bei einem Klick auf einen Button eine activate()-Funktion auslöst:

function Widget(){

    this.init = function(){
        var button = document.getElementById('clickme');
        button.onclick = function(){
            this.activate(); // Das geht schief - "this" ist hier der Button!
        }
    }

    this.activate = function(){
        alert('Widget aktiviert!')
    }

    this.init();

}

var my_widget = new Widget();

In diesem Fall referenziert this das Element, auf das wir den Event Listener setzen, also den Button. Das kann unter Umständen sinnvoll sein, hier wäre es uns aber lieber, wenn this unsere Widget-Instanz wäre. Für genau diese Fälle führt ES5 die bind()-Methode für Funktionen ein.

Was genau macht bind()?

Ruft man die bind()-Methode auf einer Funktion auf, wird eine neue Funktion zurückgegeben, die mit der Ursprungsfunktion so gut wie identisch ist, nur das this der neuen Funktion ist genau das Objekt, das man als erstes Argument an bind() übergeben hat. So lässt sich unser Widget-Problem einfach lösen:

function Widget(){

    this.init = function(){
        var button = document.getElementById('clickme');
        button.onclick = function(){
            this.activate(); // Jetzt geht es!
        }.bind(this);        // "this" der Funktion ist jetzt das "this" des Widgets!
    }

    this.activate = function(){
        alert('Widget aktiviert!')
    }

    this.init();

}

var my_widget = new Widget();

Dadurch, dass wir an den Eventhandler des Buttons bind(this) anhängen, wird das äußere this (das Widget) auch innen verwendet. bind() ist eine Methode von Function.prototype und steht damit (Browserunterstützung vorausgesetzt) allen Funktionen zur Verfügung.

Was kann bind() noch?

Zwar ist das Ändern von this der wohl häufigste Anwendungsfall von bind(), doch beim Erstellen der neuen Funktion kann auch die Liste der Argumente verändert werden. Jedes Argument von bind(), das auf das erste this-Argument folgt, wird als zusätzliches Argument vorne an die Argumentliste der Ursprungsfunktion angehängt:

// Gibt aus: ["A", "B", "C"]
var alt = function(){
    console.log(arguments);
}
alt("A", "B", "C");

// Gibt aus: ["foo", "bar", "A", "B", "C"]
var neu = alt.bind(null, "foo", "bar");
neu("A", "B", "C");

Das ist eine sehr nützliche Funktion für das Kapern von fremden Methoden. Wenn wir uns an die defineProperty()-Methode aus Teil 3 der Serie zurückerinnern, gab es dort das Problem, dass defineProperty() eine Methode des Object-Objekts und nicht von Object.prototype ist. Heißt also:

// Das hier hätten wir gern
meinObjekt.defineProperty('foo', bar);

// Aber das hier müssen wir schreiben
Object.defineProperty(meinObjekt, 'foo', bar);

Wie Molily in den Kommentaren von Teil 3 ganz richtig bemerkte, ist es aber mit nur einer Zeile Code möglich, mittels bind() ein defineProperty() für meinObjekt anzulegen:

meinObjekt.defineProperty = Object.defineProperty.bind(null, meinObjekt);

Das this ist in diesem Fall egal und daher null, aber dadurch, dass wir meinObjekt immer vor die Argumentliste von defineProperty() hängen, macht die Funktion auch als Methode von meinObjekt genau das, was sie soll.

Browserkompatibilität und Polyfills

An der Browserfront sieht es vergleichsweise mittelprächtig aus, denn nur die modernsten Surfprogramme (Firefox 4+, IE 9, Chrome) unterstützen bind() nativ. Aber es gibt auch zwei gute Nachrichten: erstens rüstet jede vernünftige JavaScript-Bibliothek (jQuery, MooTools, viele weitere) die Funktionalität von bind() nach und zweitens lässt sich selbst in Abwesenheit einer JS-Lib ein einfacher Polyfill schnell einbauen. Dieser ist zwar nicht völlig identisch mit der nativen Implementierung von bind(), was aber in 99,99% aller Fälle kein Problem sein dürfte.

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

Veröffentlicht am 30. Juni 2011

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

Veröffentlicht am 20. Juni 2011

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.