Frontend Śródziemia

Przewodnik po tworzeniu aplikacji na wiele urządzeń

pierwszym artykule na temat tworzenia eksperymentu w Chrome Podróż przez Śródziemie skupiliśmy się na tworzeniu WebGL na urządzenia mobilne. W tym artykule omawiamy wyzwania, problemy i rozwiązania, które napotkaliśmy podczas tworzenia reszty front-endu HTML5.

3 wersje tej samej witryny

Na początek omówimy dostosowanie tego eksperymentu do pracy na komputerach i urządzeniach mobilnych pod kątem rozmiaru ekranu i możliwości urządzeń.

Cały projekt jest oparty na bardzo „filmowym” stylu. Chcieliśmy, aby w ramach projektu zachować ułożenie w układzie poziomym, aby zachować magię filmu. Ponieważ większość projektu składa się z interaktywnych minigier, nie ma sensu pozwalać im na wypełnianie ramki.

Przykładem dostosowywania projektu do różnych rozmiarów może być strona docelowa.

Orły właśnie wyrzuciły nas na stronę docelową.
The eagles just dropped us at the landing page.

Witryna ma 3 tryby: na komputerze, na tablecie i na urządzeniu mobilnym. Nie tylko ze względu na układ, ale też dlatego, że musimy obsługiwać zasoby wczytywane w czasie działania i dodawać różne optymalizacje wydajności. W przypadku urządzeń o wyższej rozdzielczości niż komputery stacjonarne i laptopy, ale o gorszej wydajności niż telefony, nie jest łatwo zdefiniować ostateczny zestaw zasad.

Do wykrywania urządzeń mobilnych używamy danych o pakiecie klienta użytkownika, a do kierowania na tablety (o wymiarach 645 px i większych) – testu rozmiaru widoku. Każdy tryb może renderować wszystkie rozdzielczości, ponieważ układ jest oparty na zapytaniach dotyczących multimediów lub pozycjonowaniu względnym/procentowym za pomocą JavaScript.

W tym przypadku projekty nie są oparte na siatkach ani regułach i są dość unikalne w różnych sekcjach, więc wybór punktów przecięcia lub stylów zależy od konkretnego elementu i sytuacji. Nie raz zdarzyło się, że ustawienie idealnego układu za pomocą sass-mixins i media-queries, a potem potrzebowaliśmy dodać efekt oparty na położeniu kursora myszy lub obiektach dynamicznych, co kończyło się przepisaniem wszystkiego w JavaScript.

Dodajemy też klasę z bieżącym trybem w tagu head, aby móc używać tych informacji w stylach, jak w tym przykładzie (w 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.
   }
 }
}

Obsługujemy wszystkie rozmiary do około 360 x 320, co było dość trudne przy tworzeniu wrażenia korzystania z internetu. Na komputerach mamy minimalny rozmiar, zanim wyświetlimy suwaki, ponieważ chcemy, aby użytkownicy mogli korzystać z witryny na większym widocznym obszarze, jeśli to możliwe. Na urządzeniach mobilnych zdecydowaliśmy się umożliwić korzystanie z urządzenia w orientacji poziomej i pionowej aż do momentu, gdy trzeba będzie przełączyć urządzenie w orientację poziomą, aby skorzystać z interakcyjnych funkcji. Argument przeciwko temu był taki, że w trybie pionowym nie jest tak wciągający jak w trybie poziomym, ale strona skalowała się całkiem nieźle, więc ją zachowaliśmy.

Pamiętaj, że układ nie powinien być mylony z wykrywaniem funkcji takich jak typ wprowadzania, orientacja urządzenia, czujniki itp. Te funkcje mogą występować we wszystkich tych trybach i powinny być dostępne we wszystkich trybach. Jednym z nich jest obsługa myszy i dotyku jednocześnie. Kompensacja za jakość w przypadku Retina, ale przede wszystkim wydajność to coś innego, czasami niższa jakość jest lepsza. Na przykład na ekranach Retina tło ma połowę rozdzielczości w porównaniu do WebGL, ponieważ w przeciwnym razie musiałoby renderować 4 razy więcej pikseli.

Podczas tworzenia często używaliśmy narzędzia emulatora w Narzędziach deweloperskich, zwłaszcza w Chrome Canary, który zawiera nowe, ulepszone funkcje i wiele wstępnie ustawionych wartości. Jest to dobry sposób na szybkie sprawdzenie projektu. Nadal musimy regularnie testować na prawdziwych urządzeniach. Jednym z nich jest to, że witryna dostosowuje się do trybu pełnoekranowego. W większości przypadków strony z przewijaniem pionowym ukrywają interfejs przeglądarki podczas przewijania (Safari w iOS 7 ma obecnie z tym problem), ale musieliśmy dopasować wszystko niezależnie od tego. Użyliśmy też w emulatorze wstępnie ustawionego rozmiaru i zmieniliśmy ustawienie rozmiaru ekranu, aby symulować utratę dostępnej przestrzeni. Testowanie na rzeczywistych urządzeniach jest też ważne, aby monitorować zużycie pamięci i wydajność.

Obsługa stanu

Po stronie docelowej wyświetla się mapa Śródziemia. Czy zauważyłeś/zauważyłaś zmianę adresu URL? Strona jest aplikacją na jednej stronie, która do obsługi kierowania korzysta z interfejsu History API.

Każda sekcja witryny jest osobnym obiektem, który dziedziczy szablon funkcji, takich jak elementy DOM, przejścia, wczytywanie zasobów, usuwanie itp. Gdy eksplorujesz różne części witryny, inicjowane są sekcje, elementy są dodawane do DOM i usuwane z niego, a także wczytywane są zasoby dla bieżącej sekcji.

Użytkownik może w dowolnym momencie kliknąć przycisk Wstecz w przeglądarce lub poruszać się po menu, dlatego wszystko, co zostało utworzone, musi zostać w pewnym momencie usunięte. Czasy oczekiwania i animacje muszą zostać zatrzymane i odrzucone, ponieważ w przeciwnym razie mogą powodować niepożądane działanie, błędy i wycieki pamięci. Nie zawsze jest to łatwe, zwłaszcza gdy zbliżają się terminy i musisz jak najszybciej dodać wszystkie treści.

Wyświetlanie lokalizacji

Aby pokazać piękne krajobrazy i postaci Śródziemia, stworzyliśmy modułowy system komponentów obrazów i tekstu, które można przeciągać lub przesuwać w poziomie. Nie włączyliśmy paska przewijania, ponieważ chcemy mieć różne prędkości w różnych zakresach, na przykład w sekwencji obrazów, w której zatrzymujesz ruch poziomy, aż do końca klipu.

Sala Thranduila
Osoba: Thranduil

Oś czasu

Gdy rozpoczęliśmy prace nad rozwojem, nie wiedzieliśmy, jakie treści mają zawierać moduły w poszczególnych lokalizacjach. Wiedzieliśmy, że chcemy mieć szablonowy sposób wyświetlania różnych typów multimediów i informacji na poziomej osi czasu, który da nam swobodę tworzenia 6 różnych prezentacji lokalizacji bez konieczności wielokrotnego odtwarzania wszystkiego. Aby to umożliwić, stworzyliśmy kontroler linii czasu, który obsługuje przesuwanie modułów na podstawie ustawień i zachowania modułów.

Moduły i komponenty zachowania

Dodaliśmy obsługę różnych modułów: sekwencji obrazów, obrazów statycznych, scen paralaksy, scen ze zmianą ostrości i tekstu.

Moduł sceny paralaksy ma nieprzezroczyste tło z niestandardową liczbą warstw, które reagują na postępy widocznego obszaru w celu określenia dokładnych pozycji.

Scena z przesunięciem punktu ostrości to wariant wiadra z paralaksą, z tym wyjątkiem, że w przypadku każdej warstwy używamy 2 obrazów, które stopniowo się pojawiają i znikają, aby symulować zmianę punktu ostrości. Próbowaliśmy użyć filtra rozmycia, ale jest on nadal zbyt drogi, więc poczekamy na shadery CSS.

Zawartość w module tekstowym można przeciągać za pomocą wtyczki TweenMax Draggable. Możesz też przewijać w pionie za pomocą kółka przewijania lub przesuwając palcami. Zwróć uwagę na throw-props-plugin, który dodaje fizykę typu rzucania, gdy przesuniesz i puścisz obiekt.

Mogą one też mieć różne zachowania, które są dodawane jako zestaw komponentów. Każdy z nich ma własne ustawienia i selektory kierowania. Przesuwanie elementów, powiększanie, hotspoty dla nakładki informacyjnej, dane debugowania do testowania wizualnego, nakładka tytułu startowego, warstwa efektu rozbłysku i inne. Zostaną one dołączone do DOM lub będą kontrolować element docelowy w module.

Dzięki temu możemy tworzyć różne lokalizacje za pomocą tylko pliku konfiguracyjnego, który określa, jakie zasoby wczytywać i jak konfigurować różne rodzaje modułów i komponentów.

Sekwencje obrazów

Największe problemy w zakresie wydajności i rozmiaru pobierania stwarza sekwencja obrazów. Na ten temat jest wiele artykułów. Na urządzeniach mobilnych i tabletach zastępujemy je nieruchomym obrazem. Jeśli chcemy uzyskać przyzwoitą jakość na urządzeniach mobilnych, musimy skompresować i zapisać w pamięci zbyt dużą ilość danych. Wypróbowaliśmy kilka alternatywnych rozwiązań, takich jak użycie obrazu tła i arkusza sprite’ów, ale prowadziło to do problemów z pamięcią i opóźnień, gdy GPU musiał przełączać się między arkuszami sprite’ów. Następnie spróbowaliśmy zamienić elementy img, ale to też było zbyt wolne. Największe obciążenie stanowiło nanoszenie ramki z arkusza sprite’ów na kanwę, więc zaczęliśmy optymalizować ten proces. Aby zaoszczędzić czas obliczeń dla poszczególnych klatek, dane obrazu, które mają zostać zapisane na płótnie, są wstępnie przetwarzane za pomocą tymczasowego płóta i zapisane za pomocą funkcji putImageData() w tablicy, dekodowane i gotowe do użycia. Pierwotny arkusz sprite’ów może zostać zebrany przez mechanizm usuwania niepotrzebnych danych, a my przechowujemy w pamięci tylko minimalną ilość danych. Może nie ma potrzeby przechowywania nieodkodowanych obrazów, ale dzięki temu uzyskujemy lepszą wydajność podczas przewijania sekwencji. Kadry są dość małe, bo mają tylko 640 x 400, ale będą widoczne podczas przewijania. Gdy się zatrzymasz, wczytuje się obraz o wysokiej rozdzielczości, który szybko zniknie.

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++;
  }
}

Arkusze sprite’ów są generowane za pomocą Imagemagick. Oto prosty przykład na GitHub pokazujący, jak utworzyć arkusz sprite’ów ze wszystkich obrazów w folderze.

Animowanie modułów

Aby umieścić moduły na osi czasu, musisz wyświetlić ukrytą reprezentację osi czasu, która śledzi pozycję odtwarzania i szerokość osi czasu. Można to zrobić za pomocą samego kodu, ale podczas tworzenia i debugowania przydatna jest wizualizacja. Gdy jest używany w realnym środowisku, jest tylko aktualizowany podczas zmiany rozmiaru, aby dopasować wymiary. Niektóre moduły wypełniają widok, a inne mają własny współczynnik, więc było trochę trudno dostosować i umieścić wszystko we wszystkich rozdzielczościach, aby wszystko było widoczne i nie było zbyt przycięte. Każdy moduł ma 2 wskaźniki postępu: jeden dla widocznej pozycji na ekranie, a drugi dla czasu trwania samego modułu. Podczas tworzenia ruchu paralaksy często trudno jest obliczyć początkową i końcową pozycję obiektów, aby zsynchronizować je z oczekiwaną pozycją, gdy są widoczne. Warto wiedzieć, kiedy dokładnie moduł pojawia się w widoku, odtwarza swoją wewnętrzną ścieżkę czasową i kiedy znika z pola widzenia.

Każdy moduł ma na górze subtelną czarną warstwę, która dostosowuje poziom przezroczystości, tak aby była w pełni przezroczysta, gdy moduł znajduje się w środku. Dzięki temu możesz skupić się na jednym module naraz, co zwiększy wygodę.

Wydajność strony

Przejście z działającego prototypu do wersji gotowej bez zacięcia oznacza przejście od zgadywania do wiedzy o tym, co dzieje się w przeglądarce. W takich sytuacjach Narzędzia deweloperskie w Chrome są Twoim najlepszym przyjacielem.

Poświęciliśmy sporo czasu na optymalizację witryny. Wymuszanie akceleracji sprzętowej to jedno z najważniejszych narzędzi do płynnego wyświetlania animacji. W Narzędziach deweloperskich w Chrome szukaj też kolorowe kolumny i czerwone prostokąty. Istnieje wiele dobrych artykułów na te tematy. Przeczytaj je wszystkie. Nagroda za usunięcie ramek pomijania jest natychmiastowa, ale tak samo jest z frustracją, gdy powrócą. I tak się stanie. Jest to ciągły proces, który wymaga iteracji.

Do interpolacji właściwości, przekształceń i CSS lubię używać TweenMax od Greensock. Myśl w kontenerach i wizualizuj swoją strukturę podczas dodawania nowych warstw. Pamiętaj, że nowe przekształcenia mogą zastąpić dotychczasowe. Jeśli chcesz stosować przejścia między wartościami tylko 2D, polecenie translateZ(0), które wymuszało akcelerację sprzętową w klasie CSS, zostanie zastąpione przez matrycę 2D. Aby w takich przypadkach zachować warstwę w trybie przyspieszania, użyj w przekształceniu interpolowanym właściwości „force3D:true”, aby utworzyć matrycę 3D zamiast matrycy 2D. Łatwo o tym zapomnieć, gdy łączysz animacje CSS i JavaScriptu, aby ustawić style.

Nie zmuszaj do korzystania z akceleracji sprzętowej, gdy nie jest to konieczne. Pamięć GPU może się szybko zapełnić i spowodować niepożądane wyniki, gdy chcesz użyć akceleracji sprzętowej dla wielu kontenerów, zwłaszcza na iOS, gdzie pamięć jest bardziej ograniczona. Ładowanie mniejszych zasobów i powiększanie ich za pomocą css oraz wyłączanie niektórych efektów w trybie mobilnym przyniosły ogromne ulepszenia.

Wycieki pamięci to kolejna dziedzina, w której musieliśmy doskonalić się w swoich umiejętnościach. Podczas przechodzenia między różnymi środowiskami WebGL tworzone jest wiele obiektów, materiałów, tekstur i geometrii. Jeśli nie są gotowe do usunięcia, gdy przejdziesz do innej sekcji i usuniesz ją, prawdopodobnie spowoduje to po pewnym czasie awarię urządzenia, gdy zabraknie mu pamięci.

Wyjście z sekcji z nieudaną funkcją dispose.
Wyjście z sekcji z nieudaną funkcją dispose.
O wiele lepiej.
Już lepiej!

Aby znaleźć wyciek, wystarczyło użyć prostej metody w Narzędziach deweloperskich, która polega na rejestrowaniu osi czasu i zapisywaniu zrzutów pamięci. Łatwiej jest, jeśli istnieją konkretne obiekty, takie jak geometria 3D lub konkretna biblioteka, które możesz odfiltrować. W powyższym przykładzie okazało się, że scena 3D nadal była obecna, a także tablica, która przechowywała geometrię, nie została wyczyszczona. Jeśli trudno Ci znaleźć lokalizację obiektu, możesz skorzystać z funkcji ścieżek zatrzymywania. Wystarczy kliknąć obiekt, który chcesz sprawdzić w migawce stosu, a informacje pojawią się w panelu poniżej. Użycie dobrej struktury z mniejszymi obiektami ułatwia znajdowanie plików referencyjnych.

Scena została wywołana w EffectComposer.
Scena została wywołana w EffectComposer.

Zasadniczo warto się dwa razy zastanowić, zanim zmodyfikujesz DOM. Pamiętaj o skuteczności. Jeśli to możliwe, nie manipuluj DOM-em w pętli gry. przechowywać odniesienia w zmiennych, aby można było ich używać ponownie; Jeśli chcesz wyszukać element, użyj najkrótszej ścieżki, przechowując odwołania do strategicznych kontenerów i wyszukując w najbliższym elemencie nadrzędnym.

Opóźnij odczyt wymiarów nowo dodanych elementów lub usuwanie/dodawanie klas, jeśli występują błędy układu. Sprawdź też, czy strona jest wyświetlana zgodnie z oczekiwaniami. Czasami przeglądarka zmienia ustawienia w przypadku stylów i nie aktualizuje się po następnym wywołaniu układu. Czasami może to być naprawdę duży problem, ale jest on tam z powodu. Dowiedz się, jak działa w tle, a zyskasz wiele.

Pełny ekran

Jeśli jest to możliwe, możesz ustawić tryb pełnoekranowy w menu za pomocą interfejsu Fullscreen API. Ale na urządzeniach przeglądarki mogą też wyświetlać reklamy na pełnym ekranie. W Safari na iOS istniał wcześniej sposób na kontrolowanie tego parametru, ale nie jest on już dostępny, więc musisz przygotować projekt tak, aby działał bez niego, gdy tworzysz stronę bez przewijania. Prawdopodobnie w przyszłych aktualizacjach wprowadzimy zmiany, ponieważ ta zmiana spowodowała problemy z wieloma aplikacjami internetowymi.

Zasoby

animowane instrukcje dotyczące eksperymentów;
Animowane instrukcje dotyczące eksperymentów.

W witrynie mamy wiele różnych typów komponentów: obrazy (PNG i JPEG), SVG (wstawione i tła), arkusze sprite (PNG), niestandardowe czcionki ikon i animacje Adobe Edge. Używamy plików PNG w przypadku zasobów i animacji (arkuszy sprite), w których element nie może być wektorowy. W pozostałych przypadkach staramy się używać plików SVG.

Format wektorowy oznacza brak utraty jakości nawet po powiększeniu. 1 plik na wszystkie urządzenia.

  • mały rozmiar pliku,
  • Możemy animować poszczególne części osobno (idealne rozwiązanie w przypadku zaawansowanych animacji). Na przykład, gdy logo Hobbita (Zniszczenie Smauga) jest zmniejszone, ukrywamy „podtytuł”.
  • Może być umieszczony jako tag HTML SVG lub użyty jako obraz tła bez dodatkowego wczytywania (jest wczytywany w tym samym czasie co strona HTML).

Czcionki ikon mają te same zalety co pliki SVG pod względem skalowalności i są używane zamiast plików SVG w przypadku małych elementów, takich jak ikony, w których przypadku wystarczy możliwość zmiany koloru (wskazanie, aktywność itp.). Ikony można też łatwo ponownie wykorzystać. Wystarczy ustawić w elemencie właściwość CSS „content”.

Animacje

W niektórych przypadkach animowanie elementów SVG za pomocą kodu może być bardzo czasochłonne, zwłaszcza gdy animacja wymaga wielu zmian w trakcie procesu projektowania. Aby usprawnić współpracę projektantów i programistów, do niektórych animacji (instrukcji przed grami) używamy Adobe Edge. Proces tworzenia animacji jest bardzo podobny do tego w Flashu, co ułatwiło nam pracę, ale ma też kilka wad, zwłaszcza w przypadku integracji animacji w Edge w procesie wczytywania zasobów, ponieważ ma on własne ładowarki i logikę implementacji.

Nadal mamy przed sobą długą drogę do stworzenia idealnego procesu obsługi zasobów i ręcznie tworzonych animacji w internecie. Z niecierpliwością czekamy na rozwój takich narzędzi jak Edge. W komentarzach możesz dodawać sugestie dotyczące innych narzędzi i procesów animacji.

Podsumowanie

Teraz, gdy wszystkie części projektu zostały już opublikowane, a my możemy przyjrzeć się ostatecznemu wynikowi, muszę przyznać, że jesteśmy pod wrażeniem stanu współczesnych przeglądarek mobilnych. Gdy zaczynaliśmy ten projekt, mieliśmy znacznie niższe oczekiwania co do tego, jak płynnie, zintegrowanie i skutecznie będziemy w stanie go zrealizować. Było to dla nas bardzo pouczające doświadczenie, a czas poświęcony na iteracje i testowanie (dużo) pozwolił nam lepiej poznać działanie nowoczesnych przeglądarek. I tego właśnie potrzebujemy, jeśli chcemy skrócić czas produkcji w przypadku tego typu projektów, przechodząc od zgadywania do pewności.