Canvas, kann das was?

Mit Canvas in die Zukunft der Plug-in-freien multimedialen Websites
Kommentare

HTML5 ist das Buzzword, bei dem Webentwickler und Gestalter in den vergangenen Jahren große Augen bekommen und lange Ohren gemacht haben. Abgesehen von neuen semantischen Möglichkeiten den Code besser strukturieren zu können als es mit XHTML schon möglich war, standen der Webgemeinde nun vor allem neue multimediale Wege offen. Wir betrachten dazu Canvas etwas genauer.

Früher klöppelten die Unternehmen eigene, zumeist Plug-in-basierte, Lösungen zusammen und versuchten, sie durch rohe Marktdominanz zu Quasi-Standards zu erheben. Das zahlte sich für die Hersteller spätestens dann aus, wenn Entwickler und Unternehmen entsprechende Tools lizensieren mussten. Um grafische und interaktive Inhalte performant abzubilden, setze man hier vor allem auf Flash-basierte Technologien. Zumeist um pixelgenaue Websites, Videoplayer, Trickfilme, kleine Spiele, Rich Internet Applications oder Visualisierungen jeder Art zu erstellen. Trotz der Tatsache, dass Flash zunächst auf dem System installiert werden muss, erreichte es weltweit eine enorme Abdeckung und Akzeptanz.

Postweb 2.0

Diese Vorherrschaft begann vor einigen Jahren zu bröckeln. JavaScript wurde wesentlich performanter und HTML5 bot Entwicklern nun endlich standardisierte Lösungen, mit Videos, Audio und Echtzeitgrafiken besser umgehen zu können. Besser als je zuvor.

Eine wirklich sehenswerte und besondere Erweiterung ist . Das DOM-Element selbst erscheint zunächst funktional sehr eingeschränkt. Für modernes HTML relativ unüblich, erwarten Canvas und das Drawing API in der Tat exakte und möglichst ganzzahlige Pixelmaße. Vergleichbar mit einem Schachbrett, besitzt Canvas eine feste Breite und feste Höhe. Jedes Feld des Rasters besitzt eine bezeichnende Koordinate. An jeder Koordinate ist exakt ein Farbwert hinterlegt. Das Koordinatensystem ist, wie bei fast überall in der Computergrafik, auch hier auf dem Kopf stehend.

Nun vermittelte man als Entwickler den Gestaltern jahrelang, dass Websites flexibel seien, dass sie nicht passgenau umgesetzt werden können und man idealerweise, am besten responsive Layouts plane. Ist das jetzt alles überholt? Entwickelt man also zukünftig Webseiten nur noch in Canvas?

Nein, im Gegenteil. Der allgemeine Trend entwickelt sich auch weiterhin viel stärker in Richtung relativer und flexibler Layouts. Canvas ist eher als ein neues Werkzeug zu betrachten, mit dem es möglich wird, Anforderungen innerhalb derselben Standards umzusetzen, die sonst ein klassischer Fall für Plug-ins gewesen wären. Gerade was echte Rastergrafiken betrifft, stößt man mit den herkömmlichen Board-Mitteln des DOM schnell an gewisse Grenzen.

Anwendungsszenarien

Google hat es vorgemacht und propagierte mit Chrome OS [1], gänzlich auf Onlineanwendungen zu setzen. Doch wie würde man beispielsweise ein Bildbearbeitungsprogramm auf dem Niveau kommerzieller clientseitiger Software nachbilden? Findigen Webentwicklern fallen sicherlich direkt Lösungsansätze unter Verwendung von Backend-Technologien ein. In der Tat ist es aber absurd, für jede kleine Pixeländerung die Bilddaten über das Netz zu schicken.

Weitere klassische Anwendungsfälle für Canvas sind unter anderem auch Diagramme. Während sich Balkendiagramme auch mit herkömmlichen serverseitigen Lösungsansätzen noch denkbar realistisch umsetzen ließen, sieht es beispielsweise bei Kurvendiagrammen bereits ganz anders aus. Erst recht, wenn man eventuell vor hat, den Verlauf des Graphen live zu aktualisieren. Hier bietet sich clientseitig des Weiteren der Vorteil, interaktiv auf die Diagramme Einfluss nehmen zu können. Bisher war es üblich, Diagramme serverseitig rendern und von Browser abholen zu lassen. Hier entsteht nicht nur vermeidbare Rechenlast auf dem Server, sondern es müssten unter Umständen wesentlich mehr Informationen übertragen werden, als eine Aktualisierung benötigt.

First Steps

Einzeln und für sich betrachtet ist Canvas das vermutlich unspektakulärste Element, dass das HTML5 DOM zu bieten hat. Erzeugt man über die Definition eine Instanz (Listing 1), so stellen Browser den neuen Bereich lediglich als durchsichtige Fläche dar. Die innere und äußere Größe der Fläche wird initial über die HTML-Attribute width und height festgelegt. Hierbei gibt es allerdings eine Besonderheit: Die Werte aus den Attributen width und height bestimmen Maße unterschiedlicher Umgebungen. Zum einen das Außenmaß mit Gültigkeit innerhalb des DOM. Zum anderen die Rastermaße innerhalb der Canvas-Logik selbst. Nutzt man jedoch CSS width oder height, verändern sich lediglich die Außenmaße des DOM-Elements. Das interne Pixelraster bleibt gleich. In der Darstellung skaliert der Browser dann das interne Raster auf eine größere bzw. kleinere Version. Dieses Verhalten kennt man in ähnlicher Form von .

  
    
    
  
  
    
    
    
  

Möchte man den Inhalt von Canvas verändern, ist man in jedem Fall auf JavaScript angewiesen. Jede Canvas-Instanz besitzt einen so genannten „Context“ [2]:

document.getElementById(‚ein-canvas‘).getContext(‚2d‘);

Der Parameter ‚2d‘ teilt Canvas mit, dass der Context für das 2-D Drawing API zur Verfügung gestellt werden soll. Darüber hinaus unterstützen viele aktuelle Browser noch einen Context-Typen für dreidimensionales Zeichnen. Die Technologie dahinter ist WebGL, ein mit OpenGL ES [3] verwandter Substandard [4].

Hierbei wird Canvas Funktion als simple „Projektionsfläche“ deutlich. Die Kontexte beinhalten Logik zum Zeichnen in einen Pixel-Puffer. Der Puffer selbst ist ein eindimensionales Array. Es ist viermal größer als Pixel innerhalb eines Canvas vorhanden sind. Bei einer Größe von 640 * 480 Pixel hat das Array also 1 228 800 Felder. Mit dem Index 0 beginnend als rot, grün, blau und alpha aufgebaut (RGBA). Wer mit also gerne wie damals auf dem Amiga mit Pixel-Puffern arbeitet, kann dies auch tun (Listing 2).

var imageData = context.getImageData(0, 0, canvas.width, canvas.height);
// manipulation von imageData.data
context.putImageData(imageData, 0, 0);

Canvas bedient sich dieses Puffers und erzeugt eine grafische Repräsentation des Inhalts. Dies erklärt wiederum auch, wie es dazu kommt, dass Canvas-Daten in anderen Rastermaßen vorliegen als sie im DOM angezeigt werden. Ein sehr wichtiger Unterschied zwischen dem 2-D API und WebGL ist, dass Canvas ausschließlich mit Software-Rendering (also CPU-gestützt) arbeitet. WebGL ist performanter, weil für die grafischen Berechnungen und das Rendering unmittelbar auf die Grafikhardware gebaut werden kann (GPU-gestützt). Der Trade-Off liegt in der Komplexität der APIs. Reines WebGL, ohne Frameworks oder Libraries, ist mit einem wesentlich höheren Aufwand verbunden. Dies rührt vor allem daher, da man sich sehr Low-Level und im dreidimensionalem Koordinatenraum bewegt. Canvas 2-D ist ein vergleichsweise schmal gehaltenes API, mit dem man vor allem zweidimensionale Probleme innerhalb eines fairen Aufwands umsetzen kann.

Der 2-D Context bietet neben der Manipulation des rohen Pixel-Puffers eine Vielzahl primitiver Zeichenmethoden. Durch die Kombination von Pfaden, Kurven und extern geladener Bilder können nahezu alle Formen auf dem Canvas gerendert werden. Für oft auftretende Aufgaben, wie Rechtecke, Kreise oder Text, existieren separate Methoden. Dem Context muss dabei vor jeder neuen Zeichnung mitgeteilt werden, welche Farbe die Außenlinie bzw. die Füllung haben soll. Diese Information kann als CSS kompatible Hex-Zahl oder als RGB(A-)Wert übergeben werden (Listing 3).

function draw() {
  var canvas  = document.getElementById('ein-canvas');
  context = canvas.getContext('2d'),
    randomX = canvas.width * Math.random(),
    randomY = canvas.height * Math.random(),
    r       = Math.round(Math.random() * 255),
    g       = Math.round(Math.random() * 255),
    b       = Math.round(Math.random() * 255),
    i;

  context.fillStyle   = '#000000';
  context.strokeStyle = 'rgb(' + r + ', ' + g + ', ' + b + ')';

  context.fillRect(0, 0, canvas.width, canvas.height);

  context.beginPath();
  context.moveTo(randomX, randomY);

  for(var i = 0; i < 100; i++) {
    randomX = canvas.width * Math.random();
    randomY = canvas.height * Math.random();
    context.lineTo(randomX, randomY)
  }

  context.stroke();
  context.closePath();
}

Canvas 2-D arbeitet mit einem imperativen Zeichenmodell. Das bedeutet, es wird nur dann gezeichnet, wenn der Context dazu aufgefordert wird. Die durchgeführten Änderungen werden unmittelbar auf dem Pixel-Puffer durchgeführt, sobald eine der Methoden context.fill() bzw. context.stroke() aufgerufen wird. Man kann es wie die Bestätigung einer Befehlskette betrachten. Denn die wirklich rudimentären Methoden wie context.lineTo(x, y) zeichnen nicht direkt, sondern sammeln Koordinaten und führen die Zeichnung erst nach Bestätigung durch fill oder stroke aus. Methoden wie context.fillRect(x, y, width, height) implizieren bereits über den Namen der Methode, wie die Form zu zeichnen ist. Hier ist keine nachträgliche Bestätigung nötig, die Zeichnung geschieht direkt.

Dabei entstehen keine aufbereiteten oder verbleibenden Objekte, wie man es vielleicht von APIs mit Szenegraphen kennt. Zeichnet man mit beispielsweise fillRect, bekommt man keine weiteren Anhaltspunkte darüber, dass ein Rechteck gezeichnet wurde. Bereits nach Rücksprung aus fillRect ist die eigentliche Information über die neue Zeichnung vergessen.

Allerdings ist der Context bei Weitem nicht stateless. Neben der oben erwähnten Konfigurationsmöglichkeit zu Strich- und Füllfarbe besteht darüber hinaus noch eine Art „Configuration Stack“. An diesen gelangt man allerdings nicht direkt. Via context.save() kann man die aktuelle Konfiguration sichern. Ändert man nun beispielsweise das Attribut context.fillStyle und ruft anschließend context.restore() auf, ist die Konfiguration wieder auf dem Stand von vor context.save(). context.save() kann beliebig oft hintereinander genutzt werden. Jeder Aufruf bedeutet, die aktuelle Konfiguration auf dem Stack abzulegen. Wie für einen Stack üblich, gilt: Was zuerst draufgekommen ist, kommt zuletzt wieder raus (First In Last Out [5], Listing 4).

(...)
context.fillStyle = '#ff0000';
context.save();

context.fillStyle = '#00ff00';
context.save();

context.fillStyle = '#0000ff';
context.save();
alert(context.fillStyle); // '#0000ff'

context.restore();
alert(context.fillStyle); // '#0000ff'

context.restore();
alert(context.fillStyle); // '#00ff00'

context.restore();
alert(context.fillStyle); // '#ff0000'

(...)

Dieser Stack gilt allerdings nur für die Konfigurationen des Context-Objekts und nicht für den Pixelpuffer. Dieser bleibt zu jeder Zeit pro Instanz derselbe.

Wann ist einem diese Eigenschaft nützlich? Prinzipiell jedes Mal, wenn Einfluss auf den Context genommen wird und man den vorherigen Zustand zu einem späteren Zeitpunkt wiederherstellen möchte. Denkbar sinnvoll wäre dieses Feature beispielsweise innerhalb von Methoden, die Änderungen am Context vornehmen:

function drawSomething(context) {   context.save();   // (…)   // konfigurieren und zeichnen   // (…) context.restore(); }

So kann in jeden Fall sichergestellt werden, dass der Aufrufer von drawSomething(context) sich auch nach Aufruf von drawSomething(contex) auf seine zuvor durchgeführten Konfigurationen verlassen kann.

Transformationen

Nun mag man diese Vorgehensweise in Frage stellen, wenn es lediglich darum ginge, zwei Farbwerte in der Rückhand zu behalten. Wie bereits erwähnt, bezieht sich die Sicherung allerdings auf die gesamte Konfiguration. Dazu zählen unter anderem auch durchgeführte Transformationen.

Da Canvas, wie bekannt, keine Objekte pro gezeichneter Form erzeugt, werden Manipulationen auf dem Context selbst vorgenommen und hinterlegt. Um ein Rechteck beispielsweise in einem Winkel von 45 Grad gedreht dazustellen, muss man dem Kontext vor der Zeichenoperation durch context.fillRect dazu auffordern (Listing 5).

var PI_DIV_180 = Math.PI / 180,
    degree     = 45,
    radians    = degree * PI_DIV_180;

context.save();
context.translate(100, 100);
context.rotate(radians);
context.scale(0.5);

context.fillStyle = '#ff00ff';
context.fillRect(0, 0, 100, 100);
context.restore();

(...)

Transformationen sind ein starkes Werkzeug, wenn man komfortabel und ohne viel mathematisches Hintergrundwissen Verschiebungen, Drehungen oder Skalierungen beim Zeichnen vornehmen möchte. Hierzu stehen einem die entsprechenden Befehle context.translate(x, y), context.rotate(radians) und context.scale(scaleX, scaleY) zur Verfügung. translate sorgt dafür, dass jede Koordinate, die den Context passiert, um X und Y verschoben wird. scale kann zwei Skalierungsfaktoren handhaben: Ist nur der erste Parameter angegeben, gilt die Skalierung für X- und Y-Achse gleichermaßen. Es handelt sich hierbei um Float-Werte wobei ‚1.0‘ der Standardskalierung entspricht. Ein context.scale(0.5) entspricht der Verkleinerung auf die Hälfte der ursprünglichen Größe, context.scale(2.0) vergrößert auf das Doppelte des Ursprungs.

Zu context.rotate(radians) sollte man Folgendes wissen: Wie auch die Winkelfunktionen aus JavaScript Math (und in nahezu jeder anderen Programmiersprache), erwartet context.rotate die Winkelmaße als Bogenmaß (auch Radians genannt) [6]. Durch den alltäglichen Umgang mit Gradwinkelmaßen (45, 90, 180, 360), ist es für die meisten eher ungewohnt, in Bogenmaß (0.25π, 0.5π, π, 2π) zu denken. Daher lauert hier eine kleine Stolperfalle, die immer wieder gerne zu kurzzeitiger Verwirrung führt.

Dabei können Grad sehr gut in Winkelmaß umgerechnet werden. Ohne viel auf mathematische Hintergründe einzugehen, entspricht ein Grad exakt (Math.PI / 180) Bogenmaß. Somit lässt sich sehr einfach via degrees * (Math.PI / 180) dennoch Gradwinkel als Parameter für rotate nutzen. Das kommt einem unter anderem in einer Animation zugute. Hier erhöht man in der Regel bestimmte Werte in einem proportinalen Verhältnis (z. B. winkel++). Den Gradwinkel um eine natürliche Zahl zu verändern, fühlt sich allemal besser an und ist vermutlich auch direkter zu mappen als ein Bogenmaßwinkel um ein Vielfaches von (Math.PI / 180).

Animation

Zunächst sollte man sich bewusst werden, warum Animationen im Allgemeinen funktionieren. Das menschliche Auge ist verhältnismäßig träge. Bereits ab einer niedrigen Anzahl Bildwechsel in der Sekunde wird der eigentliche Wechsel nicht mehr wahrgenommen und als fließende Bewegung interpretiert [7]. Ein Daumenkino macht sich genau diese Gesetzmäßigkeit zu Nutzen. Genauso ist es auch bei der Animation auf Computern. Canvas ist so implementiert, dass es sehr gut darin ist, häufig wechselnde Bilddaten zu visualisieren. Als Faustregel gilt: Eine Aktualisierungsrate zwischen etwa 35 Frames per Second (FPS) bis 60 FPS reicht für die meisten Animationen aus (Listing 6).

var desiredFrames = 35,
    secondInMilis = 1000,
    interval      = secondInMillis / desiredFrames;

function draw() {
  // (...)
}

setInterval(function() {
  draw();
}, interval);

Entkoppelung ist hierbei äußerst wichtig, da das Drawing andernfalls den restlichen Programmablauf blockieren würde. setInterval(callback, interval) ist der denkbar naivste Ansatz zur entkoppelten Wiederholung. Denn setInterval läuft selbst dann weiter, wenn die Canvas-Animation gerade nicht sichtbar ist. Die Folgen sind unnütze Belastung der CPU und hochtourig drehende Lüfter. Notebook-User können sich somit ganz besonders darüber freuen, wenn eine minimierte Website ihnen dadurch zwar einen warmen Schoß bereitet, aber gleichzeitig auch die Akkuladung raubt.

Hier unterstützen moderne Browser ein ganz besonderes Feature: requestAnimationFrame(callback(time)) [8] sorgt dafür, dass der im Callback enthaltene Code nur dann ausgeführt wird, wenn das HTML-Dokument auch wirklich sichtbar ist.

Der Callback von setInterval wird stumpf nach einer bestimmten Zeitangabe neu getriggert. Daher ist es sehr wahrscheinlich, dass mehr gezeichnet wird als eigentlich dargestellt werden kann. Sprich, wenn der Browser oder das Endgerät weniger Frames pro Sekunde zeichnet als setInterval dazu auffordert, bleiben etliche Drawing Cycles ungenutzt.

Da requestAnimationFrame in der Regel mit dem Drawing Cycle der Browseranwendung gekoppelt ist, entfällt der beschriebene Nachteil. Im Gegenteil, an einigen Stellen profitiert man sogar davon.

Laut Spezifikation kann die Callback-Funktion, die an requestAnimationFrame übergeben wird, einen Parameter timestamp erhalten. Hierbei handelt es sich um einen Timestamp analog zu dem aus Date.now(). Wenn man es in seiner Animation berücksichtigt, kann man anhand der vergangenen Zeit die Werte und Positionen einer Animation manipulieren. Das ergibt vor allen Dingen bei instabilen oder zu hohen Aktualisierungsraten einen Sinn. In bestimmten Fällen – wie vielleicht bei Spielen – nimmt man es in Kauf, dass ein Spiel zeitweise sprunghaft oder ruckelig läuft, aber innerhalb des Spieluniversums mit der gewünschten Taktung und Geschwindigkeit synchron bleibt. Ansonsten bewegen sich alle Elemente nur so schnell, wie requestAnimationFrame gerade zu aktualisieren in der Lage ist. Da requestAnimationFrame ausserdem kein Drawing aufruft, wenn das HTML-Dokument gerade nicht sichtbar ist, kann es nützlich sein, nach Wiederanzeige eine Information darüber zu erhalten, um damit umzugehen (Listing 7).

function draw(timestamp) {
  var totalTime = timestamp - startTime();

  setTimeout(function(){
    requestAnimationFrame(draw);
  }, 1000 / 5);
}

var startTime = Date.now();
requestAnimationFrame(draw);

// Alternativ:
//   webkitRequestAnimationFrame(draw);
//   mozRequestAnimationFrame(draw);
//   oRequestAnimationFrame(draw);
//   msRequestAnimationFrame(draw);

Wie man es von vielen neu eingeführten CSS-Attributen kennt, ist auch requestAnimationFrame in den meisten Browsern mit einem Prefix versehen – und in älteren Browsern gar nicht vorhanden. Da JavaScript allerdings eine nahezu vollflexible Sprache ist, hat die Webcommunity auch hier schnell Wege gefunden ein entsprechendes Workaround zu entwickeln [9].

Optimierungen

Je mehr Frames pro Sekunde dargestellt werden können, desto flüssiger und realistischer kann eine Animation wirken. Hierbei ist jedoch zu beachten, dass jeder Frame zunächst durch das Programm berechnet und gezeichnet werden muss. Canvas 2-D als Software-Rendering-orientiertes API stößt dabei schnell an eine Performanceobergrenze. Das Ergebnis sind ruckelnde oder langsam ablaufende Animationen. Viele performancebedingte Probleme lassen sich durch ein paar Kniffe schnell eindämmen.

Ein wesentlich wichtiger Punkt, den man sich merken sollte wenn man mit Canvas arbeitet, ist: Canvas-Koordinaten sind Integer oder, passender formuliert, ganze Zahlen! Obwohl das Drawing API auch Fließkommazahlen als gültige Werte entgegennimmt, merkt man spätestens beim Zeichnen, dass sich hier einige Frames gewinnen lassen. Sprich, vor jedem Zeichnen sollten die Koordinaten und Längenangaben zu ganzen Zahlen gewandelt werden. Positiver Nebeneffekt: Die Kanten der gezeichneten Elemente bleiben scharf [10].

Dieser Ansatz ergibt einen Sinn, wenn man sich erneut vor Augen hält, dass das Drawing API auf einem auf einem Pixel-Puffer, also einem Array, arbeitet. Arrays kennen keine Float Indicies. Also interpoliert hier der Browser bzw. das 2-D API. Das führt zu einem markanten Performanceverlust. Laut aktueller Benchmarks scheint dies insbesondere dem Browser Chrome nicht mehr viel auszumachen. Mozilla-Browser hingegen merken den Unterschied deutlich [11].

Sei es Fluch oder Segen, bewegen sich viele der Animationen und Spiele, die mit dem Canvas 2-D API umgesetzt werden können, auf einem technologischen Niveau, das dem gegen Mitte und Ende der 90er Jahre nahe kommt. Dadurch hat man unter anderem den Vorteil, aus den Erfahrungen der Grafikprogrammierung der 90er Jahre zu lernen. Also Zeiten, in denen Entwickler Animationen auch ohne zusätzliche 3-D-Hardware im Softwaremodus geschrieben haben.

Bitshifting mit Zahlenwerten ist dabei beispielsweise eine wesentliche Methode gewesen, um bestimmte Rechenoperationen sehr performant durchführen zu können [12]. Während Douglas Crockford in „JavaScript: The Good Parts“, aus Performancegründen davon abrät, Bitshifting zu nutzen [13], erzielen Bitwise Operations in aktuellen Benchmarks dennoch sehr gute Ergebnisse [14].

Auf Plattformen, auf denen Speicher kein Problem darstellt, kann man außerdem den Trade-off von Speicher gegen Performance eingehen. Wenn klar ist, dass sich bestimmte Werte (Koordinaten, Farben, Faktoren) niemals einen bestimmten Wertebereich verlassen werden, ist es oftmals sinnvoll, diese Wertepalette in einem Lookup-Table aufzubereiten. Farbpaletten von Plasmaeffekten oder ähnliche Animationen, die auf Winkelfunktion setzen, eigenen sich oftmals gut dazu.

Fazit

Canvas ist eine prima Sache! Im Grunde genommen ist es an der Zeit, umzudenken. Viele Methoden und Lösungen, die Entwickler über die Jahre zusammengetragen haben, sind Workarounds, um mit HTML Anforderungen umzusetzen, für die HTML weder gedacht noch geeignet sind.

Wer es gewohnt ist, wie bei ActionScript/Flash, mit vorgefertigten DisplayObjects und in einer Hierarchie zu arbeiten, sollte sich zuvor eine Bibliothek suchen, die diese Features bietet. Hardwarebeschleunigtes, dreidimensionales Rendering ist dank WebGL stark auf dem Vormarsch und Bibliotheken wie ThreeJS [15] ebnen bereits im Voraus den Weg.

Bei der Unterstützung und Brauchbarkeit von Canvas haben die WebKit-basierten Browser stark vorgelegt, was nicht zuletzt auch an einer sehr performanten Implementierung der JavaScript-Engines liegt.

Auf mobilen Endgeräten lässt Canvas allerdings noch sehr zu wünschen übrig. Wer hier mehr vor hat, als ein paar selten aktualisierte Inhalte zeichnen zu lassen, wird mit Canvas nicht viel Spaß haben. Für flüssig dargestellte Bewegungen, soweit das Endgerät dies leisten kann, sind CSS3-Transitions hier in der Regel der bessere Weg. Gerade WebKit-Browser und der Mobile Safari bringen eine sehr gute Unterstützung für Animations, Transitions und Transformations mit sich. Wer es benötigt, auch im dreidimensionalen Raum.

Canvas hat bereits jetzt die Entwicklung Plug-in-freier Multimediawebsites stark beeinflussen können. Die kommenden Jahre bleiben spannend!

Unsere Redaktion empfiehlt:

Relevante Beiträge

Meinungen zu diesem Beitrag

X
- Gib Deinen Standort ein -
- or -