<?xml version="1.0" encoding="utf-8"?>
<feed xmlns="http://www.w3.org/2005/Atom">
  <id>https://web.dev/</id>
  <title>Daniel Isaksson on web.dev</title>
  <updated>2026-04-15T23:21:06Z</updated>
  <author>
    <name>Daniel Isaksson</name>
  </author>
  <link href="https://web.dev/authors/danielisaksson/feed.xml" rel="self"/>
  <link href="https://web.dev/"/>
  <icon>https://web-dev.imgix.net/image/T4FyVKpzu4WKF1kBNvXepbi08t52/rOydjtEeVxCf2G0gLuKA.png?auto=format</icon>
  <logo>https://web.dev/images/shared/rss-banner.png</logo>
  <subtitle>Technical Director at North Kingdom</subtitle>
  
  
  <entry>
    <title>The Hobbit Experience 2014</title>
    <link href="https://web.dev/hobbit2014/"/>
    <updated>2014-11-18T00:00:00Z</updated>
    <id>https://web.dev/hobbit2014/</id>
    <content type="html" mode="escaped">&lt;p&gt;In time for the new Hobbit movie “The Hobbit: The Battle of the Five Armies” we have worked on extending last year’s Chrome Experiment, &lt;a href=&quot;http://middle-earth.thehobbit.com/&quot; rel=&quot;noopener&quot;&gt;A Journey through Middle-earth&lt;/a&gt; with some new content. The main focus this time has been to widen the use of WebGL as more browsers and devices can view the content and to work with the WebRTC capabilities in Chrome and Firefox. We had three goals with this years experiment:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;P2P gameplay using WebRTC and WebGL on Chrome for Android&lt;/li&gt;
&lt;li&gt;Make a multi-player game that is easy to play and that is based on touch input&lt;/li&gt;
&lt;li&gt;Host on Google Cloud Platform&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id=&quot;defining-the-game&quot;&gt;Defining the game &lt;a class=&quot;headline-link&quot; href=&quot;https://web.dev/hobbit2014/#defining-the-game&quot;&gt;#&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;The game logic is built on a grid-based setup with troops moving on a game board. This made it easy for us to try out the gameplay on paper as we were defining the rules. Using a grid-based setup also helps with collision detection in the game to keep a good performance since you only have to check for collisions with objects in the same or neighbouring tiles.
We knew from the beginning that we wanted to focus the new game around a battle between the four main forces of Middle-earth, Humans, Dwarves, Elves and Orcs. It also had to be casual enough to be played within a Chrome Experiment and not have too many interactions to learn.
We started by defining five Battlegrounds on the Middle-earth map that serve as game-rooms where multiple players can compete in a peer-to-peer battle.
Showing multiple players in the room on a mobile screen, and allowing users to select who to challenge was a challenge in itself. To make the interaction and the scene easier we decided to only have one button to challenge and accept and only use the room to show events and who is the current king of the hill. This direction also resolved a few issues on the match-making side and allowed us to match the best candidates for a battle.
In our previous Chrome experiment &lt;a href=&quot;https://cubeslam.com/&quot; rel=&quot;noopener&quot;&gt;Cube Slam&lt;/a&gt; we learned that it takes a lot of work to handle latency in a multi-player game if the game’s result is relying on it. You constantly have to make assumptions of where the opponent’s state will be, where the opponent thinks that you are and sync that with animations on different devices. &lt;a href=&quot;http://www.gamasutra.com/blogs/MarkMennell/20140929/226628/Making_FastPaced_Multiplayer_Networked_Games_is_Hard.php&quot; rel=&quot;noopener&quot;&gt;This article&lt;/a&gt; explains these challenges in more detail. To make it a bit easier we made this game turn-based.&lt;/p&gt;
&lt;p&gt;The game logic is built on a grid-based setup with troops moving on a game board. This made it easy for us to try out the gameplay on paper as we were defining the rules. Using a grid-based setup also helps with collision detection in the game to keep a good performance since you only have to check for collisions with objects in the same or neighboring tiles.&lt;/p&gt;
&lt;h2 id=&quot;parts-of-the-game&quot;&gt;Parts of the game &lt;a class=&quot;headline-link&quot; href=&quot;https://web.dev/hobbit2014/#parts-of-the-game&quot;&gt;#&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;To make this multi-player game there are a few key parts that we had to build:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;A server side player management API handles users, match-making, sessions and game statistics.&lt;/li&gt;
&lt;li&gt;Servers to help establishing the connection between the players.&lt;/li&gt;
&lt;li&gt;An API for handling the AppEngine Channels API signaling used to connect and communicate with all the players in the game rooms.&lt;/li&gt;
&lt;li&gt;A JavaScript Game engine that handles the syncing of the state and the RTC messaging between the two players/peers.&lt;/li&gt;
&lt;li&gt;The WebGL game view.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id=&quot;player-management&quot;&gt;Player management &lt;a class=&quot;headline-link&quot; href=&quot;https://web.dev/hobbit2014/#player-management&quot;&gt;#&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;To support a large number of players we use many parallel game-rooms per Battleground. The main reason to limiting the number of players per game-room is to allow new players to reach the top of the leaderboard in reasonable time. The limit is also connected to the size of the json object describing the game-room sent through the Channel API that has a limit of 32kb.
We have to store players, rooms, scores, sessions and their relationships in the game. To do this, first we used &lt;a href=&quot;https://cloud.google.com/appengine/docs/python/ndb/&quot; rel=&quot;noopener&quot;&gt;NDB&lt;/a&gt; for entities and used the query interface to deal with relationships. NDB is an interface to the Google Cloud Datastore. Using NDB worked great in the beginning but we soon ran into a problem with how we needed to use it. The query was run against the &amp;quot;committed&amp;quot; version of the database (NDB Writes is explained at great length &lt;a href=&quot;https://cloud.google.com/appengine/docs/python/ndb/#writes&quot; rel=&quot;noopener&quot;&gt;in this in-depth article&lt;/a&gt;) which can have a delay of several seconds. But the entities themselves didn&#39;t have that delay as they respond directly from the cache. It might be a bit easier to explain with some example code:&lt;/p&gt;
&lt;div&gt;&lt;pre class=&quot;language-js&quot;&gt;&lt;code class=&quot;language-js&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;// example code to explain our issue with eventual consistency&lt;/span&gt;&lt;br /&gt;def &lt;span class=&quot;token function&quot;&gt;join_room&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;player_id&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; room_id&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt;&lt;br /&gt;    room &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; Room&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;get_by_id&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;room_id&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;br /&gt;    &lt;br /&gt;    player &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; Player&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;get_by_id&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;player_id&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;br /&gt;    player&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;room &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; room&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;key&lt;br /&gt;    player&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;put&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;br /&gt;    &lt;br /&gt;    &lt;span class=&quot;token comment&quot;&gt;// the player Entity is updated directly in the cache&lt;/span&gt;&lt;br /&gt;    &lt;span class=&quot;token comment&quot;&gt;// so calling this will return the room key as expected&lt;/span&gt;&lt;br /&gt;    player&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;room &lt;span class=&quot;token comment&quot;&gt;// = Key(Room, room_id)&lt;/span&gt;&lt;br /&gt;&lt;br /&gt;    &lt;span class=&quot;token comment&quot;&gt;// Fetch all the players with room set to &#39;room.key&#39;&lt;/span&gt;&lt;br /&gt;    players_in_room &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; Player&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;query&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;Player&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;room &lt;span class=&quot;token operator&quot;&gt;==&lt;/span&gt; room&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;key&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;fetch&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;br /&gt;    &lt;span class=&quot;token comment&quot;&gt;// = [] (an empty list of players)&lt;/span&gt;&lt;br /&gt;    &lt;span class=&quot;token comment&quot;&gt;// even though the saved player above may be expected to be in the&lt;/span&gt;&lt;br /&gt;    &lt;span class=&quot;token comment&quot;&gt;// list it may not be there because the query api is being run against the &lt;/span&gt;&lt;br /&gt;    &lt;span class=&quot;token comment&quot;&gt;// &quot;committed&quot; version and may still be empty for a few seconds&lt;/span&gt;&lt;br /&gt;&lt;br /&gt;    &lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;&lt;br /&gt;        &lt;span class=&quot;token literal-property property&quot;&gt;room&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; room&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;&lt;br /&gt;        &lt;span class=&quot;token literal-property property&quot;&gt;players&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; players_in_room&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;&lt;br /&gt;    &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;&lt;p&gt;After adding unit tests we could see the issue clearly and we moved away from the queries to instead keep the relationships in a comma separated list in &lt;a href=&quot;https://cloud.google.com/appengine/docs/python/memcache/&quot; rel=&quot;noopener&quot;&gt;memcache&lt;/a&gt;. This felt like a bit of a hack but it worked and the AppEngine memcache has a transaction-like system for the keys using the excellent “compare and set”-feature so now the tests passed again.&lt;/p&gt;
&lt;p&gt;Unfortunately memcache is not all rainbows and unicorns but comes with &lt;a href=&quot;https://cloud.google.com/appengine/docs/python/memcache/#Python_Limits&quot; rel=&quot;noopener&quot;&gt;a few limits&lt;/a&gt;, the most notable ones being the 1MB value size (can’t have too many rooms related to a battleground) and key expiration, or as &lt;a href=&quot;https://cloud.google.com/appengine/docs/python/memcache/#Python_How_cached_data_expires&quot; rel=&quot;noopener&quot;&gt;the docs&lt;/a&gt; explains it:&lt;/p&gt;
&lt;aside class=&quot;aside flow bg-state-info-bg color-state-info-text&quot;&gt;&lt;div class=&quot; flow&quot;&gt; In general, an application should not expect a cached value to always be available. &lt;/div&gt;&lt;/aside&gt;
&lt;p&gt;We did consider using another great key-value store, Redis. But at the time setting up a scalable cluster was a bit daunting and since we’d rather focus on building the experience than maintaining servers we didn’t go down that path. On the other hand the Google Cloud Platform recently released a simple &lt;a href=&quot;https://cloud.google.com/solutions/redis/click-to-deploy&quot; rel=&quot;noopener&quot;&gt;Click-to-deploy&lt;/a&gt; feature, with one of the options being a Redis Cluster so that would have been a very interesting option.&lt;/p&gt;
&lt;p&gt;Finally we found &lt;a href=&quot;https://cloud.google.com/appengine/docs/python/cloud-sql/&quot; rel=&quot;noopener&quot;&gt;Google Cloud SQL&lt;/a&gt; and moved the relationships into MySQL. It was a lot of work but eventually it worked great, the updates are now fully atomic and the tests still pass. It also made implementing the match-making and score-keeping a lot more reliable.&lt;/p&gt;
&lt;p&gt;Over time more of the data slowly has moved over from NDB and memcache to SQL but in general the player, battleground and room entities are still stored in NDB while the sessions and relationships between them all are stored in SQL.&lt;/p&gt;
&lt;p&gt;We also had to keep track on who was playing who and pair up players against each other using a matching mechanism that took into consideration the players skill level and experience. We based the match-making on the open-source library &lt;a href=&quot;https://github.com/sublee/glicko2&quot; rel=&quot;noopener&quot;&gt;Glicko2&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;Since this is a multi-player game we want to inform the other players in the room about events like “who entered or left”, “who won or lost” and if there is a challenge to accept. To handle this we built in the ability to receive notifications into the Player Management API.&lt;/p&gt;
&lt;h2 id=&quot;setting-up-webrtc&quot;&gt;Setting up WebRTC &lt;a class=&quot;headline-link&quot; href=&quot;https://web.dev/hobbit2014/#setting-up-webrtc&quot;&gt;#&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;When two players are matched up for a battle a signaling service is used to get the two matched peers talking to each other and to help start a peer connection.&lt;/p&gt;
&lt;p&gt;There are several third-party libraries you can use for the signaling service and that also simplifies setting up WebRTC. Some options are  &lt;a href=&quot;http://peerjs.com/&quot; rel=&quot;noopener&quot;&gt;PeerJS&lt;/a&gt;, &lt;a href=&quot;http://simplewebrtc.com/&quot; rel=&quot;noopener&quot;&gt;SimpleWebRTC&lt;/a&gt;, and &lt;a href=&quot;https://github.com/pubnub/webrtc&quot; rel=&quot;noopener&quot;&gt;PubNub WebRTC SDK&lt;/a&gt;. PubNub uses a hosted server solution and for this project we wanted to host on the Google Cloud Platform. The other two libraries use node.js servers that we could have installed on Google Compute Engine but we would also have to make sure it could handle thousands of concurrent users, something we already knew the Channel API can do.&lt;/p&gt;
&lt;p&gt;One of the main advantages of using the Google Cloud Platform in this case is scaling. Scaling the resources needed for an AppEngine project is easily handled through the Google Developers Console and no extra work is needed to scale the signaling service when using  the Channels API.&lt;/p&gt;
&lt;p&gt;There were some concerns about latency and how robust the Channels API is but we had previously used it for the CubeSlam project and it had proven to work for millions of users in that project so we decided to use it again.&lt;/p&gt;
&lt;p&gt;Since we didn’t choose to use a third-party library to help with WebRTC we had to build our own. Luckily we could re-use a lot of the work we did for the CubeSlam project. When both players have joined a session the session is set to “active”, and both players will then use that active session id to initiate the peer-to-peer connection through the Channel API. After that all communication between the two players will be handled over a &lt;a href=&quot;http://www.html5rocks.com/en/tutorials/webrtc/datachannels/&quot; rel=&quot;noopener&quot;&gt;RTCDataChannel&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;We also need STUN and TURN servers to help establishing the connection and cope with NATs and firewalls. Read more in depth about setting up WebRTC in the HTML5 Rocks article &lt;a href=&quot;http://www.html5rocks.com/en/tutorials/webrtc/infrastructure/&quot; rel=&quot;noopener&quot;&gt;WebRTC in the real world: STUN, TURN, and signaling&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;The number of TURN servers used also need to be able to scale depending on the traffic. To handle this we tested the &lt;a href=&quot;https://cloud.google.com/deployment-manager/overview&quot; rel=&quot;noopener&quot;&gt;Google Deployment manager&lt;/a&gt;. It allows us to dynamically deploy resources on Google Compute Engine and install TURN servers using a &lt;a href=&quot;https://gist.github.com/danielisaksson/5eb9680fb3c9e3166a33&quot; rel=&quot;noopener&quot;&gt;template&lt;/a&gt;. It’s still in alpha but for our purposes it’s worked flawlessly. For TURN server we use &lt;a href=&quot;https://code.google.com/p/coturn&quot; rel=&quot;noopener&quot;&gt;coturn&lt;/a&gt;, which is a very fast, efficient and seemingly reliable implementation of STUN/TURN.&lt;/p&gt;
&lt;h2 id=&quot;the-channel-api&quot;&gt;The Channel API &lt;a class=&quot;headline-link&quot; href=&quot;https://web.dev/hobbit2014/#the-channel-api&quot;&gt;#&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;The Channel API is used to send all communication to and from the game room on the client side. Our player Management API is using the Channel API for its notifications about game events.&lt;/p&gt;
&lt;p&gt;Working with the Channels API had a few speedbumps. One example is that since the messages can come unordered we had to wrap all messages in an object and sort them. Here’s some example code on how it works:&lt;/p&gt;
&lt;div&gt;&lt;pre class=&quot;language-js&quot;&gt;&lt;code class=&quot;language-js&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;var&lt;/span&gt; que &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;  &lt;span class=&quot;token comment&quot;&gt;// [seq, packet...]&lt;/span&gt;&lt;br /&gt;&lt;span class=&quot;token keyword&quot;&gt;var&lt;/span&gt; seq &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;&lt;br /&gt;&lt;span class=&quot;token keyword&quot;&gt;var&lt;/span&gt; rcv &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;-&lt;/span&gt;&lt;span class=&quot;token number&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;&lt;br /&gt;&lt;br /&gt;&lt;span class=&quot;token keyword&quot;&gt;function&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;send&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token parameter&quot;&gt;message&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;&lt;br /&gt;  &lt;span class=&quot;token keyword&quot;&gt;var&lt;/span&gt; packet &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token constant&quot;&gt;JSON&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;stringify&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;&lt;br /&gt;    &lt;span class=&quot;token literal-property property&quot;&gt;seq&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; seq&lt;span class=&quot;token operator&quot;&gt;++&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;&lt;br /&gt;    &lt;span class=&quot;token literal-property property&quot;&gt;msg&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; message&lt;br /&gt;  &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;&lt;br /&gt;  channel&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;send&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;packet&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;&lt;br /&gt;&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;br /&gt;&lt;br /&gt;&lt;span class=&quot;token keyword&quot;&gt;function&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;recv&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token parameter&quot;&gt;packet&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;&lt;br /&gt;  &lt;span class=&quot;token keyword&quot;&gt;var&lt;/span&gt; data &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token constant&quot;&gt;JSON&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;parse&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;packet&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;&lt;br /&gt;&lt;br /&gt;  &lt;span class=&quot;token keyword&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;data&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;seq &lt;span class=&quot;token operator&quot;&gt;&amp;amp;&lt;/span&gt;lt&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; rcv&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;&lt;br /&gt;    &lt;span class=&quot;token comment&quot;&gt;// ignoring message, older or already received&lt;/span&gt;&lt;br /&gt;  &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;else&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;data&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;seq &lt;span class=&quot;token operator&quot;&gt;&gt;&lt;/span&gt; rcv &lt;span class=&quot;token operator&quot;&gt;+&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;&lt;br /&gt;    &lt;span class=&quot;token comment&quot;&gt;// message from the future. queue it up.&lt;/span&gt;&lt;br /&gt;    que&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;push&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;data&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;seq&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; packet&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;&lt;br /&gt;  &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;else&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;&lt;br /&gt;    &lt;span class=&quot;token comment&quot;&gt;// message in order! update the rcv index and emit the message&lt;/span&gt;&lt;br /&gt;    rcv &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; data&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;seq&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;&lt;br /&gt;    &lt;span class=&quot;token function&quot;&gt;emit&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&#39;message&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; data&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;message&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;&lt;br /&gt;&lt;br /&gt;    &lt;span class=&quot;token comment&quot;&gt;// and now that we have updated the `rcv` index we &lt;/span&gt;&lt;br /&gt;    &lt;span class=&quot;token comment&quot;&gt;// will check the que for any other we can send&lt;/span&gt;&lt;br /&gt;    &lt;span class=&quot;token function&quot;&gt;setTimeout&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;flush&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;10&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;&lt;br /&gt;  &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;br /&gt;&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;br /&gt;&lt;br /&gt;&lt;span class=&quot;token keyword&quot;&gt;function&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;flush&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;&lt;br /&gt;  &lt;span class=&quot;token keyword&quot;&gt;for&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token keyword&quot;&gt;var&lt;/span&gt; i&lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token number&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt; i&lt;span class=&quot;token operator&quot;&gt;&amp;amp;&lt;/span&gt;lt&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;que&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;length&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt; i&lt;span class=&quot;token operator&quot;&gt;++&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;&lt;br /&gt;    &lt;span class=&quot;token keyword&quot;&gt;var&lt;/span&gt; seq &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; que&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;i&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;&lt;br /&gt;    &lt;span class=&quot;token keyword&quot;&gt;var&lt;/span&gt; packet &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; que&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;i&lt;span class=&quot;token operator&quot;&gt;+&lt;/span&gt;&lt;span class=&quot;token number&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;&lt;br /&gt;    &lt;span class=&quot;token keyword&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;data&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;seq &lt;span class=&quot;token operator&quot;&gt;==&lt;/span&gt; rcv &lt;span class=&quot;token operator&quot;&gt;+&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;&lt;br /&gt;      &lt;span class=&quot;token function&quot;&gt;recv&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;packet&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;&lt;br /&gt;      &lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;token comment&quot;&gt;// wait for next flush&lt;/span&gt;&lt;br /&gt;    &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;br /&gt;  &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;br /&gt;&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;&lt;p&gt;We also wanted to keep the different APIs of the site modular and separated from the hosting of the site and started up with using the &lt;a href=&quot;https://cloud.google.com/appengine/docs/python/modules/&quot; rel=&quot;noopener&quot;&gt;modules&lt;/a&gt; built into GAE. Unfortunately after getting it all to work in dev we realized that the Channel API &lt;a href=&quot;http://stackoverflow.com/questions/19163056/app-engine-python-modules-and-channel-service&quot; rel=&quot;noopener&quot;&gt;does not work&lt;/a&gt; with modules at all in production. Instead we moved to using separate GAE instances and ran into CORS problems that forced us to use an iframe &lt;a href=&quot;https://gist.github.com/danielisaksson/f3a1afefdfaffdb4d22f&quot; rel=&quot;noopener&quot;&gt;postMessage bridge&lt;/a&gt;.&lt;/p&gt;
&lt;h2 id=&quot;game-engine&quot;&gt;Game engine &lt;a class=&quot;headline-link&quot; href=&quot;https://web.dev/hobbit2014/#game-engine&quot;&gt;#&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;To make the game-engine as dynamic as possible we built the front end application using the &lt;a href=&quot;http://en.m.wikipedia.org/wiki/Entity_component_system&quot; rel=&quot;noopener&quot;&gt;entity-component-system (ECS)&lt;/a&gt; approach. When we started the development the wireframes and functional specification was not set, so it was very helpful to be able to add features and logic as the development progressed. For example, the &lt;a href=&quot;http://demo.northkingdom.com/hobbit/battle/entity/&quot; rel=&quot;noopener&quot;&gt;first prototype&lt;/a&gt; used a simple canvas-render-system to display the entities in a grid. A couple of &lt;a href=&quot;http://demo.northkingdom.com/hobbit/battle/ai/&quot; rel=&quot;noopener&quot;&gt;iterations later&lt;/a&gt;, a system for collisions was added, and one for AI-controlled players. In the middle of the project we could switch to a 3d-renderer-system without changing the rest of the code. When the networking parts was up and running the ai-system could be modified to use remote commands.&lt;/p&gt;
&lt;p&gt;So the basic logic of the multiplayer is to send the configuration of the action-command to the other peer through DataChannels and let the simulation act as if it’s an AI-player. On top of that there’s logic to decide which turn it is, if the player presses pass/attack-buttons, queue commands if they come in while the player still looking at the previous animation etc.&lt;/p&gt;
&lt;p&gt;If it was just two users switching turns, both peers could share the responsibility to pass the turn to the opponent when they were done, but there is a third player involved. The AI-system became handy again (not just for testing), when we needed to add enemies like spiders and trolls. To make them fit into the turn-based flow it had to be spawned and executed exactly the same on both sides. This was solved by letting one peer control the turn-system and send the current status to the remote peer. Then when it’s the spiders turn, the turn manager let the ai-system create a command which is sent to the remote user. Since the game-engine is just acting on commands and entity-id:s the game will be simulated just the same on both sides. All units can also have the ai-component which enables easy automated testing.&lt;/p&gt;
&lt;p&gt;It was optimal to have a simpler canvas-renderer in the beginning of development while focusing on the game logic. But the real fun started when the 3d version was implemented and the scenes came to life with environments and animations. We use &lt;a href=&quot;http://threejs.org/&quot; rel=&quot;noopener&quot;&gt;three.js&lt;/a&gt; as 3d-engine, and it was easy to get to a playable state because of the architecture.&lt;/p&gt;
&lt;p&gt;The mouse position is sent more frequently to the remote user and a 3d-light subtle hints about where the cursor is at the moment.&lt;/p&gt;
</content>
    <author>
      <name>Daniel Isaksson</name>
    </author>
  </entry>
  
  <entry>
    <title>The Front-end of Middle-earth</title>
    <link href="https://web.dev/hobbit-front-end/"/>
    <updated>2013-12-11T00:00:00Z</updated>
    <id>https://web.dev/hobbit-front-end/</id>
    <content type="html" mode="escaped">&lt;p&gt;In &lt;a href=&quot;http://www.html5rocks.com/en/tutorials/casestudies/hobbit/&quot; rel=&quot;noopener&quot;&gt;our first article&lt;/a&gt; about the development of the Chrome Experiment &lt;a href=&quot;http://middle-earth.thehobbit.com/&quot; rel=&quot;noopener&quot;&gt;A Journey Through Middle-earth&lt;/a&gt; we focused on WebGL development for mobile devices. In this article we discuss the challenges, problems and solutions we encountered when creating the rest of the HTML5 front-end.&lt;/p&gt;
&lt;h2 id=&quot;three-versions-of-the-same-site&quot;&gt;Three versions of the same site &lt;a class=&quot;headline-link&quot; href=&quot;https://web.dev/hobbit-front-end/#three-versions-of-the-same-site&quot;&gt;#&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;Let’s start by talking a bit about adapting this experiment to work on both desktop computers and mobile devices from a screen-size and device-capabilities perspective.&lt;/p&gt;
&lt;p&gt;The whole project is based on a very “cinematic” style, where we design-wise wanted to keep the experience within a landscape-oriented fixed frame to keep the magic from the movie. Since a large chunk of the project consists of interactive mini “games” it wouldn’t make sense to let them overflow the frame either.&lt;/p&gt;
&lt;p&gt;We can take the landing page as an example of how we adapt the design for different sizes.&lt;/p&gt;
&lt;figure&gt;
&lt;img alt=&quot;The eagles just dropped us at the landing page.&quot; decoding=&quot;async&quot; height=&quot;424&quot; loading=&quot;lazy&quot; sizes=&quot;(min-width: 800px) 800px, calc(100vw - 48px)&quot; src=&quot;https://web-dev.imgix.net/image/T4FyVKpzu4WKF1kBNvXepbi08t52/AxrZEfU6BKgkNRmkTznh.png?auto=format&quot; srcset=&quot;https://web-dev.imgix.net/image/T4FyVKpzu4WKF1kBNvXepbi08t52/AxrZEfU6BKgkNRmkTznh.png?auto=format&amp;w=200 200w, https://web-dev.imgix.net/image/T4FyVKpzu4WKF1kBNvXepbi08t52/AxrZEfU6BKgkNRmkTznh.png?auto=format&amp;w=228 228w, https://web-dev.imgix.net/image/T4FyVKpzu4WKF1kBNvXepbi08t52/AxrZEfU6BKgkNRmkTznh.png?auto=format&amp;w=260 260w, https://web-dev.imgix.net/image/T4FyVKpzu4WKF1kBNvXepbi08t52/AxrZEfU6BKgkNRmkTznh.png?auto=format&amp;w=296 296w, https://web-dev.imgix.net/image/T4FyVKpzu4WKF1kBNvXepbi08t52/AxrZEfU6BKgkNRmkTznh.png?auto=format&amp;w=338 338w, https://web-dev.imgix.net/image/T4FyVKpzu4WKF1kBNvXepbi08t52/AxrZEfU6BKgkNRmkTznh.png?auto=format&amp;w=385 385w, https://web-dev.imgix.net/image/T4FyVKpzu4WKF1kBNvXepbi08t52/AxrZEfU6BKgkNRmkTznh.png?auto=format&amp;w=439 439w, https://web-dev.imgix.net/image/T4FyVKpzu4WKF1kBNvXepbi08t52/AxrZEfU6BKgkNRmkTznh.png?auto=format&amp;w=500 500w, https://web-dev.imgix.net/image/T4FyVKpzu4WKF1kBNvXepbi08t52/AxrZEfU6BKgkNRmkTznh.png?auto=format&amp;w=571 571w, https://web-dev.imgix.net/image/T4FyVKpzu4WKF1kBNvXepbi08t52/AxrZEfU6BKgkNRmkTznh.png?auto=format&amp;w=650 650w, https://web-dev.imgix.net/image/T4FyVKpzu4WKF1kBNvXepbi08t52/AxrZEfU6BKgkNRmkTznh.png?auto=format&amp;w=741 741w, https://web-dev.imgix.net/image/T4FyVKpzu4WKF1kBNvXepbi08t52/AxrZEfU6BKgkNRmkTznh.png?auto=format&amp;w=845 845w, https://web-dev.imgix.net/image/T4FyVKpzu4WKF1kBNvXepbi08t52/AxrZEfU6BKgkNRmkTznh.png?auto=format&amp;w=964 964w, https://web-dev.imgix.net/image/T4FyVKpzu4WKF1kBNvXepbi08t52/AxrZEfU6BKgkNRmkTznh.png?auto=format&amp;w=1098 1098w, https://web-dev.imgix.net/image/T4FyVKpzu4WKF1kBNvXepbi08t52/AxrZEfU6BKgkNRmkTznh.png?auto=format&amp;w=1252 1252w, https://web-dev.imgix.net/image/T4FyVKpzu4WKF1kBNvXepbi08t52/AxrZEfU6BKgkNRmkTznh.png?auto=format&amp;w=1428 1428w, https://web-dev.imgix.net/image/T4FyVKpzu4WKF1kBNvXepbi08t52/AxrZEfU6BKgkNRmkTznh.png?auto=format&amp;w=1600 1600w&quot; width=&quot;800&quot; /&gt;
&lt;figcaption&gt;The eagles just dropped us at the landing page.&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;p&gt;The site has three different modes: desktop, tablet and mobile. Not just to handle layout, but because we need to handle runtime-loaded assets and add various performance optimizations. With devices that have a higher resolution than desktop computers and laptops but have worse performance than phones, it&#39;s not an easy task to define the ultimate set of rules.&lt;/p&gt;
&lt;p&gt;We’re using user-agent data to detect mobile devices and a viewport-size test to target tablets among those (645px and higher). Each different mode can in fact render all resolutions, because the layout is based on media queries or relative/percentage positioning with JavaScript.&lt;/p&gt;
&lt;p&gt;Since the designs in this case aren’t based on grids or rules and are quite unique between the different sections it really depends on the specific element and scenario as to what breakpoints or styles to use. It happened more than once that we had set up the perfect layout with nice sass-mixins and media-queries, and then we needed to add an effect based on the mouse position or dynamic objects, and ended up rewriting everything in JavaScript.&lt;/p&gt;
&lt;p&gt;We also add a class with the current mode in the head tag so we can use that info in our styles, like in this example (in SCSS):&lt;/p&gt;
&lt;div&gt;&lt;pre class=&quot;language-css&quot;&gt;&lt;code class=&quot;language-css&quot;&gt;&lt;span class=&quot;token selector&quot;&gt;.loc-hobbit-logo&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;&lt;br /&gt;&lt;br /&gt;  &lt;span class=&quot;token selector&quot;&gt;// Default values here.&lt;br /&gt;&lt;br /&gt;  .desktop &amp;amp;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;&lt;br /&gt;     // Applies only in desktop mode.&lt;br /&gt;  &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;br /&gt;&lt;br /&gt; &lt;span class=&quot;token selector&quot;&gt;.tablet &amp;amp;, .mobile &amp;amp;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;&lt;br /&gt;   &lt;br /&gt;   // Different asset for mobile and tablets perhaps.&lt;br /&gt;&lt;br /&gt;   &lt;span class=&quot;token atrule&quot;&gt;&lt;span class=&quot;token rule&quot;&gt;@media&lt;/span&gt; screen &lt;span class=&quot;token keyword&quot;&gt;and&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token property&quot;&gt;max-height&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; 760px&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token property&quot;&gt;max-width&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; 760px&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;&lt;br /&gt;     // Breakpoint-specific styles.&lt;br /&gt;   &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;br /&gt;&lt;br /&gt;   &lt;span class=&quot;token atrule&quot;&gt;&lt;span class=&quot;token rule&quot;&gt;@media&lt;/span&gt; screen &lt;span class=&quot;token keyword&quot;&gt;and&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token property&quot;&gt;max-height&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; 570px&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token property&quot;&gt;max-width&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; 400px&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;&lt;br /&gt;     // Breakpoint-specific styles.&lt;br /&gt;   &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;br /&gt; &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;br /&gt;&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;&lt;p&gt;We support all sizes down to about 360x320, which has been pretty challenging when making a immersive web experience. On desktop we have a minimum size before we show scrollbars because we want you to experience the site in a larger viewport if possible. On mobile devices we decided to allow both landscape and portrait mode all the way up to the interactive experiences, where we ask you to turn the device to landscape. The argument against this was that it’s not as immersive in portrait as in landscape; but the site scaled pretty well so we kept it.&lt;/p&gt;
&lt;aside class=&quot;aside flow bg-state-info-bg color-state-info-text&quot;&gt;&lt;div class=&quot; flow&quot;&gt; &lt;strong&gt;DeviceOrientation events&lt;/strong&gt; The content layout is controlled by breakpoints and CSS but we also need to handle the event in JavaScript to pause game-loop and keep the correct state. It turns out you can’t rely on the value in window.orientation because it’s not standard and varies across devices. Instead, listen to the event, but look for &lt;code&gt;window.innerWidth&lt;/code&gt; and &lt;code&gt;window.innerHeight&lt;/code&gt; to determine the orientation. &lt;/div&gt;&lt;/aside&gt;
&lt;p&gt;It’s important to note that layout shouldn’t be mixed up with feature detection like input type, device orientation, sensors etc. Those features can exist in all of these modes and should span across all. Supporting mouse and touch at the same time is one example. Retina compensation for quality but most of all performance is another, sometimes lesser quality is better. As an example the canvas is half the resolution in the WebGL experiences on retina displays, which would otherwise have to render four times the number of pixels&lt;/p&gt;
&lt;aside class=&quot;aside flow bg-state-info-bg color-state-info-text&quot;&gt;&lt;div class=&quot; flow&quot;&gt; You can easily try out all sizes right in your browser by &lt;a href=&quot;https://developers.google.com/chrome-developer-tools/docs/mobile-emulation&quot;&gt;emulating&lt;/a&gt; a device in Chrome DevTools. When switching between mobile, tablet and desktop versions you have to reload the site to use the correct dependencies and settings. &lt;/div&gt;&lt;/aside&gt;
&lt;p&gt;We frequently used the emulator tool in DevTools during development, especially in Chrome Canary which has new improved features and lots of presets. It is a good way of quickly validating design. We still needed to test on real devices regularly. One reason was because the site is adapting to fullscreen. Pages with vertical scroll hide the browser UI when scrolling in most cases (Safari on iOS7 has problems with this currently) but we had to fit everything independent of the that. We also used a preset in the emulator and changed the screen size setting to simulate the loss of available space. Testing on real devices is also important for monitoring memory-consumption and performance&lt;/p&gt;
&lt;h2 id=&quot;handling-the-state&quot;&gt;Handling the state &lt;a class=&quot;headline-link&quot; href=&quot;https://web.dev/hobbit-front-end/#handling-the-state&quot;&gt;#&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;After the &lt;a href=&quot;http://middle-earth.thehobbit.com/&quot; rel=&quot;noopener&quot;&gt;landing page&lt;/a&gt; we land at the map of Middle-earth. Did you notice the URL changing? The site is a single page application that uses the &lt;a href=&quot;http://diveintohtml5.info/history.html&quot; rel=&quot;noopener&quot;&gt;History API&lt;/a&gt; to handle &lt;a href=&quot;http://visionmedia.github.io/page.js/&quot; rel=&quot;noopener&quot;&gt;routing&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;Each section of the site is its own object inheriting a boilerplate of functionality such as DOM-elements, transitions, loading of assets, disposing etc. When you explore different parts of the site, sections are initiated, elements are added to and removed from the DOM and assets for the current section are loaded.&lt;/p&gt;
&lt;p&gt;Since the user can hit the browser’s back button or navigate via the menu at any time, everything that is created needs to be disposed of at some point. Timeouts and animations need to be stopped and discarded or they will cause unwanted behaviour, errors, and memory leaks. This is not always an easy task, especially when deadlines are approaching and you need to get everything in there as fast as possible.&lt;/p&gt;
&lt;aside class=&quot;aside flow bg-state-info-bg color-state-info-text&quot;&gt;&lt;div class=&quot; flow&quot;&gt; Keep calm and add those event listeners. Make a practice of adding a dispose function to every object. Watch out for leaving timers and tweens behind. If tweening, use the equivalent of &lt;code&gt;TweenMax.killTweensOf(foo)&lt;/code&gt; or save references and stop them from triggering callbacks. Remove runtime added DOM elements. Use profiling tools regulary to keep an eye on the memory consumption and leaks. &lt;/div&gt;&lt;/aside&gt;
&lt;h2 id=&quot;showing-off-the-locations&quot;&gt;Showing off the locations &lt;a class=&quot;headline-link&quot; href=&quot;https://web.dev/hobbit-front-end/#showing-off-the-locations&quot;&gt;#&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;To show off the beautiful settings and the characters of Middle-earth we built a modular system of image and text components that you can drag or swipe horizontally. We haven’t enabled a scrollbar here since we want to have different speeds on different ranges, like in image sequences where you stop the motion sideways until the clip has played out.&lt;/p&gt;
&lt;aside class=&quot;aside flow bg-state-info-bg color-state-info-text&quot;&gt;&lt;div class=&quot; flow&quot;&gt; A scrollbar sets expectations about the behavior of your site. It can be a bad user experience when a website hijacks this control. &lt;/div&gt;&lt;/aside&gt;
&lt;figure&gt;
&lt;img alt=&quot;Thranduil&amp;#x27;s Hall&quot; decoding=&quot;async&quot; height=&quot;71&quot; loading=&quot;lazy&quot; sizes=&quot;(min-width: 800px) 800px, calc(100vw - 48px)&quot; src=&quot;https://web-dev.imgix.net/image/T4FyVKpzu4WKF1kBNvXepbi08t52/ebf9JxwetAeDmWbqofcW.jpg?auto=format&quot; srcset=&quot;https://web-dev.imgix.net/image/T4FyVKpzu4WKF1kBNvXepbi08t52/ebf9JxwetAeDmWbqofcW.jpg?auto=format&amp;w=200 200w, https://web-dev.imgix.net/image/T4FyVKpzu4WKF1kBNvXepbi08t52/ebf9JxwetAeDmWbqofcW.jpg?auto=format&amp;w=228 228w, https://web-dev.imgix.net/image/T4FyVKpzu4WKF1kBNvXepbi08t52/ebf9JxwetAeDmWbqofcW.jpg?auto=format&amp;w=260 260w, https://web-dev.imgix.net/image/T4FyVKpzu4WKF1kBNvXepbi08t52/ebf9JxwetAeDmWbqofcW.jpg?auto=format&amp;w=296 296w, https://web-dev.imgix.net/image/T4FyVKpzu4WKF1kBNvXepbi08t52/ebf9JxwetAeDmWbqofcW.jpg?auto=format&amp;w=338 338w, https://web-dev.imgix.net/image/T4FyVKpzu4WKF1kBNvXepbi08t52/ebf9JxwetAeDmWbqofcW.jpg?auto=format&amp;w=385 385w, https://web-dev.imgix.net/image/T4FyVKpzu4WKF1kBNvXepbi08t52/ebf9JxwetAeDmWbqofcW.jpg?auto=format&amp;w=439 439w, https://web-dev.imgix.net/image/T4FyVKpzu4WKF1kBNvXepbi08t52/ebf9JxwetAeDmWbqofcW.jpg?auto=format&amp;w=500 500w, https://web-dev.imgix.net/image/T4FyVKpzu4WKF1kBNvXepbi08t52/ebf9JxwetAeDmWbqofcW.jpg?auto=format&amp;w=571 571w, https://web-dev.imgix.net/image/T4FyVKpzu4WKF1kBNvXepbi08t52/ebf9JxwetAeDmWbqofcW.jpg?auto=format&amp;w=650 650w, https://web-dev.imgix.net/image/T4FyVKpzu4WKF1kBNvXepbi08t52/ebf9JxwetAeDmWbqofcW.jpg?auto=format&amp;w=741 741w, https://web-dev.imgix.net/image/T4FyVKpzu4WKF1kBNvXepbi08t52/ebf9JxwetAeDmWbqofcW.jpg?auto=format&amp;w=845 845w, https://web-dev.imgix.net/image/T4FyVKpzu4WKF1kBNvXepbi08t52/ebf9JxwetAeDmWbqofcW.jpg?auto=format&amp;w=964 964w, https://web-dev.imgix.net/image/T4FyVKpzu4WKF1kBNvXepbi08t52/ebf9JxwetAeDmWbqofcW.jpg?auto=format&amp;w=1098 1098w, https://web-dev.imgix.net/image/T4FyVKpzu4WKF1kBNvXepbi08t52/ebf9JxwetAeDmWbqofcW.jpg?auto=format&amp;w=1252 1252w, https://web-dev.imgix.net/image/T4FyVKpzu4WKF1kBNvXepbi08t52/ebf9JxwetAeDmWbqofcW.jpg?auto=format&amp;w=1428 1428w, https://web-dev.imgix.net/image/T4FyVKpzu4WKF1kBNvXepbi08t52/ebf9JxwetAeDmWbqofcW.jpg?auto=format&amp;w=1600 1600w&quot; width=&quot;800&quot; /&gt;
&lt;figcaption&gt;&lt;a href=&quot;http://middle-earth.thehobbit.com/thranduils-hall&quot;&gt;Thranduil&#39;s Hall&lt;/a&gt; timeline&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;h3 id=&quot;the-timeline&quot;&gt;The timeline &lt;a class=&quot;headline-link&quot; href=&quot;https://web.dev/hobbit-front-end/#the-timeline&quot;&gt;#&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;When development started we didn’t know the content of the modules for each location. What we knew was that we wanted a templated way of showing different types of media and information in a horizontal timeline that would give us the freedom to have six different location presentations without having to rebuild everything six times. To manage this we created a timeline controller that handle the panning of its modules based on settings and the modules’ behaviours.&lt;/p&gt;
&lt;h3 id=&quot;modules-and-behaviour-components&quot;&gt;Modules and behaviour components &lt;a class=&quot;headline-link&quot; href=&quot;https://web.dev/hobbit-front-end/#modules-and-behaviour-components&quot;&gt;#&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;The different modules we added support for are image-sequence, still image, parallax scene, focus-shift scene and text.&lt;/p&gt;
&lt;p&gt;The parallax scene module has an opaque background with a custom number of layers that listens to the viewport progress for exact positions.&lt;/p&gt;
&lt;p&gt;The focus-shift scene is a variant of the parallax bucket, with the addition that we use two images for each layer which fades in and out to simulate a focus change. We tried to use the blur filter, but it’s still to expensive, so we’ll wait for CSS shaders for this.&lt;/p&gt;
&lt;p&gt;The content in the text module is drag-enabled with the TweenMax plugin &lt;a href=&quot;http://www.greensock.com/draggable/&quot; rel=&quot;noopener&quot;&gt;Draggable&lt;/a&gt;. You can also use the scrollwheel or two-finger swipe to scroll vertically. Note the &lt;a href=&quot;http://www.greensock.com/throwprops/&quot; rel=&quot;noopener&quot;&gt;throw-props-plugin&lt;/a&gt; that adds the fling-style physics when you swipe and release.&lt;/p&gt;
&lt;p&gt;The modules can also have different behaviours that are added as a set of components. They all have their own target selectors and settings. Translate to move an element, scale to zoom, hotspots for info overlay, debug metrics for testing visually, a start-title overlay, a flare layer, and some more. These will be appended to the DOM or controlling their target element inside the module.&lt;/p&gt;
&lt;p&gt;With this in place we can create the different locations with just a &lt;a href=&quot;https://gist.github.com/inear/7626665&quot; rel=&quot;noopener&quot;&gt;config file&lt;/a&gt; that defines what
assets to load and setup the different kinds of modules and components.&lt;/p&gt;
&lt;h3 id=&quot;image-sequences&quot;&gt;Image sequences &lt;a class=&quot;headline-link&quot; href=&quot;https://web.dev/hobbit-front-end/#image-sequences&quot;&gt;#&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;The most challenging of the modules from a performance and a download-size aspect is the image sequence. There’s a bunch to read about this &lt;a href=&quot;http://awardwinningfjords.com/2012/03/08/image-sequences.html&quot; rel=&quot;noopener&quot;&gt;topic&lt;/a&gt;. On mobile and tablets we replace this with a still image. It’s too much data to decode and store in memory if we want decent quality on mobile. We tried multiple alternative solutions; using a background image and a spritesheet first, but it led to memory problems and lag when the GPU needed to swap between spritesheets. Then we tried swapping img elements, but it was also too slow. Drawing a frame from a spritesheet to a canvas was the most performant, so we began optimizing that. To save computation time each frame, the image data to write into the canvas is pre-processed via a temporary canvas and saved with putImageData() to an array, decoded and ready to use. The original spritesheet can then be garbage collected, and we store only the minimum amount of data needed in memory. Maybe it’s actually less to store undecoded images, but we get better performance while scrubbing the sequence this way. The frames are pretty small, just 640x400, but those will just be visible during scrubbing. When you stop, a high-res image loads and quickly fades in.&lt;/p&gt;
&lt;div&gt;&lt;pre class=&quot;language-js&quot;&gt;&lt;code class=&quot;language-js&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;var&lt;/span&gt; canvas &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; document&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;createElement&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&#39;canvas&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;&lt;br /&gt;canvas&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;width &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; imageWidth&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;&lt;br /&gt;canvas&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;height &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; imageHeight&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;&lt;br /&gt;&lt;br /&gt;&lt;span class=&quot;token keyword&quot;&gt;var&lt;/span&gt; ctx &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; canvas&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;getContext&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&#39;2d&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;&lt;br /&gt;ctx&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;drawImage&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;sheet&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;&lt;br /&gt;&lt;br /&gt;&lt;span class=&quot;token keyword&quot;&gt;var&lt;/span&gt; tilesX &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; imageWidth &lt;span class=&quot;token operator&quot;&gt;/&lt;/span&gt; tileWidth&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;&lt;br /&gt;&lt;span class=&quot;token keyword&quot;&gt;var&lt;/span&gt; tilesY &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; imageHeight &lt;span class=&quot;token operator&quot;&gt;/&lt;/span&gt; tileHeight&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;&lt;br /&gt;&lt;br /&gt;&lt;span class=&quot;token keyword&quot;&gt;var&lt;/span&gt; canvasPaste &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; canvas&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;cloneNode&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token boolean&quot;&gt;false&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;&lt;br /&gt;canvasPaste&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;width &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; tileWidth&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;&lt;br /&gt;canvasPaste&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;height &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; tileHeight&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;&lt;br /&gt;&lt;br /&gt;&lt;span class=&quot;token keyword&quot;&gt;var&lt;/span&gt; i&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; j&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; canvasPasteTemp&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; imgData&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;br /&gt;&lt;span class=&quot;token keyword&quot;&gt;var&lt;/span&gt; currentIndex &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;&lt;br /&gt;&lt;span class=&quot;token keyword&quot;&gt;var&lt;/span&gt; startIndex &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; index &lt;span class=&quot;token operator&quot;&gt;*&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;16&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;&lt;br /&gt;&lt;span class=&quot;token keyword&quot;&gt;for&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;i &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt; i &lt;span class=&quot;token operator&quot;&gt;&amp;lt;&lt;/span&gt; tilesY&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt; i&lt;span class=&quot;token operator&quot;&gt;++&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;&lt;br /&gt;  &lt;span class=&quot;token keyword&quot;&gt;for&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;j &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt; j &lt;span class=&quot;token operator&quot;&gt;&amp;lt;&lt;/span&gt; tilesX&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt; j&lt;span class=&quot;token operator&quot;&gt;++&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;&lt;br /&gt;    &lt;span class=&quot;token comment&quot;&gt;// Store the image data of each tile in the array.&lt;/span&gt;&lt;br /&gt;    canvasPasteTemp &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; canvasPaste&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;cloneNode&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token boolean&quot;&gt;false&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;&lt;br /&gt;    imgData &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; ctx&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;getImageData&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;j &lt;span class=&quot;token operator&quot;&gt;*&lt;/span&gt; tileWidth&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; i &lt;span class=&quot;token operator&quot;&gt;*&lt;/span&gt; tileHeight&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; tileWidth&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; tileHeight&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;&lt;br /&gt;    canvasPasteTemp&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;getContext&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&#39;2d&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;putImageData&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;imgData&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;&lt;br /&gt;&lt;br /&gt;    list&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt; startIndex &lt;span class=&quot;token operator&quot;&gt;+&lt;/span&gt; currentIndex &lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; imgData&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;&lt;br /&gt;&lt;br /&gt;    currentIndex&lt;span class=&quot;token operator&quot;&gt;++&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;&lt;br /&gt;  &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;br /&gt;&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;&lt;p&gt;The sprite-sheets are generated with &lt;a href=&quot;http://www.imagemagick.org/script/index.php&quot; rel=&quot;noopener&quot;&gt;Imagemagick&lt;/a&gt;. Here is a simple &lt;a href=&quot;https://gist.github.com/inear/7616849&quot; rel=&quot;noopener&quot;&gt;example on GitHub&lt;/a&gt; that shows how to create a spritesheet of all images inside a folder.&lt;/p&gt;
&lt;h3 id=&quot;animating-the-modules&quot;&gt;Animating the modules &lt;a class=&quot;headline-link&quot; href=&quot;https://web.dev/hobbit-front-end/#animating-the-modules&quot;&gt;#&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;To place the modules on the timeline, a hidden representation of the timeline, displayed offscreen, keeps track on the ‘playhead’ and the width of the timeline. This can be done with just code, but it was good with a visual representation when developing and debugging. When running for real it’s just updated on resize to set dimensions. Some modules fills the viewport and some have their own ratio, so it was a little tricky to scale and position everything in all resolutions so everything is visible and not cropped too much. Each module has two progress indicators, one for the visible position on screen and one for the duration of the module itself. When making parallax movement it’s often hard to calculate start- and end-position of objects to sync with the expected position when it’s in view. It’s good to know exactly when a module enters the view, plays its internal timeline and when it animates out of view again.&lt;/p&gt;
&lt;p&gt;Each module has a subtle black layer on top that adjusts its opacity so it’s fully transparent when it’s in the center position. This helps you to focus on one module at a time, which enhances the experience.&lt;/p&gt;
&lt;h3 id=&quot;page-performance&quot;&gt;Page performance &lt;a class=&quot;headline-link&quot; href=&quot;https://web.dev/hobbit-front-end/#page-performance&quot;&gt;#&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;Moving from a functioning prototype to a jank-free release version means going from guessing to knowing of what happens in the browser. This is where Chrome DevTools is your best friend.&lt;/p&gt;
&lt;p&gt;We have spent quite a lot of time optimising the site. Forcing hardware-acceleration is one of the most important tools of course to get smooth animations. But also hunting &lt;a href=&quot;https://developers.google.com/chrome-developer-tools/docs/tips-and-tricks#timeline-frames-mode&quot; rel=&quot;noopener&quot;&gt;colorful columns&lt;/a&gt; and red rectangles in Chrome DevTools. There are many good articles about the topics, and you should read them &lt;a href=&quot;http://jankfree.org/&quot; rel=&quot;noopener&quot;&gt;all&lt;/a&gt;. The reward for removing skipping frames is instant, but so is the frustration when they return again. And they will. It&#39;s an ongoing process that needs iterations.&lt;/p&gt;
&lt;aside class=&quot;aside flow bg-state-info-bg color-state-info-text&quot;&gt;&lt;div class=&quot; flow&quot;&gt; Watch the layers panel (only in Canary) and the “paint rectangles” in Chrome DevTools. If, for example, child elements need to be updated per frame and be painted you should investigate if it’s faster to rearrange the layers to minimize the areas that need to be painted as much as possible. &lt;/div&gt;&lt;/aside&gt;
&lt;p&gt;I like to use TweenMax from Greensock for tweening properties, transforms and CSS. Think in containers, visualise your structure as you add new layers. Keep in mind that existing transforms can be overwritten by new transforms. The translateZ(0) that forced hardware acceleration in your CSS class is replaced by a 2D matrix if you tween 2D values only. To keep the layer in acceleration mode in those cases, use the property “force3D:true” in the tween to make a 3D matrix instead of a 2D matrix. It’s easy to forget when you combine CSS and JavaScript tweens to set styles.&lt;/p&gt;
&lt;p&gt;Don’t force hardware acceleration where it’s not needed. GPU memory can quickly fill up and cause unwanted results when you want to hardware-accelerate many containers, especially on iOS where memory have more constraints. To load smaller assets and scale them up with css and disable some of the effects in mobile mode made huge improvements.&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;http://www.html5rocks.com/en/tutorials/memory/effectivemanagement/&quot; rel=&quot;noopener&quot;&gt;Memory leaks&lt;/a&gt; was another field we needed to improve our skills in. When navigating between the different WebGL experiences a lot of objects, materials, textures and geometry are created. If those are not ready for garbage collection when you navigate away and remove the section they will probably cause the device to crash after a while when it runs out of memory.&lt;/p&gt;
&lt;figure&gt;
&lt;img alt=&quot;Exiting a section with a failing dispose function.&quot; decoding=&quot;async&quot; height=&quot;61&quot; loading=&quot;lazy&quot; sizes=&quot;(min-width: 667px) 667px, calc(100vw - 48px)&quot; src=&quot;https://web-dev.imgix.net/image/T4FyVKpzu4WKF1kBNvXepbi08t52/LgHLhBoc1xsHi8j3PuP0.png?auto=format&quot; srcset=&quot;https://web-dev.imgix.net/image/T4FyVKpzu4WKF1kBNvXepbi08t52/LgHLhBoc1xsHi8j3PuP0.png?auto=format&amp;w=200 200w, https://web-dev.imgix.net/image/T4FyVKpzu4WKF1kBNvXepbi08t52/LgHLhBoc1xsHi8j3PuP0.png?auto=format&amp;w=228 228w, https://web-dev.imgix.net/image/T4FyVKpzu4WKF1kBNvXepbi08t52/LgHLhBoc1xsHi8j3PuP0.png?auto=format&amp;w=260 260w, https://web-dev.imgix.net/image/T4FyVKpzu4WKF1kBNvXepbi08t52/LgHLhBoc1xsHi8j3PuP0.png?auto=format&amp;w=296 296w, https://web-dev.imgix.net/image/T4FyVKpzu4WKF1kBNvXepbi08t52/LgHLhBoc1xsHi8j3PuP0.png?auto=format&amp;w=338 338w, https://web-dev.imgix.net/image/T4FyVKpzu4WKF1kBNvXepbi08t52/LgHLhBoc1xsHi8j3PuP0.png?auto=format&amp;w=385 385w, https://web-dev.imgix.net/image/T4FyVKpzu4WKF1kBNvXepbi08t52/LgHLhBoc1xsHi8j3PuP0.png?auto=format&amp;w=439 439w, https://web-dev.imgix.net/image/T4FyVKpzu4WKF1kBNvXepbi08t52/LgHLhBoc1xsHi8j3PuP0.png?auto=format&amp;w=500 500w, https://web-dev.imgix.net/image/T4FyVKpzu4WKF1kBNvXepbi08t52/LgHLhBoc1xsHi8j3PuP0.png?auto=format&amp;w=571 571w, https://web-dev.imgix.net/image/T4FyVKpzu4WKF1kBNvXepbi08t52/LgHLhBoc1xsHi8j3PuP0.png?auto=format&amp;w=650 650w, https://web-dev.imgix.net/image/T4FyVKpzu4WKF1kBNvXepbi08t52/LgHLhBoc1xsHi8j3PuP0.png?auto=format&amp;w=741 741w, https://web-dev.imgix.net/image/T4FyVKpzu4WKF1kBNvXepbi08t52/LgHLhBoc1xsHi8j3PuP0.png?auto=format&amp;w=845 845w, https://web-dev.imgix.net/image/T4FyVKpzu4WKF1kBNvXepbi08t52/LgHLhBoc1xsHi8j3PuP0.png?auto=format&amp;w=964 964w, https://web-dev.imgix.net/image/T4FyVKpzu4WKF1kBNvXepbi08t52/LgHLhBoc1xsHi8j3PuP0.png?auto=format&amp;w=1098 1098w, https://web-dev.imgix.net/image/T4FyVKpzu4WKF1kBNvXepbi08t52/LgHLhBoc1xsHi8j3PuP0.png?auto=format&amp;w=1252 1252w, https://web-dev.imgix.net/image/T4FyVKpzu4WKF1kBNvXepbi08t52/LgHLhBoc1xsHi8j3PuP0.png?auto=format&amp;w=1334 1334w&quot; width=&quot;667&quot; /&gt;
&lt;figcaption&gt;Exiting a section with a failing dispose function.&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;figure&gt;
&lt;img alt=&quot;Much better!&quot; decoding=&quot;async&quot; height=&quot;62&quot; loading=&quot;lazy&quot; sizes=&quot;(min-width: 679px) 679px, calc(100vw - 48px)&quot; src=&quot;https://web-dev.imgix.net/image/T4FyVKpzu4WKF1kBNvXepbi08t52/wV383MdysOSfWvrtxDl0.png?auto=format&quot; srcset=&quot;https://web-dev.imgix.net/image/T4FyVKpzu4WKF1kBNvXepbi08t52/wV383MdysOSfWvrtxDl0.png?auto=format&amp;w=200 200w, https://web-dev.imgix.net/image/T4FyVKpzu4WKF1kBNvXepbi08t52/wV383MdysOSfWvrtxDl0.png?auto=format&amp;w=228 228w, https://web-dev.imgix.net/image/T4FyVKpzu4WKF1kBNvXepbi08t52/wV383MdysOSfWvrtxDl0.png?auto=format&amp;w=260 260w, https://web-dev.imgix.net/image/T4FyVKpzu4WKF1kBNvXepbi08t52/wV383MdysOSfWvrtxDl0.png?auto=format&amp;w=296 296w, https://web-dev.imgix.net/image/T4FyVKpzu4WKF1kBNvXepbi08t52/wV383MdysOSfWvrtxDl0.png?auto=format&amp;w=338 338w, https://web-dev.imgix.net/image/T4FyVKpzu4WKF1kBNvXepbi08t52/wV383MdysOSfWvrtxDl0.png?auto=format&amp;w=385 385w, https://web-dev.imgix.net/image/T4FyVKpzu4WKF1kBNvXepbi08t52/wV383MdysOSfWvrtxDl0.png?auto=format&amp;w=439 439w, https://web-dev.imgix.net/image/T4FyVKpzu4WKF1kBNvXepbi08t52/wV383MdysOSfWvrtxDl0.png?auto=format&amp;w=500 500w, https://web-dev.imgix.net/image/T4FyVKpzu4WKF1kBNvXepbi08t52/wV383MdysOSfWvrtxDl0.png?auto=format&amp;w=571 571w, https://web-dev.imgix.net/image/T4FyVKpzu4WKF1kBNvXepbi08t52/wV383MdysOSfWvrtxDl0.png?auto=format&amp;w=650 650w, https://web-dev.imgix.net/image/T4FyVKpzu4WKF1kBNvXepbi08t52/wV383MdysOSfWvrtxDl0.png?auto=format&amp;w=741 741w, https://web-dev.imgix.net/image/T4FyVKpzu4WKF1kBNvXepbi08t52/wV383MdysOSfWvrtxDl0.png?auto=format&amp;w=845 845w, https://web-dev.imgix.net/image/T4FyVKpzu4WKF1kBNvXepbi08t52/wV383MdysOSfWvrtxDl0.png?auto=format&amp;w=964 964w, https://web-dev.imgix.net/image/T4FyVKpzu4WKF1kBNvXepbi08t52/wV383MdysOSfWvrtxDl0.png?auto=format&amp;w=1098 1098w, https://web-dev.imgix.net/image/T4FyVKpzu4WKF1kBNvXepbi08t52/wV383MdysOSfWvrtxDl0.png?auto=format&amp;w=1252 1252w, https://web-dev.imgix.net/image/T4FyVKpzu4WKF1kBNvXepbi08t52/wV383MdysOSfWvrtxDl0.png?auto=format&amp;w=1358 1358w&quot; width=&quot;679&quot; /&gt;
&lt;figcaption&gt;Much better!&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;p&gt;To find the leak it was pretty straight forward workflow in DevTools, recording the timeline and capturing heap snapshots. It’s easier if there are specific objects, like 3D geometry or a specific library, that you can filter out. In the example above it turned out that the 3D scene was still around and also an array that stored geometry was not cleared. If you find it hard to locate where the object lives, there is a nice feature that let you view this called &lt;a href=&quot;https://developers.google.com/chrome-developer-tools/docs/heap-profiling?hl=sv&amp;amp;csw=1#views_paths&quot; rel=&quot;noopener&quot;&gt;retaining paths&lt;/a&gt;. Just click the object you want to inspect in the heap snapshot and you get the information in a panel below. Using a good structure with smaller objects helps when locating your references.&lt;/p&gt;
&lt;figure&gt;
&lt;img alt=&quot;The scene was referenced in the EffectComposer.&quot; decoding=&quot;async&quot; height=&quot;334&quot; loading=&quot;lazy&quot; sizes=&quot;(min-width: 593px) 593px, calc(100vw - 48px)&quot; src=&quot;https://web-dev.imgix.net/image/T4FyVKpzu4WKF1kBNvXepbi08t52/rPmywWW0dm3GoCrS2qPk.png?auto=format&quot; srcset=&quot;https://web-dev.imgix.net/image/T4FyVKpzu4WKF1kBNvXepbi08t52/rPmywWW0dm3GoCrS2qPk.png?auto=format&amp;w=200 200w, https://web-dev.imgix.net/image/T4FyVKpzu4WKF1kBNvXepbi08t52/rPmywWW0dm3GoCrS2qPk.png?auto=format&amp;w=228 228w, https://web-dev.imgix.net/image/T4FyVKpzu4WKF1kBNvXepbi08t52/rPmywWW0dm3GoCrS2qPk.png?auto=format&amp;w=260 260w, https://web-dev.imgix.net/image/T4FyVKpzu4WKF1kBNvXepbi08t52/rPmywWW0dm3GoCrS2qPk.png?auto=format&amp;w=296 296w, https://web-dev.imgix.net/image/T4FyVKpzu4WKF1kBNvXepbi08t52/rPmywWW0dm3GoCrS2qPk.png?auto=format&amp;w=338 338w, https://web-dev.imgix.net/image/T4FyVKpzu4WKF1kBNvXepbi08t52/rPmywWW0dm3GoCrS2qPk.png?auto=format&amp;w=385 385w, https://web-dev.imgix.net/image/T4FyVKpzu4WKF1kBNvXepbi08t52/rPmywWW0dm3GoCrS2qPk.png?auto=format&amp;w=439 439w, https://web-dev.imgix.net/image/T4FyVKpzu4WKF1kBNvXepbi08t52/rPmywWW0dm3GoCrS2qPk.png?auto=format&amp;w=500 500w, https://web-dev.imgix.net/image/T4FyVKpzu4WKF1kBNvXepbi08t52/rPmywWW0dm3GoCrS2qPk.png?auto=format&amp;w=571 571w, https://web-dev.imgix.net/image/T4FyVKpzu4WKF1kBNvXepbi08t52/rPmywWW0dm3GoCrS2qPk.png?auto=format&amp;w=650 650w, https://web-dev.imgix.net/image/T4FyVKpzu4WKF1kBNvXepbi08t52/rPmywWW0dm3GoCrS2qPk.png?auto=format&amp;w=741 741w, https://web-dev.imgix.net/image/T4FyVKpzu4WKF1kBNvXepbi08t52/rPmywWW0dm3GoCrS2qPk.png?auto=format&amp;w=845 845w, https://web-dev.imgix.net/image/T4FyVKpzu4WKF1kBNvXepbi08t52/rPmywWW0dm3GoCrS2qPk.png?auto=format&amp;w=964 964w, https://web-dev.imgix.net/image/T4FyVKpzu4WKF1kBNvXepbi08t52/rPmywWW0dm3GoCrS2qPk.png?auto=format&amp;w=1098 1098w, https://web-dev.imgix.net/image/T4FyVKpzu4WKF1kBNvXepbi08t52/rPmywWW0dm3GoCrS2qPk.png?auto=format&amp;w=1186 1186w&quot; width=&quot;593&quot; /&gt;
&lt;figcaption&gt;The scene was referenced in the EffectComposer.&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;p&gt;In general, it&#39;s healthy to think twice before you manipulate the DOM. When you do, think about efficiency. Don&#39;t manipulate the DOM inside a game loop if you can help it. Store references in variables for reuse. If you need to search for an element, use the shortest route by storing references to strategic containers and searching inside the nearest ancestor element.&lt;/p&gt;
&lt;p&gt;Delay reading dimensions of newly added elements or when removing/adding classes if you experience layout bugs. Or make sure &lt;a href=&quot;http://www.html5rocks.com/en/tutorials/speed/high-performance-animations/#toc-animating-layout-properties&quot; rel=&quot;noopener&quot;&gt;Layout is triggered&lt;/a&gt;. Sometimes the browser batch changes to styles, and will not update after the next layout trigger. This can really be a big problem sometimes, but it’s there for a reason, so try to learn how it’s working behind the scenes and you will gain a lot.&lt;/p&gt;
&lt;h3 id=&quot;fullscreen&quot;&gt;Fullscreen &lt;a class=&quot;headline-link&quot; href=&quot;https://web.dev/hobbit-front-end/#fullscreen&quot;&gt;#&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;When available, you have the option to put the site in fullscreen-mode in the menu via the Fullscreen API. But on devices there is also the browsers decision to put it into fullscreen. Safari on iOS had previously a hack to let you control that, but that is not available anymore so you have to prepare your design to work without it when making a non-scrolling page. We can probably expect updates on this in future updates, since it has broke a lot of web-apps.&lt;/p&gt;
&lt;h2 id=&quot;assets&quot;&gt;Assets &lt;a class=&quot;headline-link&quot; href=&quot;https://web.dev/hobbit-front-end/#assets&quot;&gt;#&lt;/a&gt;&lt;/h2&gt;
&lt;figure&gt;
&lt;img alt=&quot;Animated instructions for the experiments.&quot; decoding=&quot;async&quot; height=&quot;222&quot; loading=&quot;lazy&quot; sizes=&quot;(min-width: 800px) 800px, calc(100vw - 48px)&quot; src=&quot;https://web-dev.imgix.net/image/T4FyVKpzu4WKF1kBNvXepbi08t52/FIinD7TN05e0D5UO6BXb.jpg?auto=format&quot; srcset=&quot;https://web-dev.imgix.net/image/T4FyVKpzu4WKF1kBNvXepbi08t52/FIinD7TN05e0D5UO6BXb.jpg?auto=format&amp;w=200 200w, https://web-dev.imgix.net/image/T4FyVKpzu4WKF1kBNvXepbi08t52/FIinD7TN05e0D5UO6BXb.jpg?auto=format&amp;w=228 228w, https://web-dev.imgix.net/image/T4FyVKpzu4WKF1kBNvXepbi08t52/FIinD7TN05e0D5UO6BXb.jpg?auto=format&amp;w=260 260w, https://web-dev.imgix.net/image/T4FyVKpzu4WKF1kBNvXepbi08t52/FIinD7TN05e0D5UO6BXb.jpg?auto=format&amp;w=296 296w, https://web-dev.imgix.net/image/T4FyVKpzu4WKF1kBNvXepbi08t52/FIinD7TN05e0D5UO6BXb.jpg?auto=format&amp;w=338 338w, https://web-dev.imgix.net/image/T4FyVKpzu4WKF1kBNvXepbi08t52/FIinD7TN05e0D5UO6BXb.jpg?auto=format&amp;w=385 385w, https://web-dev.imgix.net/image/T4FyVKpzu4WKF1kBNvXepbi08t52/FIinD7TN05e0D5UO6BXb.jpg?auto=format&amp;w=439 439w, https://web-dev.imgix.net/image/T4FyVKpzu4WKF1kBNvXepbi08t52/FIinD7TN05e0D5UO6BXb.jpg?auto=format&amp;w=500 500w, https://web-dev.imgix.net/image/T4FyVKpzu4WKF1kBNvXepbi08t52/FIinD7TN05e0D5UO6BXb.jpg?auto=format&amp;w=571 571w, https://web-dev.imgix.net/image/T4FyVKpzu4WKF1kBNvXepbi08t52/FIinD7TN05e0D5UO6BXb.jpg?auto=format&amp;w=650 650w, https://web-dev.imgix.net/image/T4FyVKpzu4WKF1kBNvXepbi08t52/FIinD7TN05e0D5UO6BXb.jpg?auto=format&amp;w=741 741w, https://web-dev.imgix.net/image/T4FyVKpzu4WKF1kBNvXepbi08t52/FIinD7TN05e0D5UO6BXb.jpg?auto=format&amp;w=845 845w, https://web-dev.imgix.net/image/T4FyVKpzu4WKF1kBNvXepbi08t52/FIinD7TN05e0D5UO6BXb.jpg?auto=format&amp;w=964 964w, https://web-dev.imgix.net/image/T4FyVKpzu4WKF1kBNvXepbi08t52/FIinD7TN05e0D5UO6BXb.jpg?auto=format&amp;w=1098 1098w, https://web-dev.imgix.net/image/T4FyVKpzu4WKF1kBNvXepbi08t52/FIinD7TN05e0D5UO6BXb.jpg?auto=format&amp;w=1252 1252w, https://web-dev.imgix.net/image/T4FyVKpzu4WKF1kBNvXepbi08t52/FIinD7TN05e0D5UO6BXb.jpg?auto=format&amp;w=1428 1428w, https://web-dev.imgix.net/image/T4FyVKpzu4WKF1kBNvXepbi08t52/FIinD7TN05e0D5UO6BXb.jpg?auto=format&amp;w=1600 1600w&quot; width=&quot;800&quot; /&gt;
&lt;figcaption&gt;Animated instructions for the experiments.&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;p&gt;Throughout the site we have a lot of different types of assets, we use images (PNG and JPEG), SVG (inline and background), spritesheets (PNG), custom icon fonts and Adobe Edge animations. We use PNGs for assets and animations (spritesheets) where the element can&#39;t be vector based, otherwise we try to use SVGs as much as possible.&lt;/p&gt;
&lt;p&gt;The vector format means no loss of quality, even if we scale it. 1 file for all devices.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Small file size.&lt;/li&gt;
&lt;li&gt;We can animate each part separately (perfect for advanced animations). As an example we hide the &amp;quot;subtitle&amp;quot; of the Hobbit logo (the desolation of Smaug) when it&#39;s scaled down.&lt;/li&gt;
&lt;li&gt;It can be embedded as an SVG HTML tag or used as a background-image with no extra loading (it’s loaded the same time as the html page).&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Icon typefaces have the same advantages as SVG when it comes to scalability and are used instead of SVG for small elements like icons on which we only need to be able to change the colour (hover, active, etc.). The icons are also very easy to reuse, you just need to set the CSS &amp;quot;content&amp;quot; property of an element.&lt;/p&gt;
&lt;h2 id=&quot;animations&quot;&gt;Animations &lt;a class=&quot;headline-link&quot; href=&quot;https://web.dev/hobbit-front-end/#animations&quot;&gt;#&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;In some cases animating SVG elements with code can be very time consuming, especially when the animation needs to be changed a lot during the design process. To improve the workflow between designers and developers we use Adobe Edge for some animations (the instructions before the games). The animation workflow is really close to Flash and that helped the team but there are a few drawbacks, especially with integrating the Edge animations in our asset loading process since it comes with it’s own loaders and implementation logic.&lt;/p&gt;
&lt;p&gt;I still feel we have a long way to go before we have a perfect workflow for handling assets and handmade animations on the web. We’re looking forward to seeing how tools like Edge will evolve. Feel free to add suggestions on other animation tools and workflows in the comments.&lt;/p&gt;
&lt;h2 id=&quot;conclusion&quot;&gt;Conclusion &lt;a class=&quot;headline-link&quot; href=&quot;https://web.dev/hobbit-front-end/#conclusion&quot;&gt;#&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;Now when all the parts of the project are released and we look at the final result I must say we are quite impressed with the state of modern mobile browsers. When we started off this project we had much lower expectations on how seamless, integrated and performant we would be able to make it. It&#39;s been a great learning experience for us and all the time spent iterating and testing (a lot) has improved our understanding of how modern browsers work. And that&#39;s what it will take if we want to shorten the production time on these types of projects, going from guessing to knowing.&lt;/p&gt;
</content>
    <author>
      <name>Einar Öberg</name>
    </author><author>
      <name>Daniel Isaksson</name>
    </author>
  </entry>
  
  <entry>
    <title>The Hobbit Experience</title>
    <link href="https://web.dev/hobbit/"/>
    <updated>2013-11-20T00:00:00Z</updated>
    <id>https://web.dev/hobbit/</id>
    <content type="html" mode="escaped">&lt;p&gt;Historically, bringing interactive, web-based, multimedia-heavy experiences to mobiles and tablets has been a challenge. The main constraints have been performance, API availability, limitations in HTML5 audio on devices and the lack of seamless inline video playback.&lt;/p&gt;
&lt;p&gt;Earlier this year we started a project with friends from Google and Warner Bros. to make a mobile-first web experience for the new Hobbit movie, &lt;strong&gt;The Hobbit: The Desolation of Smaug&lt;/strong&gt;. Building a multimedia-heavy mobile Chrome Experiment has been a really inspiring and challenging task.&lt;/p&gt;
&lt;p&gt;The experience is optimized for Chrome for Android on the new Nexus devices where we now have access to WebGL and Web Audio. However a large portion of the experience is accessible on non-WebGL devices and browsers as well thanks to hardware-accelerated compositing and CSS animations.&lt;/p&gt;
&lt;p&gt;The whole experience is based on a map of Middle-earth and the locations and characters from the Hobbit movies. Using WebGL made it possible for us to dramatize and explore the rich world of the Hobbit trilogy and let the users control the experience.&lt;/p&gt;
&lt;h2 id=&quot;challenges-of-webgl-on-mobile-devices&quot;&gt;Challenges of WebGL on mobile devices &lt;a class=&quot;headline-link&quot; href=&quot;https://web.dev/hobbit/#challenges-of-webgl-on-mobile-devices&quot;&gt;#&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;First, the term &amp;quot;mobile devices&amp;quot; is very broad. The specs for devices vary a lot. So as a developer you need to decide if you want to support more devices with a less complex experience or, as we did in this case, limit the supported devices to those able to display a more realistic 3D world. For “Journey through Middle-earth” we focused on Nexus devices and five popular Android smartphones.&lt;/p&gt;
&lt;p&gt;In the experiment, we used &lt;a href=&quot;http://threejs.org/&quot; rel=&quot;noopener&quot;&gt;three.js&lt;/a&gt; as we’ve done for some of our previous WebGL projects. We started implementation by building an initial version of the &lt;a href=&quot;http://middle-earth.thehobbit.com/trollshaw/experience&quot; rel=&quot;noopener&quot;&gt;Trollshaw game&lt;/a&gt; that would run well on the Nexus 10 tablet. After some initial testing on the device, we had a list of optimizations in mind that looked much like what we normally would use for a low-spec laptop:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Use low-poly models&lt;/li&gt;
&lt;li&gt;Use low-res textures&lt;/li&gt;
&lt;li&gt;Reduce the number of drawcalls as much as possible by &lt;a href=&quot;http://www.google.com/url?q=http%3A%2F%2Flearningthreejs.com%2Fblog%2F2011%2F10%2F05%2Fperformance-merging-geometry%2F&amp;amp;sa=D&amp;amp;sntz=1&amp;amp;usg=AFQjCNEekldEIRJOLOTgexpMUFPq6Obtvg&quot; rel=&quot;noopener&quot;&gt;merging geometry&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;Simplify materials and lighting&lt;/li&gt;
&lt;li&gt;Remove post effects and turn off antialiasing&lt;/li&gt;
&lt;li&gt;Optimise Javascript performance&lt;/li&gt;
&lt;li&gt;Render the WebGL canvas at half size and scale up with CSS&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;After applying these optimizations to our first rough version of the game, we had a steady frame rate of 30FPS that we were happy with. At that point our goal was to improve the visuals without negatively impacting frame rate. We tried many tricks: some turned out to really have an impact on performance; a few didn’t have as big an effect as we’d hoped.&lt;/p&gt;
&lt;h3 id=&quot;use-low-poly-models&quot;&gt;Use low-poly models &lt;a class=&quot;headline-link&quot; href=&quot;https://web.dev/hobbit/#use-low-poly-models&quot;&gt;#&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;Let’s start with the models. Using low-poly models certainly helps download time, as well as the time it takes to initialize the scene. We found that we could increase the complexity quite a lot without affecting performance much. The troll models we use in this game are about 5K faces and the scene is around 40K faces and that works fine.&lt;/p&gt;
&lt;figure&gt;
&lt;img alt=&quot;One of the trolls of Trollshaw forest&quot; decoding=&quot;async&quot; height=&quot;732&quot; loading=&quot;lazy&quot; sizes=&quot;(min-width: 800px) 800px, calc(100vw - 48px)&quot; src=&quot;https://web-dev.imgix.net/image/T4FyVKpzu4WKF1kBNvXepbi08t52/atliubYSO7t5PZmmsQWa.jpg?auto=format&quot; srcset=&quot;https://web-dev.imgix.net/image/T4FyVKpzu4WKF1kBNvXepbi08t52/atliubYSO7t5PZmmsQWa.jpg?auto=format&amp;w=200 200w, https://web-dev.imgix.net/image/T4FyVKpzu4WKF1kBNvXepbi08t52/atliubYSO7t5PZmmsQWa.jpg?auto=format&amp;w=228 228w, https://web-dev.imgix.net/image/T4FyVKpzu4WKF1kBNvXepbi08t52/atliubYSO7t5PZmmsQWa.jpg?auto=format&amp;w=260 260w, https://web-dev.imgix.net/image/T4FyVKpzu4WKF1kBNvXepbi08t52/atliubYSO7t5PZmmsQWa.jpg?auto=format&amp;w=296 296w, https://web-dev.imgix.net/image/T4FyVKpzu4WKF1kBNvXepbi08t52/atliubYSO7t5PZmmsQWa.jpg?auto=format&amp;w=338 338w, https://web-dev.imgix.net/image/T4FyVKpzu4WKF1kBNvXepbi08t52/atliubYSO7t5PZmmsQWa.jpg?auto=format&amp;w=385 385w, https://web-dev.imgix.net/image/T4FyVKpzu4WKF1kBNvXepbi08t52/atliubYSO7t5PZmmsQWa.jpg?auto=format&amp;w=439 439w, https://web-dev.imgix.net/image/T4FyVKpzu4WKF1kBNvXepbi08t52/atliubYSO7t5PZmmsQWa.jpg?auto=format&amp;w=500 500w, https://web-dev.imgix.net/image/T4FyVKpzu4WKF1kBNvXepbi08t52/atliubYSO7t5PZmmsQWa.jpg?auto=format&amp;w=571 571w, https://web-dev.imgix.net/image/T4FyVKpzu4WKF1kBNvXepbi08t52/atliubYSO7t5PZmmsQWa.jpg?auto=format&amp;w=650 650w, https://web-dev.imgix.net/image/T4FyVKpzu4WKF1kBNvXepbi08t52/atliubYSO7t5PZmmsQWa.jpg?auto=format&amp;w=741 741w, https://web-dev.imgix.net/image/T4FyVKpzu4WKF1kBNvXepbi08t52/atliubYSO7t5PZmmsQWa.jpg?auto=format&amp;w=845 845w, https://web-dev.imgix.net/image/T4FyVKpzu4WKF1kBNvXepbi08t52/atliubYSO7t5PZmmsQWa.jpg?auto=format&amp;w=964 964w, https://web-dev.imgix.net/image/T4FyVKpzu4WKF1kBNvXepbi08t52/atliubYSO7t5PZmmsQWa.jpg?auto=format&amp;w=1098 1098w, https://web-dev.imgix.net/image/T4FyVKpzu4WKF1kBNvXepbi08t52/atliubYSO7t5PZmmsQWa.jpg?auto=format&amp;w=1252 1252w, https://web-dev.imgix.net/image/T4FyVKpzu4WKF1kBNvXepbi08t52/atliubYSO7t5PZmmsQWa.jpg?auto=format&amp;w=1428 1428w, https://web-dev.imgix.net/image/T4FyVKpzu4WKF1kBNvXepbi08t52/atliubYSO7t5PZmmsQWa.jpg?auto=format&amp;w=1600 1600w&quot; width=&quot;800&quot; /&gt;
&lt;figcaption&gt;One of the trolls of Trollshaw forest&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;p&gt;For another (not yet released) location in the experience we saw more impact on performance from reducing polygons. In that case we loaded lower-polygon objects for mobile devices than the objects we loaded for desktop. Creating different sets of 3D models requires some extra work and isn’t always required. It really depends on how complex your models are to begin with.&lt;/p&gt;
&lt;p&gt;When working on big scenes with a lot of objects, we tried to be strategic on how we divide the geometry. This enabled us to switch less important meshes on and off quickly, to find a setting that worked for all mobile devices. Then, we could choose to merge the geometry in JavaScript at runtime for dynamic optimization or to merge it in pre-production to save requests.&lt;/p&gt;
&lt;h3 id=&quot;use-low-res-textures&quot;&gt;Use low-res textures &lt;a class=&quot;headline-link&quot; href=&quot;https://web.dev/hobbit/#use-low-res-textures&quot;&gt;#&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;To reduce load time on mobile devices, we chose to load different textures that were half the size of the textures on desktop. It turns out that all devices can &lt;a href=&quot;http://webglstats.com/#h_texsize&quot; rel=&quot;noopener&quot;&gt;handle texture sizes&lt;/a&gt; up to 2048x2048px and most can handle 4096x4096px. Texture lookup on the individual textures doesn’t seem to be a problem once they are uploaded to GPU. The total size of the textures must fit in the GPU memory to avoid having textures constantly up- and downloaded, but this is probably not a big problem for most web-experiences. However, combining textures into as few spritesheets as possible is important to reduce the number of drawcalls  -  this is something that has a big impact on performance on mobile devices.&lt;/p&gt;
&lt;figure&gt;
&lt;img alt=&quot;Texture for one of the trolls of Trollshaw forest&quot; decoding=&quot;async&quot; height=&quot;512&quot; loading=&quot;lazy&quot; sizes=&quot;(min-width: 512px) 512px, calc(100vw - 48px)&quot; src=&quot;https://web-dev.imgix.net/image/T4FyVKpzu4WKF1kBNvXepbi08t52/OpVvRIIqlQUQOlJprLhV.jpg?auto=format&quot; srcset=&quot;https://web-dev.imgix.net/image/T4FyVKpzu4WKF1kBNvXepbi08t52/OpVvRIIqlQUQOlJprLhV.jpg?auto=format&amp;w=200 200w, https://web-dev.imgix.net/image/T4FyVKpzu4WKF1kBNvXepbi08t52/OpVvRIIqlQUQOlJprLhV.jpg?auto=format&amp;w=228 228w, https://web-dev.imgix.net/image/T4FyVKpzu4WKF1kBNvXepbi08t52/OpVvRIIqlQUQOlJprLhV.jpg?auto=format&amp;w=260 260w, https://web-dev.imgix.net/image/T4FyVKpzu4WKF1kBNvXepbi08t52/OpVvRIIqlQUQOlJprLhV.jpg?auto=format&amp;w=296 296w, https://web-dev.imgix.net/image/T4FyVKpzu4WKF1kBNvXepbi08t52/OpVvRIIqlQUQOlJprLhV.jpg?auto=format&amp;w=338 338w, https://web-dev.imgix.net/image/T4FyVKpzu4WKF1kBNvXepbi08t52/OpVvRIIqlQUQOlJprLhV.jpg?auto=format&amp;w=385 385w, https://web-dev.imgix.net/image/T4FyVKpzu4WKF1kBNvXepbi08t52/OpVvRIIqlQUQOlJprLhV.jpg?auto=format&amp;w=439 439w, https://web-dev.imgix.net/image/T4FyVKpzu4WKF1kBNvXepbi08t52/OpVvRIIqlQUQOlJprLhV.jpg?auto=format&amp;w=500 500w, https://web-dev.imgix.net/image/T4FyVKpzu4WKF1kBNvXepbi08t52/OpVvRIIqlQUQOlJprLhV.jpg?auto=format&amp;w=571 571w, https://web-dev.imgix.net/image/T4FyVKpzu4WKF1kBNvXepbi08t52/OpVvRIIqlQUQOlJprLhV.jpg?auto=format&amp;w=650 650w, https://web-dev.imgix.net/image/T4FyVKpzu4WKF1kBNvXepbi08t52/OpVvRIIqlQUQOlJprLhV.jpg?auto=format&amp;w=741 741w, https://web-dev.imgix.net/image/T4FyVKpzu4WKF1kBNvXepbi08t52/OpVvRIIqlQUQOlJprLhV.jpg?auto=format&amp;w=845 845w, https://web-dev.imgix.net/image/T4FyVKpzu4WKF1kBNvXepbi08t52/OpVvRIIqlQUQOlJprLhV.jpg?auto=format&amp;w=964 964w, https://web-dev.imgix.net/image/T4FyVKpzu4WKF1kBNvXepbi08t52/OpVvRIIqlQUQOlJprLhV.jpg?auto=format&amp;w=1024 1024w&quot; width=&quot;512&quot; /&gt;
&lt;figcaption&gt;Texture for one of the trolls of Trollshaw forest&lt;br /&gt;
(original size 512x512px)&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;h3 id=&quot;simplify-materials-and-lighting&quot;&gt;Simplify materials and lighting &lt;a class=&quot;headline-link&quot; href=&quot;https://web.dev/hobbit/#simplify-materials-and-lighting&quot;&gt;#&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;The choice of materials can also greatly affect performance and must be managed wisely on mobile. Using &lt;code&gt; MeshLambertMaterial&lt;/code&gt;  (per vertex light calculation) in three.js instead of &lt;code&gt; MeshPhongMaterial&lt;/code&gt;  (per texel light calculation) is one thing we used to optimise performance. Basically we tried to use as simple shaders with as few lighting calculations as possible.&lt;/p&gt;
&lt;p&gt;To see how the materials you use affect a scene’s performance, you can override the materials of the scene with a &lt;code&gt; MeshBasicMaterial&lt;/code&gt; . This will give you a good comparison.&lt;/p&gt;
&lt;div&gt;&lt;pre class=&quot;language-js&quot;&gt;&lt;code class=&quot;language-js&quot;&gt;scene&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;overrideMaterial &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;new&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;THREE&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;MeshBasicMaterial&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;token literal-property property&quot;&gt;color&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;token number&quot;&gt;0x333333&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token literal-property property&quot;&gt;wireframe&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;token boolean&quot;&gt;true&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;&lt;h3 id=&quot;optimise-javascript-performance&quot;&gt;Optimise JavaScript performance &lt;a class=&quot;headline-link&quot; href=&quot;https://web.dev/hobbit/#optimise-javascript-performance&quot;&gt;#&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;When building games for mobile, the GPU isn’t always the biggest hurdle. A lot of time is spent on the CPU, especially physics and skeletal animations. One trick that helps sometimes, depending on the simulation, is to only run these expensive calculations every other frame. You can also use available JavaScript optimisation techniques when it comes to object pooling, &lt;a href=&quot;http://www.html5rocks.com/en/tutorials/speed/static-mem-pools/&quot; rel=&quot;noopener&quot;&gt;garbage collection and object creation&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;Updating pre-allocated objects in loops instead of creating new objects is an important step to avoid garbage collection &amp;quot;hiccups&amp;quot; during the game.&lt;/p&gt;
&lt;p&gt;For example, consider code like this:&lt;/p&gt;
&lt;div&gt;&lt;pre class=&quot;language-js&quot;&gt;&lt;code class=&quot;language-js&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;var&lt;/span&gt; currentPos &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;new&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;THREE&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;Vector3&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;&lt;br /&gt;&lt;br /&gt;&lt;span class=&quot;token keyword&quot;&gt;function&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;gameLoop&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;&lt;br /&gt;  currentPos &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;new&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;THREE&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;Vector3&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token number&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;+&lt;/span&gt;offsetX&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;token number&quot;&gt;100&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;token number&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;&lt;br /&gt;&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;&lt;p&gt;An improved version of this loop avoids creating new objects that must be garbage collected:&lt;/p&gt;
&lt;div&gt;&lt;pre class=&quot;language-js&quot;&gt;&lt;code class=&quot;language-js&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;var&lt;/span&gt; originPos &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;new&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;THREE&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;Vector3&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token number&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;token number&quot;&gt;100&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;token number&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;&lt;br /&gt;&lt;span class=&quot;token keyword&quot;&gt;var&lt;/span&gt; currentPos &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;new&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;THREE&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;Vector3&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;&lt;br /&gt;&lt;span class=&quot;token keyword&quot;&gt;function&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;gameLoop&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;&lt;br /&gt;  currentPos&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;copy&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;originPos&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;x &lt;span class=&quot;token operator&quot;&gt;+=&lt;/span&gt; offsetX&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;&lt;br /&gt;  &lt;span class=&quot;token comment&quot;&gt;//or&lt;/span&gt;&lt;br /&gt;  currentPos&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;set&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;originPos&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;x&lt;span class=&quot;token operator&quot;&gt;+&lt;/span&gt;offsetX&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;originPos&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;y&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;originPos&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;z&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;&lt;br /&gt;&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;&lt;p&gt;As much as possible, event handlers should only update properties, and let the &lt;code&gt; requestAnimationFrame&lt;/code&gt;  render-loop handle updating the stage.&lt;/p&gt;
&lt;p&gt;Another tip is to optimise and/or pre-calculate ray-casting operations. For example, if you need to attach an object to a mesh during a static path movement, you can &amp;quot;record&amp;quot; the positions during one loop and then read from this data instead of ray-casting against the mesh. Or as we do in the &lt;a href=&quot;http://middle-earth.thehobbit.com/rivendell/experience&quot; rel=&quot;noopener&quot;&gt;Rivendell experience&lt;/a&gt;, ray-cast to look for mouse interactions with a simpler low-poly invisible mesh. Searching for collisions on a high-poly mesh is very slow and should be avoided in a game-loop in general.&lt;/p&gt;
&lt;h3 id=&quot;render-the-webgl-canvas-at-half-size-and-scale-up-with-css&quot;&gt;Render the WebGL canvas at half size and scale up with CSS &lt;a class=&quot;headline-link&quot; href=&quot;https://web.dev/hobbit/#render-the-webgl-canvas-at-half-size-and-scale-up-with-css&quot;&gt;#&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;The size of the WebGL canvas is probably the single most effective parameter you can tweak to optimise performance. The bigger the canvas you use to draw your 3D scene, the more pixels have to be drawn on every frame. This of course affects performance.The Nexus 10 with its high-density 2560x1600 pixel display has to push 4 times the number of pixels as a low-density tablet. To optimise this for mobile we use a &lt;a href=&quot;http://blog.tojicode.com/2011/07/dirty-full-frame-webgl-performance-hack.html&quot; rel=&quot;noopener&quot;&gt;trick&lt;/a&gt; where we set the canvas to half the size (50%) and then scale it up to its intended size (100%) with hardware-accelerated CSS 3D transforms. The downside of this is a pixelated image where thin lines can become a problem but on a high-res screen the effect isn’t that bad. It’s absolutely worth the extra performance.&lt;/p&gt;
&lt;figure&gt;
&lt;img alt=&quot;The same scene without canvas scaling on the Nexus 10 (16FPS) and scaled to 50% (33FPS)&quot; decoding=&quot;async&quot; height=&quot;400&quot; loading=&quot;lazy&quot; sizes=&quot;(min-width: 800px) 800px, calc(100vw - 48px)&quot; src=&quot;https://web-dev.imgix.net/image/T4FyVKpzu4WKF1kBNvXepbi08t52/qrYKVjMtRVRg9vIHFxY6.jpg?auto=format&quot; srcset=&quot;https://web-dev.imgix.net/image/T4FyVKpzu4WKF1kBNvXepbi08t52/qrYKVjMtRVRg9vIHFxY6.jpg?auto=format&amp;w=200 200w, https://web-dev.imgix.net/image/T4FyVKpzu4WKF1kBNvXepbi08t52/qrYKVjMtRVRg9vIHFxY6.jpg?auto=format&amp;w=228 228w, https://web-dev.imgix.net/image/T4FyVKpzu4WKF1kBNvXepbi08t52/qrYKVjMtRVRg9vIHFxY6.jpg?auto=format&amp;w=260 260w, https://web-dev.imgix.net/image/T4FyVKpzu4WKF1kBNvXepbi08t52/qrYKVjMtRVRg9vIHFxY6.jpg?auto=format&amp;w=296 296w, https://web-dev.imgix.net/image/T4FyVKpzu4WKF1kBNvXepbi08t52/qrYKVjMtRVRg9vIHFxY6.jpg?auto=format&amp;w=338 338w, https://web-dev.imgix.net/image/T4FyVKpzu4WKF1kBNvXepbi08t52/qrYKVjMtRVRg9vIHFxY6.jpg?auto=format&amp;w=385 385w, https://web-dev.imgix.net/image/T4FyVKpzu4WKF1kBNvXepbi08t52/qrYKVjMtRVRg9vIHFxY6.jpg?auto=format&amp;w=439 439w, https://web-dev.imgix.net/image/T4FyVKpzu4WKF1kBNvXepbi08t52/qrYKVjMtRVRg9vIHFxY6.jpg?auto=format&amp;w=500 500w, https://web-dev.imgix.net/image/T4FyVKpzu4WKF1kBNvXepbi08t52/qrYKVjMtRVRg9vIHFxY6.jpg?auto=format&amp;w=571 571w, https://web-dev.imgix.net/image/T4FyVKpzu4WKF1kBNvXepbi08t52/qrYKVjMtRVRg9vIHFxY6.jpg?auto=format&amp;w=650 650w, https://web-dev.imgix.net/image/T4FyVKpzu4WKF1kBNvXepbi08t52/qrYKVjMtRVRg9vIHFxY6.jpg?auto=format&amp;w=741 741w, https://web-dev.imgix.net/image/T4FyVKpzu4WKF1kBNvXepbi08t52/qrYKVjMtRVRg9vIHFxY6.jpg?auto=format&amp;w=845 845w, https://web-dev.imgix.net/image/T4FyVKpzu4WKF1kBNvXepbi08t52/qrYKVjMtRVRg9vIHFxY6.jpg?auto=format&amp;w=964 964w, https://web-dev.imgix.net/image/T4FyVKpzu4WKF1kBNvXepbi08t52/qrYKVjMtRVRg9vIHFxY6.jpg?auto=format&amp;w=1098 1098w, https://web-dev.imgix.net/image/T4FyVKpzu4WKF1kBNvXepbi08t52/qrYKVjMtRVRg9vIHFxY6.jpg?auto=format&amp;w=1252 1252w, https://web-dev.imgix.net/image/T4FyVKpzu4WKF1kBNvXepbi08t52/qrYKVjMtRVRg9vIHFxY6.jpg?auto=format&amp;w=1428 1428w, https://web-dev.imgix.net/image/T4FyVKpzu4WKF1kBNvXepbi08t52/qrYKVjMtRVRg9vIHFxY6.jpg?auto=format&amp;w=1600 1600w&quot; width=&quot;800&quot; /&gt;
&lt;figcaption&gt;The same scene without canvas scaling on the Nexus 10 (16FPS) and scaled to 50% (33FPS).&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;h3 id=&quot;objects-as-building-blocks&quot;&gt;Objects as building blocks &lt;a class=&quot;headline-link&quot; href=&quot;https://web.dev/hobbit/#objects-as-building-blocks&quot;&gt;#&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;To be able to create the big maze of the &lt;a href=&quot;http://middle-earth.thehobbit.com/dolguldur/experience&quot; rel=&quot;noopener&quot;&gt;Dol Guldur&lt;/a&gt; castle and the never ending valley of Rivendell we made a set of building block 3D models that we re-use. Re-using objects lets us ensure that objects are instantiated and uploaded at the start of the experience, and not in the middle of it.&lt;/p&gt;
&lt;figure&gt;
&lt;img alt=&quot;3D object building blocks used in the maze of Dol Guldur.&quot; decoding=&quot;async&quot; height=&quot;450&quot; loading=&quot;lazy&quot; sizes=&quot;(min-width: 800px) 800px, calc(100vw - 48px)&quot; src=&quot;https://web-dev.imgix.net/image/T4FyVKpzu4WKF1kBNvXepbi08t52/uo2fKSut8TWsmFns7TlR.jpg?auto=format&quot; srcset=&quot;https://web-dev.imgix.net/image/T4FyVKpzu4WKF1kBNvXepbi08t52/uo2fKSut8TWsmFns7TlR.jpg?auto=format&amp;w=200 200w, https://web-dev.imgix.net/image/T4FyVKpzu4WKF1kBNvXepbi08t52/uo2fKSut8TWsmFns7TlR.jpg?auto=format&amp;w=228 228w, https://web-dev.imgix.net/image/T4FyVKpzu4WKF1kBNvXepbi08t52/uo2fKSut8TWsmFns7TlR.jpg?auto=format&amp;w=260 260w, https://web-dev.imgix.net/image/T4FyVKpzu4WKF1kBNvXepbi08t52/uo2fKSut8TWsmFns7TlR.jpg?auto=format&amp;w=296 296w, https://web-dev.imgix.net/image/T4FyVKpzu4WKF1kBNvXepbi08t52/uo2fKSut8TWsmFns7TlR.jpg?auto=format&amp;w=338 338w, https://web-dev.imgix.net/image/T4FyVKpzu4WKF1kBNvXepbi08t52/uo2fKSut8TWsmFns7TlR.jpg?auto=format&amp;w=385 385w, https://web-dev.imgix.net/image/T4FyVKpzu4WKF1kBNvXepbi08t52/uo2fKSut8TWsmFns7TlR.jpg?auto=format&amp;w=439 439w, https://web-dev.imgix.net/image/T4FyVKpzu4WKF1kBNvXepbi08t52/uo2fKSut8TWsmFns7TlR.jpg?auto=format&amp;w=500 500w, https://web-dev.imgix.net/image/T4FyVKpzu4WKF1kBNvXepbi08t52/uo2fKSut8TWsmFns7TlR.jpg?auto=format&amp;w=571 571w, https://web-dev.imgix.net/image/T4FyVKpzu4WKF1kBNvXepbi08t52/uo2fKSut8TWsmFns7TlR.jpg?auto=format&amp;w=650 650w, https://web-dev.imgix.net/image/T4FyVKpzu4WKF1kBNvXepbi08t52/uo2fKSut8TWsmFns7TlR.jpg?auto=format&amp;w=741 741w, https://web-dev.imgix.net/image/T4FyVKpzu4WKF1kBNvXepbi08t52/uo2fKSut8TWsmFns7TlR.jpg?auto=format&amp;w=845 845w, https://web-dev.imgix.net/image/T4FyVKpzu4WKF1kBNvXepbi08t52/uo2fKSut8TWsmFns7TlR.jpg?auto=format&amp;w=964 964w, https://web-dev.imgix.net/image/T4FyVKpzu4WKF1kBNvXepbi08t52/uo2fKSut8TWsmFns7TlR.jpg?auto=format&amp;w=1098 1098w, https://web-dev.imgix.net/image/T4FyVKpzu4WKF1kBNvXepbi08t52/uo2fKSut8TWsmFns7TlR.jpg?auto=format&amp;w=1252 1252w, https://web-dev.imgix.net/image/T4FyVKpzu4WKF1kBNvXepbi08t52/uo2fKSut8TWsmFns7TlR.jpg?auto=format&amp;w=1428 1428w, https://web-dev.imgix.net/image/T4FyVKpzu4WKF1kBNvXepbi08t52/uo2fKSut8TWsmFns7TlR.jpg?auto=format&amp;w=1600 1600w&quot; width=&quot;800&quot; /&gt;
&lt;figcaption&gt;3D object building blocks used in the maze of Dol Guldur.&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;p&gt;In Rivendell we have a number of ground sections that we constantly reposition in Z-depth as the user’s journey progresses. As the user passes sections, these are repositioned in the far distance.&lt;/p&gt;
&lt;p&gt;For the Dol Guldur castle we wanted the maze to be regenerated for every game.  To do this we created a script that regenerates the maze.&lt;/p&gt;
&lt;p&gt;Merging the whole structure into one big mesh from the beginning results in a very big scene and poor performance. To address this, we decided to hide and show the building blocks depending on whether they are in view. Right from the start, we had an idea about using a 2D raycaster script but in the end we used the &lt;a href=&quot;https://github.com/mrdoob/three.js/blob/master/src/math/Frustum.js&quot; rel=&quot;noopener&quot;&gt;built-in three.js frustrum culling&lt;/a&gt;. We reused the raycaster script to zoom in on the &amp;quot;danger&amp;quot; the player is facing.&lt;/p&gt;
&lt;p&gt;The next big thing to handle is user interaction. On desktop you have mouse and keyboard input; on mobile devices your users interact with touch, swipe, pinch, device orientation etc.&lt;/p&gt;
&lt;h2 id=&quot;using-touch-interaction-in-mobile-web-experiences&quot;&gt;Using touch interaction in mobile web experiences &lt;a class=&quot;headline-link&quot; href=&quot;https://web.dev/hobbit/#using-touch-interaction-in-mobile-web-experiences&quot;&gt;#&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;Adding touch support isn’t difficult. There are &lt;a href=&quot;http://www.html5rocks.com/en/mobile/touch/&quot; rel=&quot;noopener&quot;&gt;great articles&lt;/a&gt; to read about the topic. But there are some small things that can make it more complicated.&lt;/p&gt;
&lt;p&gt;You can have &lt;strong&gt;both&lt;/strong&gt; touch and mouse. The Chromebook Pixel and other touch enabled laptops have both mouse and touch support. One common mistake is to check if the device is touch enabled and then only add touch event listeners and none for mouse.&lt;/p&gt;
&lt;p&gt;Don’t update rendering in event listeners. Save the touch events to variables instead and react to them in the requestAnimationFrame render loop. This improves performance and also coalesce conflicting events. Make sure that you re-use objects instead of creating new objects in the event listeners.&lt;/p&gt;
&lt;p&gt;Remember that it’s multitouch: event.touches is an array of all touches. In some cases it’s more interesting to look at event.targetTouches or event.changedTouches instead and just react to the touches you are interested in. To separate taps from swipes we use a delay before we check if the touch has moved (swipe) or if it’s still (tap). To get a pinch we measure the distance between the two initial touches and how that changes over time.&lt;/p&gt;
&lt;p&gt;In a 3D world you have to decide how your camera reacts to mouse vs. swipe actions. One common way of adding camera movement is to follow the mouse movement. This can be done with either direct control using mouse position or with a delta movement (position change). You don’t always want the same behaviour on a mobile device as a desktop browser. We tested extensively to decide what felt right for each version.&lt;/p&gt;
&lt;p&gt;When dealing with smaller screens and touchscreens you’ll find that the user’s fingers and UI interaction graphics are often in the way of what you want to show. This is something we are used to when designing native apps but haven’t really had to think about before with web experiences. This is a real challenge for designers and UX designers.&lt;/p&gt;
&lt;h2 id=&quot;summary&quot;&gt;Summary &lt;a class=&quot;headline-link&quot; href=&quot;https://web.dev/hobbit/#summary&quot;&gt;#&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;Our overall experience from this project is that WebGL on mobile works really well, especially on newer, high-end devices. When it comes to performance it seems like polygon count and texture size mostly affect download and initialization times and that materials, shaders and the size of the WebGL canvas are the most important parts to optimise for mobile performance. However, it’s the sum of the parts that affects the performance so everything you can do to optimise counts.&lt;/p&gt;
&lt;p&gt;Targeting mobile devices also means that you have to get used to thinking about touch interactions and that it’s not only about the pixel size  -  it’s the physical size of the screen as well. In some cases we had to move the 3D camera closer to actually see what was going on.&lt;/p&gt;
&lt;p&gt;The experiment is launched and it’s been a fantastic journey. Hope you enjoy it!&lt;/p&gt;
&lt;p&gt;Want to try it out? Take your own &lt;a href=&quot;http://middle-earth.thehobbit.com/&quot; rel=&quot;noopener&quot;&gt;Journey to Middle-Earth&lt;/a&gt;.&lt;/p&gt;
</content>
    <author>
      <name>Daniel Isaksson</name>
    </author>
  </entry>
</feed>
