Das Front-End von Mittelerde

Schritt-für-Schritt-Anleitung zur Entwicklung für mehrere Geräte

Daniel Isaksson
Daniel Isaksson
Einar Öberg
Einar Öberg

In unserem ersten Artikel über die Entwicklung des Chrome-Experiments Eine Reise durch Mittelerde ging es um die WebGL-Entwicklung für Mobilgeräte. In diesem Artikel erörtern wir die Herausforderungen, Probleme und Lösungen, auf die wir beim Erstellen des restlichen HTML5-Front-Ends gestoßen sind.

Drei Versionen derselben Website

Sehen wir uns zuerst einmal an, wie wir dieses Experiment im Hinblick auf Bildschirmgröße und Gerätefunktionen so anpassen, dass es sowohl auf Desktop-Computern als auch auf Mobilgeräten funktioniert.

Das gesamte Projekt basiert auf einem sehr „filmischen“ Stil, bei dem wir das Erlebnis in einem querformatigen festen Rahmen halten wollten, um die Magie des Films zu behalten. Da ein großer Teil des Projekts aus interaktiven Minispielen besteht, wäre es auch nicht sinnvoll, diese über den Frame hinaus zu lassen.

Nehmen wir die Landingpage als Beispiel dafür, wie wir das Design für verschiedene Größen anpassen.

Die Adler haben uns einfach auf die Landingpage gesetzt.
Die Adler haben uns gerade auf der Landingpage abgelegt.

Es gibt drei verschiedene Modi: Desktop-Computer, Tablets und Mobilgeräte. Nicht nur für das Layout, sondern auch, weil wir mit der Laufzeit geladene Assets verarbeiten und verschiedene Leistungsoptimierungen hinzufügen müssen. Bei Geräten, die eine höhere Auflösung als Desktop-Computer und Laptops haben, aber eine schlechtere Leistung als Smartphones haben, ist es nicht leicht, die endgültigen Regeln zu definieren.

Wir verwenden User-Agent-Daten, um Mobilgeräte zu erkennen, und einen Test mit der Größe des Darstellungsbereichs, um unsere Anzeigen auf Tablets auszurichten (645 Pixel und höher). Jeder Modus kann tatsächlich alle Auflösungen rendern, da das Layout auf Medienabfragen oder auf der relativen/Prozentsatzpositionierung mit JavaScript basiert.

Da die Designs in diesem Fall nicht auf Rastern oder Regeln basieren und zwischen den verschiedenen Abschnitten ziemlich unterschiedlich sind, hängt es wirklich vom spezifischen Element und Szenario in Bezug auf die zu verwendenden Haltepunkte oder Stile ab. Mehr als einmal kam es, dass wir das perfekte Layout mit netten Sass-Mixins und Medienabfragen eingerichtet hatten und dann einen Effekt hinzufügen mussten, der auf der Mausposition oder dynamischen Objekten basiert, und dann alles in JavaScript neu geschrieben haben.

Wir fügen auch eine Klasse mit dem aktuellen Modus im head-Tag hinzu, damit wir diese Informationen in unseren Stilen verwenden können, wie in diesem Beispiel (in SCSS):

.loc-hobbit-logo {

  // Default values here.

  .desktop & {
     // Applies only in desktop mode.
  }

 .tablet &, .mobile & {
   
   // Different asset for mobile and tablets perhaps.

   @media screen and (max-height: 760px), (max-width: 760px) {
     // Breakpoint-specific styles.
   }

   @media screen and (max-height: 570px), (max-width: 400px) {
     // Breakpoint-specific styles.
   }
 }
}

Wir unterstützen alle Größen bis auf etwa 360 x 320 Pixel, was bei der Erstellung eines immersiven Weberlebnisses eine ziemliche Herausforderung darstellt. Für Desktop-Computer gilt eine Mindestgröße, bevor Bildlaufleisten angezeigt werden, da wir möchten, dass Sie die Website nach Möglichkeit in einem größeren Darstellungsbereich sehen können. Für Mobilgeräte bieten wir sowohl das Quer- als auch das Hochformat bis hin zu den interaktiven Funktionen an, bei denen du das Gerät ins Querformat drehen musst. Das Argument war, dass die Website im Hochformat nicht so eindrucksvoll ist wie im Querformat, aber die Website hat ziemlich gut skaliert, also haben wir sie beibehalten.

Beachten Sie, dass das Layout nicht mit der Funktionserkennung wie Eingabetyp, Geräteausrichtung, Sensoren usw. verwechselt werden darf. Diese Funktionen können in allen diesen Modi vorhanden sein und sollten sich über alle erstrecken. Ein Beispiel dafür ist die gleichzeitige Unterstützung von Maus und Touch. Retina-Kompensation für die Qualität, aber vor allem Leistung ist eine andere, manchmal ist eine geringere Qualität besser. Der Canvas hat beispielsweise auf Retina-Displays nur die Hälfte der Auflösung in WebGL-Inhalten, die andernfalls viermal so viele Pixel rendern müssten.

Wir haben das Emulator-Tool während der Entwicklung häufig in den Entwicklertools verwendet, insbesondere in Chrome Canary mit neuen verbesserten Funktionen und vielen Voreinstellungen. Dies ist eine gute Möglichkeit, Designs schnell zu validieren. Wir mussten weiterhin regelmäßig auf echten Geräten testen. Das liegt unter anderem daran, dass sich die Website an den Vollbildmodus anpasst. Bei Seiten mit vertikalem Scrollen wird beim Scrollen in den meisten Fällen die Benutzeroberfläche des Browsers ausgeblendet. Bei Safari unter iOS7 treten derzeit Probleme auf, aber wir mussten alles unabhängig davon anpassen. Wir haben auch eine Voreinstellung im Emulator verwendet und die Einstellung für die Bildschirmgröße geändert, um den Verlust von verfügbarem Speicherplatz zu simulieren. Tests auf echten Geräten sind auch wichtig, um den Arbeitsspeicherverbrauch und die Leistung zu überwachen

Umgang mit dem Status

Nach der Landingpage landen wir auf der Karte von Mittelerde. Haben Sie bemerkt, dass sich die URL geändert hat? Die Website ist eine einseitige Anwendung, die für das Routing die History API verwendet.

Jeder Bereich der Website ist ein eigenes Objekt, das einen Textbaustein mit Funktionen wie DOM-Elemente, Übergänge, Laden von Assets, Entsorgen usw. übernimmt. Wenn Sie verschiedene Teile der Website erkunden, werden Abschnitte initiiert, Elemente zum DOM hinzugefügt und aus dem DOM entfernt und Assets für den aktuellen Abschnitt werden geladen.

Da der Nutzer jederzeit auf die Schaltfläche „Zurück“ des Browsers klicken oder durch das Menü navigieren kann, muss alles, was erstellt wird, irgendwann entsorgt werden. Zeitüberschreitungen und Animationen müssen gestoppt und verworfen werden. Andernfalls führen sie zu unerwünschtem Verhalten, Fehlern und Speicherlecks. Das ist nicht immer eine einfache Aufgabe, insbesondere wenn die Fristen näher rücken und Sie alles so schnell wie möglich erledigen müssen.

Präsentation der Standorte

Um die wunderschönen Schauplätze und die Charaktere von Mittelerde zu zeigen, haben wir ein modulares System aus Bild- und Textkomponenten entwickelt, das du horizontal ziehen oder wischen kannst. Wir haben hier keine Bildlaufleiste aktiviert, da wir unterschiedliche Geschwindigkeiten in verschiedenen Bereichen haben möchten, z. B. bei Bildsequenzen, bei denen die Bewegung seitlich gestoppt wird, bis der Clip abgespielt wurde.

Thranduils Halle
Thranduil's Hall – Zeitplan

Die Zeitachse

Zu Beginn der Entwicklung waren wir noch nicht mit dem Inhalt der Module für die einzelnen Standorte vertraut. Wir wussten, dass wir nach einer Vorlage suchten, um verschiedene Arten von Medien und Informationen in einer horizontalen Zeitleiste darzustellen, mit der wir sechs verschiedene Standortpräsentationen halten konnten, ohne alles sechsmal neu erstellen zu müssen. Dazu haben wir einen Zeitachsen-Controller entwickelt, der das Schwenken der Module basierend auf den Einstellungen und dem Verhalten der Module steuert.

Module und Verhaltenskomponenten

Die verschiedenen Module, die unterstützt werden, sind Bildsequenzen, Standbild, Parallaxe-Szene, Fokusverschiebungsszene und Text.

Das Parallaxe-Szenenmodul hat einen opaken Hintergrund mit einer benutzerdefinierten Anzahl von Ebenen, die den Fortschritt des Darstellungsbereichs für exakte Positionen abhören.

Die Fokus-Shift-Szene ist eine Variante des Parallaxe-Buckets. Außerdem verwenden wir für jede Ebene zwei Bilder, die ein- und ausgeblendet werden, um eine Fokusänderung zu simulieren. Wir haben versucht, den Unschärfefilter zu verwenden, aber er ist immer noch zu teuer. Daher warten wir auf CSS-Shader ab.

Der Inhalt im Textmodul ist mit dem TweenMax-Plug-in Draggable ziehbar. Sie können auch das Scrollrad verwenden oder mit zwei Fingern wischen, um vertikal zu scrollen. Beachten Sie das throw-props-plugin, das die fling-ähnliche Physik hinzufügt, wenn Sie wischen und loslassen.

Die Module können auch unterschiedliche Verhaltensweisen aufweisen, die als eine Reihe von Komponenten hinzugefügt werden. Sie alle haben ihre eigenen Zielauswahlen und -einstellungen. Übersetzen, um ein Element zu verschieben, zum Zoomen zu skalieren, Hotspots für Informations-Overlays, Debug-Messwerte für visuelle Tests, ein Start-Titel-Overlay, einen Flare-Layer und vieles mehr. Diese werden an das DOM angehängt oder ihr Zielelement innerhalb des Moduls gesteuert.

Damit können wir die verschiedenen Speicherorte nur mit einer Konfigurationsdatei erstellen, in der definiert ist, welche Assets geladen werden sollen, und die verschiedenen Arten von Modulen und Komponenten einrichten.

Bildsequenzen

Die Bildsequenz ist die größte Herausforderung im Hinblick auf Leistung und Downloadgröße. Zu diesem Thema gibt es eine Menge zu lesen. Auf Mobilgeräten und Tablets ersetzen wir dieses Bild durch ein Standbild. Es sind zu viele Daten, die decodiert und im Arbeitsspeicher gespeichert werden könnten, wenn wir eine angemessene Qualität auf Mobilgeräten wünschen. Wir haben verschiedene alternative Lösungen ausprobiert. Wir haben zuerst ein Hintergrundbild und ein Sprite Sheet verwendet, aber es führte zu Speicherproblemen und Verzögerungen beim Wechseln zwischen der GPU, wenn die GPU zwischen den Sprite Sheets wechseln musste. Dann haben wir versucht, die img-Elemente auszutauschen, aber es ging zu langsam. Am leistungsstärksten war es, einen Frame von einem Sprite Sheet auf einen Canvas zu zeichnen. Deshalb haben wir mit der Optimierung begonnen. Um bei jedem Frame Rechenzeit zu sparen, werden die in das Canvas zu schreibenden Bilddaten über ein temporäres Canvas-Element vorverarbeitet und mit "putImageData()" in einem Array gespeichert. Anschließend werden sie decodiert und sofort verwendet. Das ursprüngliche Sprite Sheet kann dann durch die automatische Speicherbereinigung gelöscht werden und es wird nur die Mindestmenge der Daten im Arbeitsspeicher gespeichert, die benötigt werden. Vielleicht ist es eigentlich weniger nötig, nicht codierte Bilder zu speichern, aber wir erzielen beim Scrubbing der Sequenz eine bessere Leistung. Die Frames sind nur 640 × 400 Pixel groß, sie sind aber nur beim Scrubbing sichtbar. Wenn Sie aufhören, wird ein hochauflösendes Bild geladen und kurz danach eingeblendet.

var canvas = document.createElement('canvas');
canvas.width = imageWidth;
canvas.height = imageHeight;

var ctx = canvas.getContext('2d');
ctx.drawImage(sheet, 0, 0);

var tilesX = imageWidth / tileWidth;
var tilesY = imageHeight / tileHeight;

var canvasPaste = canvas.cloneNode(false);
canvasPaste.width = tileWidth;
canvasPaste.height = tileHeight;

var i, j, canvasPasteTemp, imgData, 
var currentIndex = 0;
var startIndex = index * 16;
for (i = 0; i < tilesY; i++) {
  for (j = 0; j < tilesX; j++) {
    // Store the image data of each tile in the array.
    canvasPasteTemp = canvasPaste.cloneNode(false);
    imgData = ctx.getImageData(j * tileWidth, i * tileHeight, tileWidth, tileHeight);
    canvasPasteTemp.getContext('2d').putImageData(imgData, 0, 0);

    list[ startIndex + currentIndex ] = imgData;

    currentIndex++;
  }
}

Die Sprite Sheets werden mit Imagemagick generiert. Hier ist ein einfaches Beispiel auf GitHub, das zeigt, wie Sie ein Sprite Sheet aller Bilder in einem Ordner erstellen.

Module animieren

Um die Module auf der Zeitachse zu platzieren, verfolgt eine versteckte Darstellung der Zeitleiste, die außerhalb des Bildschirms angezeigt wird, den Abspielkopf und die Breite der Zeitleiste. Dies kann allein mit Code erledigt werden, aber dies war gut mit einer visuellen Darstellung bei Entwicklung und Debugging. Bei der tatsächlichen Ausführung wird nur die Größe auf die Abmessungen aktualisiert. Einige Module füllen den Darstellungsbereich aus, andere haben ihr eigenes Seitenverhältnis. Daher war es etwas schwierig, alles in allen Auflösungen zu skalieren und zu positionieren, damit alles sichtbar und nicht zu stark zugeschnitten ist. Jedes Modul hat zwei Fortschrittsanzeigen: eine für die sichtbare Position auf dem Bildschirm und eine für die Dauer des Moduls selbst. Bei Parallaxenbewegungen ist es oft schwierig, die Start- und Endposition von Objekten zu berechnen, damit sie sich mit der erwarteten Position synchronisieren, wenn sie sichtbar sind. Es ist gut zu wissen, wann ein Modul in der Ansicht zu sehen ist, seine interne Zeitleiste abspielt und wann es wieder aus dem Bild entfernt wird.

Über jedem Modul befindet sich eine dezente schwarze Schicht, die seine Deckkraft anpasst, sodass es vollständig transparent ist, wenn es sich in der Mitte befindet. So können Sie sich immer nur auf ein Modul nach dem anderen konzentrieren, was den Lernprozess verbessert.

Seitenleistung

Der Wechsel von einem funktionierenden Prototyp auf eine Release-Version ohne Verzögerungen bedeutet, dass Sie nicht mehr raten müssen, sondern wissen, was im Browser passiert. Hier kommen die Chrome-Entwicklertools ins Spiel.

Wir haben ziemlich viel Zeit in die Optimierung der Website investiert. Das Erzwingen der Hardwarebeschleunigung ist natürlich eines der wichtigsten Tools für flüssige Animationen. Aber auch nach farbigen Spalten und roten Rechtecken in den Chrome-Entwicklertools suchen. Es gibt viele gute Artikel zu diesen Themen, die Sie alle lesen sollten. Die Belohnung für das Entfernen überspringbarer Frames ist sofort, aber auch die Frustration, wenn sie wieder zurückkehren. Und sie werden es auch tun. Es ist ein fortlaufender Prozess, der Iterationen erfordert.

Ich verwende TweenMax von Greensock gerne zum Tweening von Eigenschaften, Transformationen und CSS. Betrachten Sie Ihre Struktur beim Hinzufügen neuer Ebenen in Containern. Beachten Sie, dass vorhandene Transformationen durch neue Transformationen überschrieben werden können. Die TranslateZ(0), die die Hardwarebeschleunigung in Ihrer CSS-Klasse erzwungen hat, wird durch eine 2D-Matrix ersetzt, wenn Sie nur 2D-Werte anpassen. Um die Ebene in diesen Fällen im Beschleunigungsmodus zu belassen, verwenden Sie die Eigenschaft „force3D:true“, um eine 3D-Matrix anstelle einer 2D-Matrix zu erstellen. Das geht schnell verloren, wenn Sie CSS- und JavaScript-Versionen kombinieren, um Stile festzulegen.

Erzwinge die Hardwarebeschleunigung nicht, wenn sie nicht benötigt wird. Der GPU-Arbeitsspeicher kann schnell gefüllt sein und zu unerwünschten Ergebnissen führen, wenn Sie viele Container hardwarebeschleunigen möchten. Das gilt insbesondere unter iOS, wo der Arbeitsspeicher mehr Einschränkungen hat. Das Laden kleinerer Assets, das Hochskalieren mit CSS und das Deaktivieren einiger Effekte im mobilen Modus führte zu erheblichen Verbesserungen.

Speicherlecks waren ein weiterer Bereich, in dem wir unsere Fähigkeiten verbessern mussten. Bei der Navigation zwischen den verschiedenen WebGL-Inhalten entstehen viele Objekte, Materialien, Texturen und Geometrie. Wenn diese noch nicht für die automatische Speicherbereinigung bereit sind, wenn Sie die Seite verlassen und den Abschnitt entfernen, können sie dazu führen, dass das Gerät nach einer Weile abstürzt, wenn nicht genügend Arbeitsspeicher vorhanden ist.

Einen Abschnitt mit einer fehlgeschlagenen Dispose-Funktion verlassen
Einen Abschnitt mit einer fehlgeschlagenen Dispose-Funktion beenden.
Das ist viel besser!
Viel besser!

Es war ein ziemlich unkomplizierter Workflow in den Entwicklertools, bei dem die Zeitleiste aufgezeichnet und Heap-Snapshots erfasst wurden. Es ist einfacher, bestimmte Objekte wie 3D-Geometrie oder eine bestimmte Bibliothek herauszufiltern. Im obigen Beispiel stellte sich heraus, dass die 3D-Szene noch da war und auch ein Array, in dem Geometrie gespeichert war, nicht gelöscht wurde. Wenn es schwierig ist, den Speicherort des Objekts zu finden, gibt es eine praktische Funktion, mit der Sie sich dies anzeigen lassen können: Pfade beibehalten. Klicken Sie einfach auf das Objekt, das Sie im Heap-Snapshot untersuchen möchten, und Sie sehen die Informationen in einem Bereich darunter. Die Verwendung einer guten Struktur mit kleineren Objekten hilft beim Auffinden Ihrer Referenzen.

Auf diese Szene wurde in EffectComposer verwiesen.
Auf die Szene wurde in EffectComposer verwiesen.

Im Allgemeinen ist es sinnvoll, sich nochmals genau Gedanken zu machen, bevor Sie das DOM manipulieren. Denken Sie dabei an Effizienz. Sie sollten das DOM in einer Spielschleife nicht manipulieren, wenn es möglich ist. Speichern Sie Verweise in Variablen zur Wiederverwendung. Wenn Sie nach einem Element suchen müssen, verwenden Sie die kürzeste Route, indem Sie Verweise auf strategische Container speichern und im nächstgelegenen Ancestor-Element suchen.

Verzögern Sie das Lesen von Dimensionen neu hinzugefügter Elemente oder beim Entfernen/Hinzufügen von Klassen, wenn Layoutfehler auftreten. Alternativ können Sie auch Layout auslösen. Manchmal ändert sich der Browser-Batch in Stile und wird nach dem nächsten Layout-Trigger nicht mehr aktualisiert. Das kann manchmal ein großes Problem sein, aber nicht ohne Grund. Versuche also zu verstehen, wie es hinter den Kulissen funktioniert, und du wirst eine Menge davon bekommen.

Vollbild

Sofern verfügbar, können Sie die Website über die Fullscreen API im Menü im Vollbildmodus anzeigen. Aber auf Geräten entscheiden sich auch die Browser dafür, den Vollbildmodus zu aktivieren. Safari unter iOS hatte zuvor einen Hack, mit dem Sie dies steuern konnten. Das ist aber nicht mehr verfügbar. Daher müssen Sie Ihr Design so vorbereiten, dass es ohne Scrollen funktioniert, wenn Sie eine Seite ohne Scrollen erstellen. Wir können in Zukunft wahrscheinlich mit Aktualisierungen rechnen, da viele Web-Apps nicht funktionieren.

Assets

Animierte Anleitung für die Experimente
Animierte Anleitung für die Tests

Auf der gesamten Website gibt es viele verschiedene Arten von Assets, zum Beispiel Bilder (PNG und JPEG), SVG (Inline und Hintergrund), Sprite Sheets (PNG), benutzerdefinierte Symbolschriften und Adobe Edge-Animationen. Wir verwenden PNGs für Assets und Animationen (Spritesheets), bei denen das Element nicht vektorbasiert sein kann. Andernfalls versuchen wir, so oft wie möglich SVGs zu verwenden.

Das Vektorformat bedeutet keinen Qualitätsverlust, auch wenn es skaliert wird. 1 Datei für alle Geräte.

  • Kleine Dateigröße.
  • Wir können jeden Teil separat animieren (ideal für fortgeschrittene Animationen). Beispielsweise verbergen wir den Untertitel des Hobbit-Logos (die Einöde von Smaug), wenn es verkleinert wird.
  • Es kann als SVG-HTML-Tag eingebettet oder als Hintergrundbild verwendet werden, ohne dass zusätzliche Elemente geladen werden, da es zusammen mit der HTML-Seite geladen wird.

Symbolschriftbilder haben hinsichtlich der Skalierbarkeit dieselben Vorteile wie SVG und werden anstelle von SVG für kleine Elemente wie Symbole verwendet, bei denen nur die Farbe geändert werden muss (Hover, Active usw.). Die Symbole sind zudem sehr leicht wiederverwendbar. Sie müssen nur die CSS-Eigenschaft "content" eines Elements festlegen.

Animationen

In einigen Fällen kann das Animieren von SVG-Elementen mit Code sehr zeitaufwendig sein, insbesondere wenn die Animation während des Designprozesses häufig geändert werden muss. Um den Workflow zwischen Designern und Entwicklern zu verbessern, verwenden wir für einige Animationen (die Anweisungen vor den Spielen) Adobe Edge. Der Animationsworkflow ist sehr nah an Flash und das hat dem Team geholfen. Es gibt jedoch auch ein paar Nachteile, insbesondere bei der Integration der Edge-Animationen in unseren Asset-Ladeprozess, da dieser über eigene Loader und Implementierungslogik verfügt.

Ich glaube, wir haben noch einen langen Weg vor uns, bis wir einen perfekten Workflow für die Verarbeitung von Assets und selbst erstellten Animationen im Web haben. Wir sind gespannt, wie sich Tools wie Edge weiterentwickeln werden. In den Kommentaren kannst du auch Vorschläge zu anderen Animationstools und Workflows hinzufügen.

Fazit

Wenn nun alle Teile des Projekts veröffentlicht sind und wir uns das Endergebnis ansehen, muss ich sagen, dass wir vom Status moderner mobiler Browser ziemlich beeindruckt sind. Zu Beginn dieses Projekts hatten wir viel weniger Erwartungen an die nahtlose, integrierte und leistungsfähige Lösung. Das war eine großartige Lernerfahrung für uns und die Zeit, die wir viel durch Iteration und Tests verbracht haben, hat unser Verständnis dafür verbessert, wie moderne Browser funktionieren. Und genau das brauchen wir, wenn wir die Produktionszeit für diese Art von Projekten verkürzen wollen – vom Raten bis zum Wissen.