<?xml version="1.0" encoding="utf-8"?>
<feed xmlns="http://www.w3.org/2005/Atom">
  <id>https://web.dev/</id>
  <title>Marcin Wichary on web.dev</title>
  <updated>2026-04-15T23:21:06Z</updated>
  <author>
    <name>Marcin Wichary</name>
  </author>
  <link href="https://web.dev/authors/marcinwichary/feed.xml" rel="self"/>
  <link href="https://web.dev/"/>
  <icon>https://web-dev.imgix.net/image/T4FyVKpzu4WKF1kBNvXepbi08t52/xUepvN9NAbPTBl8gZNkS.png?auto=format</icon>
  <logo>https://web.dev/images/shared/rss-banner.png</logo>
  <subtitle>Sr. User Experience Designer at Google</subtitle>
  
  
  <entry>
    <title>Jumping the hurdles with the Gamepad API</title>
    <link href="https://web.dev/doodles-gamepad/"/>
    <updated>2012-08-06T00:00:00Z</updated>
    <id>https://web.dev/doodles-gamepad/</id>
    <content type="html" mode="escaped">&lt;h2 id=&quot;introduction&quot;&gt;Introduction &lt;a class=&quot;headline-link&quot; href=&quot;https://web.dev/doodles-gamepad/#introduction&quot;&gt;#&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;Let the novices keep their keyboards for adventure games, their precious multi-touchy-feely fingertips for fruit cutting, and their fancy newfangled motion sensors for pretending they can dance like Michael Jackson. (Newsflash: They can&#39;t.) But you&#39;re different. You&#39;re better. You&#39;re a pro. For you, the games begin and end with a gamepad in your hands.&lt;/p&gt;
&lt;p&gt;But wait. Aren&#39;t you out of luck if you want to support a gamepad in your web app? Not anymore. The brand new &lt;a href=&quot;http://dvcs.w3.org/hg/gamepad/raw-file/default/gamepad.html&quot; rel=&quot;noopener&quot;&gt;Gamepad API&lt;/a&gt; comes to the rescue, allowing you to use JavaScript to read the state of any gamepad controller attached to your computer. It&#39;s so fresh off the presses, that it only landed in Chrome 21 last week – and it&#39;s also on the verge of being supported in Firefox (currently available in a &lt;a href=&quot;http://people.mozilla.com/~tmielczarek/gamepad/&quot; rel=&quot;noopener&quot;&gt;special build&lt;/a&gt;).&lt;/p&gt;
&lt;p&gt;That turned out to be pretty great timing, because we get a chance to use it recently in the &lt;a href=&quot;http://www.google.com/doodles/hurdles-2012&quot; rel=&quot;noopener&quot;&gt;Hurdles 2012 Google doodle&lt;/a&gt;. This article will briefly explain how we added Gamepad API to the doodle, and what we learned during the process.&lt;/p&gt;
&lt;figure&gt;
&lt;img alt=&quot;Hurdles 2012 Google doodle&quot; decoding=&quot;async&quot; height=&quot;207&quot; loading=&quot;lazy&quot; sizes=&quot;(min-width: 522px) 522px, calc(100vw - 48px)&quot; src=&quot;https://web-dev.imgix.net/image/T4FyVKpzu4WKF1kBNvXepbi08t52/T4oshCTuLhNguN14Vuht.png?auto=format&quot; srcset=&quot;https://web-dev.imgix.net/image/T4FyVKpzu4WKF1kBNvXepbi08t52/T4oshCTuLhNguN14Vuht.png?auto=format&amp;w=200 200w, https://web-dev.imgix.net/image/T4FyVKpzu4WKF1kBNvXepbi08t52/T4oshCTuLhNguN14Vuht.png?auto=format&amp;w=228 228w, https://web-dev.imgix.net/image/T4FyVKpzu4WKF1kBNvXepbi08t52/T4oshCTuLhNguN14Vuht.png?auto=format&amp;w=260 260w, https://web-dev.imgix.net/image/T4FyVKpzu4WKF1kBNvXepbi08t52/T4oshCTuLhNguN14Vuht.png?auto=format&amp;w=296 296w, https://web-dev.imgix.net/image/T4FyVKpzu4WKF1kBNvXepbi08t52/T4oshCTuLhNguN14Vuht.png?auto=format&amp;w=338 338w, https://web-dev.imgix.net/image/T4FyVKpzu4WKF1kBNvXepbi08t52/T4oshCTuLhNguN14Vuht.png?auto=format&amp;w=385 385w, https://web-dev.imgix.net/image/T4FyVKpzu4WKF1kBNvXepbi08t52/T4oshCTuLhNguN14Vuht.png?auto=format&amp;w=439 439w, https://web-dev.imgix.net/image/T4FyVKpzu4WKF1kBNvXepbi08t52/T4oshCTuLhNguN14Vuht.png?auto=format&amp;w=500 500w, https://web-dev.imgix.net/image/T4FyVKpzu4WKF1kBNvXepbi08t52/T4oshCTuLhNguN14Vuht.png?auto=format&amp;w=571 571w, https://web-dev.imgix.net/image/T4FyVKpzu4WKF1kBNvXepbi08t52/T4oshCTuLhNguN14Vuht.png?auto=format&amp;w=650 650w, https://web-dev.imgix.net/image/T4FyVKpzu4WKF1kBNvXepbi08t52/T4oshCTuLhNguN14Vuht.png?auto=format&amp;w=741 741w, https://web-dev.imgix.net/image/T4FyVKpzu4WKF1kBNvXepbi08t52/T4oshCTuLhNguN14Vuht.png?auto=format&amp;w=845 845w, https://web-dev.imgix.net/image/T4FyVKpzu4WKF1kBNvXepbi08t52/T4oshCTuLhNguN14Vuht.png?auto=format&amp;w=964 964w, https://web-dev.imgix.net/image/T4FyVKpzu4WKF1kBNvXepbi08t52/T4oshCTuLhNguN14Vuht.png?auto=format&amp;w=1044 1044w&quot; width=&quot;522&quot; /&gt;
&lt;figcaption&gt;Hurdles 2012 Google doodle&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;h2 id=&quot;gamepad-tester&quot;&gt;Gamepad tester &lt;a class=&quot;headline-link&quot; href=&quot;https://web.dev/doodles-gamepad/#gamepad-tester&quot;&gt;#&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;Ephemeral as they are, interactive doodles tend to be pretty complex under the hood. So that it&#39;s easier to demonstrate what we&#39;re talking about, we took the gamepad code from the doodle, and put together a simple gamepad tester. You can use it to see whether your USB gamepad works correctly – and also look under the hood to examine how it&#39;s done.&lt;/p&gt;
&lt;h2 id=&quot;what-browsers-support-it-today&quot;&gt;What browsers support it today? &lt;a class=&quot;headline-link&quot; href=&quot;https://web.dev/doodles-gamepad/#what-browsers-support-it-today&quot;&gt;#&lt;/a&gt;&lt;/h2&gt;
&lt;div class=&quot;wdi-browser-compat&quot;&gt;
  &lt;span class=&quot;wdi-browser-compat__label&quot;&gt;Browser support&lt;/span&gt;
  &lt;ul class=&quot;wdi-browser-compat__items&quot;&gt;
    &lt;li class=&quot;wdi-browser-compat__item&quot;&gt;
    &lt;span class=&quot;wdi-browser-compat__icon&quot; data-browser=&quot;chrome&quot;&gt;
      &lt;span class=&quot;visually-hidden&quot;&gt;Chrome 21, Supported&lt;/span&gt;
    &lt;/span&gt;
    &lt;span class=&quot;wdi-browser-compat__version&quot; data-compat=&quot;yes&quot; title=&quot;Supported&quot; aria-label=&quot;Supported&quot;&gt;
      21
    &lt;/span&gt;
    &lt;/li&gt;&lt;li class=&quot;wdi-browser-compat__item&quot;&gt;
    &lt;span class=&quot;wdi-browser-compat__icon&quot; data-browser=&quot;firefox&quot;&gt;
      &lt;span class=&quot;visually-hidden&quot;&gt;Firefox 29, Supported&lt;/span&gt;
    &lt;/span&gt;
    &lt;span class=&quot;wdi-browser-compat__version&quot; data-compat=&quot;yes&quot; title=&quot;Supported&quot; aria-label=&quot;Supported&quot;&gt;
      29
    &lt;/span&gt;
    &lt;/li&gt;&lt;li class=&quot;wdi-browser-compat__item&quot;&gt;
    &lt;span class=&quot;wdi-browser-compat__icon&quot; data-browser=&quot;edge&quot;&gt;
      &lt;span class=&quot;visually-hidden&quot;&gt;Edge 12, Supported&lt;/span&gt;
    &lt;/span&gt;
    &lt;span class=&quot;wdi-browser-compat__version&quot; data-compat=&quot;yes&quot; title=&quot;Supported&quot; aria-label=&quot;Supported&quot;&gt;
      12
    &lt;/span&gt;
    &lt;/li&gt;&lt;li class=&quot;wdi-browser-compat__item&quot;&gt;
    &lt;span class=&quot;wdi-browser-compat__icon&quot; data-browser=&quot;safari&quot;&gt;
      &lt;span class=&quot;visually-hidden&quot;&gt;Safari 10.1, Supported&lt;/span&gt;
    &lt;/span&gt;
    &lt;span class=&quot;wdi-browser-compat__version&quot; data-compat=&quot;yes&quot; title=&quot;Supported&quot; aria-label=&quot;Supported&quot;&gt;
      10.1
    &lt;/span&gt;
    &lt;/li&gt;
  &lt;/ul&gt;
  &lt;a class=&quot;wdi-browser-compat__link&quot; href=&quot;https://developer.mozilla.org/docs/Web/API/Gamepad#browser_compatibility&quot; target=&quot;_blank&quot;&gt;Source&lt;/a&gt;
&lt;/div&gt;
&lt;h2 id=&quot;what-gamepads-can-be-used&quot;&gt;What gamepads can be used? &lt;a class=&quot;headline-link&quot; href=&quot;https://web.dev/doodles-gamepad/#what-gamepads-can-be-used&quot;&gt;#&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;Generally, any modern gamepad that is supported by your system natively should work. We tested various gamepads from off-brand USB controllers on a PC, through PlayStation 2 gamepads connected via a dongle to a Mac, all the way to Bluetooth controllers paired with a Chrome OS notebook.&lt;/p&gt;
&lt;figure&gt;
&lt;img alt=&quot;Gamepads&quot; decoding=&quot;async&quot; height=&quot;400&quot; loading=&quot;lazy&quot; sizes=&quot;(min-width: 300px) 300px, calc(100vw - 48px)&quot; src=&quot;https://web-dev.imgix.net/image/T4FyVKpzu4WKF1kBNvXepbi08t52/DZxDnKUwgi5p28CJTX5x.jpg?auto=format&quot; srcset=&quot;https://web-dev.imgix.net/image/T4FyVKpzu4WKF1kBNvXepbi08t52/DZxDnKUwgi5p28CJTX5x.jpg?auto=format&amp;w=200 200w, https://web-dev.imgix.net/image/T4FyVKpzu4WKF1kBNvXepbi08t52/DZxDnKUwgi5p28CJTX5x.jpg?auto=format&amp;w=228 228w, https://web-dev.imgix.net/image/T4FyVKpzu4WKF1kBNvXepbi08t52/DZxDnKUwgi5p28CJTX5x.jpg?auto=format&amp;w=260 260w, https://web-dev.imgix.net/image/T4FyVKpzu4WKF1kBNvXepbi08t52/DZxDnKUwgi5p28CJTX5x.jpg?auto=format&amp;w=296 296w, https://web-dev.imgix.net/image/T4FyVKpzu4WKF1kBNvXepbi08t52/DZxDnKUwgi5p28CJTX5x.jpg?auto=format&amp;w=338 338w, https://web-dev.imgix.net/image/T4FyVKpzu4WKF1kBNvXepbi08t52/DZxDnKUwgi5p28CJTX5x.jpg?auto=format&amp;w=385 385w, https://web-dev.imgix.net/image/T4FyVKpzu4WKF1kBNvXepbi08t52/DZxDnKUwgi5p28CJTX5x.jpg?auto=format&amp;w=439 439w, https://web-dev.imgix.net/image/T4FyVKpzu4WKF1kBNvXepbi08t52/DZxDnKUwgi5p28CJTX5x.jpg?auto=format&amp;w=500 500w, https://web-dev.imgix.net/image/T4FyVKpzu4WKF1kBNvXepbi08t52/DZxDnKUwgi5p28CJTX5x.jpg?auto=format&amp;w=571 571w, https://web-dev.imgix.net/image/T4FyVKpzu4WKF1kBNvXepbi08t52/DZxDnKUwgi5p28CJTX5x.jpg?auto=format&amp;w=600 600w&quot; width=&quot;300&quot; /&gt;
&lt;figcaption&gt;Gamepads&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;p&gt;This is a photo of some controllers we used for testing our doodle – &amp;quot;Yes, mom, that really is what I do at work.&amp;quot; If your controller doesn&#39;t work, or if the controls are mapped incorrectly, please &lt;a href=&quot;https://code.google.com/p/chromium/issues/entry?template=Defect%20report%20from%20user&amp;amp;cc=scottmg@chromium.org&amp;amp;labels=Type-Feature,Pri-2,Area-Internals,Feature-Gamepad&quot; rel=&quot;noopener&quot;&gt;file a bug against Chrome&lt;/a&gt; or &lt;a href=&quot;https://bugzilla.mozilla.org/&quot; rel=&quot;noopener&quot;&gt;Firefox&lt;/a&gt; . (Please test in the absolutely newest version of each browser to make sure it&#39;s not already fixed.)&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; Some gamepads have a switch at the back telling them to use a different mode of operation. Toggling that might make them work better. &lt;/div&gt;&lt;/aside&gt;
&lt;aside class=&quot;aside flow bg-state-info-bg color-state-info-text&quot;&gt;&lt;div class=&quot; flow&quot;&gt; Some gamepads have a compatibility mode switch up front that makes four-axis d-pad and the left analogue stick switch places (logically). &lt;/div&gt;&lt;/aside&gt;
&lt;h2 id=&quot;feature-detecting-the-gamepad-apiless&quot;&gt;Feature Detecting the Gamepad API&amp;lt; &lt;a class=&quot;headline-link&quot; href=&quot;https://web.dev/doodles-gamepad/#feature-detecting-the-gamepad-apiless&quot;&gt;#&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;Easy enough in Chrome:&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; gamepadSupportAvailable &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 operator&quot;&gt;!&lt;/span&gt;navigator&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;webkitGetGamepads &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 operator&quot;&gt;!&lt;/span&gt;navigator&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;webkitGamepads&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;&lt;p&gt;It does not seem possible to detect this in Firefox just yet – everything is event-based, and all the event handlers need to be attached to window, which prevents a typical technique of detecting event handlers from working.&lt;/p&gt;
&lt;p&gt;But we&#39;re sure that is temporary. The ever-so-awesome &lt;a href=&quot;http://modernizr.com/&quot; rel=&quot;noopener&quot;&gt;Modernizr&lt;/a&gt; already tells you about the Gamepad API, so we recommend this for all your current and future detecting needs:&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; gamepadSupportAvailable &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; Modernizr&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;gamepads&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;&lt;h2 id=&quot;finding-out-about-connected-gamepads&quot;&gt;Finding out about connected gamepads &lt;a class=&quot;headline-link&quot; href=&quot;https://web.dev/doodles-gamepad/#finding-out-about-connected-gamepads&quot;&gt;#&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;Even if you connect the gamepad, it won&#39;t manifest itself in any way unless the user presses any of its buttons first. This is to prevent fingerprinting, although proves to be a bit of a challenge for user experience: you can&#39;t ask the user to press the button or provide gamepad-specific instructions because you have no idea whether they connected their controller.&lt;/p&gt;
&lt;p&gt;Once you clear that hurdle (sorry…), however, more await.&lt;/p&gt;
&lt;h3 id=&quot;polling&quot;&gt;Polling &lt;a class=&quot;headline-link&quot; href=&quot;https://web.dev/doodles-gamepad/#polling&quot;&gt;#&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;Chrome&#39;s implementation of the API exposes a function – &lt;code&gt;navigator.webkitGetGamepads()&lt;/code&gt; – you can use to get a list of all the gamepads currently plugged into to the system, alongside with their current state (buttons + sticks). The first connected gamepad will be returned as the first entry in the array, and so on.&lt;/p&gt;
&lt;p&gt;(This function call just recently replaced an array you could access directly – &lt;code&gt;navigator.webkitGamepads[]&lt;/code&gt;. As of early August 2012, accessing this array is still necessary in Chrome 21, while the function call works in Chrome 22 and newer. Moving forward, the function call is the recommended way to use the API, and will slowly trickle to all the installed Chrome browsers.)&lt;/p&gt;
&lt;p&gt;The so-far-implemented part of the spec requires you to continuously check the state of connected gamepads (and compare it to the previous one if necessary), instead of firing events when things change. We relied on &lt;a href=&quot;http://www.html5rocks.com/tutorials/speed/animations/&quot; rel=&quot;noopener&quot;&gt;requestAnimationFrame()&lt;/a&gt; to set up polling in the most efficient and battery-friendly way. For our doodle, even though we already have a &lt;code&gt;requestAnimationFrame()&lt;/code&gt; loop to support animations, we created a second completely separate one – it was simpler to code and shouldn&#39;t affect performance in any way.&lt;/p&gt;
&lt;p&gt;Here is the code from the tester:&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;/**&lt;br /&gt; * Starts a polling loop to check for gamepad state.&lt;br /&gt; */&lt;/span&gt;&lt;br /&gt;&lt;span class=&quot;token function-variable function&quot;&gt;startPolling&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;function&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 comment&quot;&gt;// Don&#39;t accidentally start a second loop, man.&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;&lt;span class=&quot;token operator&quot;&gt;!&lt;/span&gt;gamepadSupport&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;ticking&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;    gamepadSupport&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;ticking &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;br /&gt;    gamepadSupport&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;tick&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 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 punctuation&quot;&gt;,&lt;/span&gt;&lt;br /&gt;&lt;br /&gt;&lt;span class=&quot;token comment&quot;&gt;/**&lt;br /&gt; * Stops a polling loop by setting a flag which will prevent the next&lt;br /&gt; * requestAnimationFrame() from being scheduled.&lt;br /&gt; */&lt;/span&gt;&lt;br /&gt;&lt;span class=&quot;token function-variable function&quot;&gt;stopPolling&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;function&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;    gamepadSupport&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;ticking &lt;span class=&quot;token operator&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;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;br /&gt;&lt;br /&gt;&lt;span class=&quot;token comment&quot;&gt;/**&lt;br /&gt; * A function called with each requestAnimationFrame(). Polls the gamepad&lt;br /&gt; * status and schedules another poll.&lt;br /&gt; */&lt;/span&gt;&lt;br /&gt;&lt;span class=&quot;token function-variable function&quot;&gt;tick&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;function&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;    gamepadSupport&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;pollStatus&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;    gamepadSupport&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;scheduleNextTick&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 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 function-variable function&quot;&gt;scheduleNextTick&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;function&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 comment&quot;&gt;// Only schedule the next frame if we haven&#39;t decided to stop via&lt;/span&gt;&lt;br /&gt;    &lt;span class=&quot;token comment&quot;&gt;// stopPolling() before.&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;gamepadSupport&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;ticking&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;window&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;requestAnimationFrame&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;        window&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;requestAnimationFrame&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;gamepadSupport&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;tick&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 keyword&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;window&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;mozRequestAnimationFrame&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;        window&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;mozRequestAnimationFrame&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;gamepadSupport&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;tick&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 keyword&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;window&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;webkitRequestAnimationFrame&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;        window&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;webkitRequestAnimationFrame&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;gamepadSupport&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;tick&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 comment&quot;&gt;// Note lack of setTimeout since all the browsers that support&lt;/span&gt;&lt;br /&gt;    &lt;span class=&quot;token comment&quot;&gt;// Gamepad API are already supporting requestAnimationFrame().&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;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;&lt;br /&gt;&lt;br /&gt;&lt;span class=&quot;token comment&quot;&gt;/**&lt;br /&gt; * Checks for the gamepad status. Monitors the necessary data and notices&lt;br /&gt; * the differences from previous state (buttons for Chrome/Firefox,&lt;br /&gt; * new connects/disconnects for Chrome). If differences are noticed, asks&lt;br /&gt; * to update the display accordingly. Should run as close to 60 frames per&lt;br /&gt; * second as possible.&lt;br /&gt; */&lt;/span&gt;&lt;br /&gt;&lt;span class=&quot;token function-variable function&quot;&gt;pollStatus&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;function&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 comment&quot;&gt;// (Code goes here.)&lt;/span&gt;&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;/code&gt;&lt;/pre&gt;
&lt;/div&gt;&lt;p&gt;If you only care about one gamepad, getting its data might be as simple as:&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; gamepad &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; navigator&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;webkitGetGamepads &lt;span class=&quot;token operator&quot;&gt;&amp;amp;&amp;amp;&lt;/span&gt; navigator&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;webkitGetGamepads&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 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;/code&gt;&lt;/pre&gt;
&lt;/div&gt;&lt;p&gt;/
If you want to be a bit more clever, or support more than one player simultaneously, you will need to add a few more lines of code to react to more complex scenarious (two or more gamepads connected, some of them getting disconnected mid-way, etc.). You can look at the &lt;a href=&quot;https://github.com/html5rocks/www.html5rocks.com/blob/master/content/tutorials/doodles/gamepad/static/gamepad-tester/gamepad.js#L187&quot; rel=&quot;noopener&quot;&gt;source code&lt;/a&gt; of our tester, function &lt;code&gt;pollGamepads()&lt;/code&gt;, for one approach on how to solve this.&lt;/p&gt;
&lt;h3 id=&quot;events&quot;&gt;Events &lt;a class=&quot;headline-link&quot; href=&quot;https://web.dev/doodles-gamepad/#events&quot;&gt;#&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;Firefox uses an alternative, better way described in the Gamepad API spec. Instead of asking you to poll, it exposes two events – &lt;code&gt;MozGamepadConnected&lt;/code&gt; and &lt;code&gt;MozGamepadDisconnected&lt;/code&gt; – which are fired whenever a gamepad gets plugged in (or, more precisely, plugged in and &amp;quot;announced&amp;quot; by pressing any of its buttons) or unplugged. The gamepad object that will continue to reflect its future state is passed as the &lt;code&gt;.gamepad&lt;/code&gt; parameter of the event object.&lt;/p&gt;
&lt;p&gt;From the tester source 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;/**&lt;br /&gt; * React to the gamepad being connected. Today, this will only be executed&lt;br /&gt; * on Firefox.&lt;br /&gt; */&lt;/span&gt;&lt;br /&gt;&lt;span class=&quot;token function-variable function&quot;&gt;onGamepadConnect&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;function&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token parameter&quot;&gt;event&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;// Add the new gamepad on the list of gamepads to look after.&lt;/span&gt;&lt;br /&gt;    gamepadSupport&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;gamepads&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;event&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;gamepad&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;// Start the polling loop to monitor button changes.&lt;/span&gt;&lt;br /&gt;    gamepadSupport&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;startPolling&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 comment&quot;&gt;// Ask the tester to update the screen to show more gamepads.&lt;/span&gt;&lt;br /&gt;    tester&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;updateGamepads&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;gamepadSupport&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;gamepads&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 punctuation&quot;&gt;,&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;&lt;h3 id=&quot;summary&quot;&gt;Summary &lt;a class=&quot;headline-link&quot; href=&quot;https://web.dev/doodles-gamepad/#summary&quot;&gt;#&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;In the end, our initialization function in the tester, supporting both approaches, looks 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 comment&quot;&gt;/**&lt;br /&gt; * Initialize support for Gamepad API.&lt;br /&gt; */&lt;/span&gt;&lt;br /&gt;&lt;span class=&quot;token function-variable function&quot;&gt;init&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;function&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 comment&quot;&gt;// As of writing, it seems impossible to detect Gamepad API support&lt;/span&gt;&lt;br /&gt;    &lt;span class=&quot;token comment&quot;&gt;// in Firefox, hence we need to hardcode it in the third clause.&lt;/span&gt;&lt;br /&gt;    &lt;span class=&quot;token comment&quot;&gt;// (The preceding two clauses are for Chrome.)&lt;/span&gt;&lt;br /&gt;    &lt;span class=&quot;token keyword&quot;&gt;var&lt;/span&gt; gamepadSupportAvailable &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 operator&quot;&gt;!&lt;/span&gt;navigator&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;webkitGetGamepads &lt;span class=&quot;token operator&quot;&gt;||&lt;/span&gt;&lt;br /&gt;        &lt;span class=&quot;token operator&quot;&gt;!&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;!&lt;/span&gt;navigator&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;webkitGamepads &lt;span class=&quot;token operator&quot;&gt;||&lt;/span&gt;&lt;br /&gt;        &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;navigator&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;userAgent&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;indexOf&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&#39;Firefox/&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &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;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;&lt;span class=&quot;token operator&quot;&gt;!&lt;/span&gt;gamepadSupportAvailable&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;// It doesn&#39;t seem Gamepad API is available – show a message telling&lt;/span&gt;&lt;br /&gt;    &lt;span class=&quot;token comment&quot;&gt;// the visitor about it.&lt;/span&gt;&lt;br /&gt;    tester&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;showNotSupported&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 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;// Firefox supports the connect/disconnect event, so we attach event&lt;/span&gt;&lt;br /&gt;    &lt;span class=&quot;token comment&quot;&gt;// handlers to those.&lt;/span&gt;&lt;br /&gt;    window&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;addEventListener&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&#39;MozGamepadConnected&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;&lt;br /&gt;                            gamepadSupport&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;onGamepadConnect&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;    window&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;addEventListener&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&#39;MozGamepadDisconnected&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;&lt;br /&gt;                            gamepadSupport&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;onGamepadDisconnect&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;&lt;br /&gt;    &lt;span class=&quot;token comment&quot;&gt;// Since Chrome only supports polling, we initiate polling loop straight&lt;/span&gt;&lt;br /&gt;    &lt;span class=&quot;token comment&quot;&gt;// away. For Firefox, we will only do it if we get a connect event.&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;&lt;span class=&quot;token operator&quot;&gt;!&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;!&lt;/span&gt;navigator&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;webkitGamepads &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 operator&quot;&gt;!&lt;/span&gt;navigator&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;webkitGetGamepads&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;        gamepadSupport&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;startPolling&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 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;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;&lt;h2 id=&quot;gamepad-info&quot;&gt;Gamepad info &lt;a class=&quot;headline-link&quot; href=&quot;https://web.dev/doodles-gamepad/#gamepad-info&quot;&gt;#&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;Every gamepad connected to the system will be represented by an object looking something 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 literal-property property&quot;&gt;id&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;﻿PLAYSTATION(R)3 Controller (STANDARD GAMEPAD Vendor: 054c Product: 0268)&quot;&lt;/span&gt;&lt;br /&gt;&lt;span class=&quot;token literal-property property&quot;&gt;index&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;br /&gt;&lt;span class=&quot;token literal-property property&quot;&gt;timestamp&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;18395424738498&lt;/span&gt;&lt;br /&gt;&lt;span class=&quot;token literal-property property&quot;&gt;buttons&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; Array&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token number&quot;&gt;8&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;br /&gt;    &lt;span class=&quot;token number&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;0&lt;/span&gt;&lt;br /&gt;    &lt;span class=&quot;token number&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;0&lt;/span&gt;&lt;br /&gt;    &lt;span class=&quot;token number&quot;&gt;2&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;br /&gt;    &lt;span class=&quot;token number&quot;&gt;3&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;0&lt;/span&gt;&lt;br /&gt;    &lt;span class=&quot;token number&quot;&gt;4&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;0&lt;/span&gt;&lt;br /&gt;    &lt;span class=&quot;token number&quot;&gt;5&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;0&lt;/span&gt;&lt;br /&gt;    &lt;span class=&quot;token number&quot;&gt;6&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;0.03291&lt;/span&gt;&lt;br /&gt;    &lt;span class=&quot;token number&quot;&gt;7&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;0&lt;/span&gt;&lt;br /&gt;&lt;span class=&quot;token literal-property property&quot;&gt;axes&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; Array&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token number&quot;&gt;4&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;br /&gt;    &lt;span class=&quot;token number&quot;&gt;0&lt;/span&gt;&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;0.01176&lt;/span&gt;&lt;br /&gt;    &lt;span class=&quot;token number&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;0.01961&lt;/span&gt;&lt;br /&gt;    &lt;span class=&quot;token number&quot;&gt;2&lt;/span&gt;&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;0.00392&lt;/span&gt;&lt;br /&gt;    &lt;span class=&quot;token number&quot;&gt;3&lt;/span&gt;&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;0.01176&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;&lt;h3 id=&quot;basic-info&quot;&gt;Basic info &lt;a class=&quot;headline-link&quot; href=&quot;https://web.dev/doodles-gamepad/#basic-info&quot;&gt;#&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;The top few fields are simple metadata:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;id&lt;/code&gt;: a textual description of the gamepad&lt;/li&gt;
&lt;li&gt;&lt;code&gt;index&lt;/code&gt;: an integer useful to tell different gamepads attached to one computer apart&lt;/li&gt;
&lt;li&gt;&lt;code&gt;timestamp&lt;/code&gt;: the timestamp of the last update of the button/axes state (currently only supported in Chrome)&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&quot;buttons-and-sticks&quot;&gt;Buttons and sticks &lt;a class=&quot;headline-link&quot; href=&quot;https://web.dev/doodles-gamepad/#buttons-and-sticks&quot;&gt;#&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;Today&#39;s gamepads are not exactly what your grandpa might have used to save the princess in the wrong castle – they typically have at least sixteen separate buttons (some discrete, some analogue), in addition to two analogue sticks. Gamepad API will tell you about all the buttons and analogue sticks that are reported by the operating system.&lt;/p&gt;
&lt;p&gt;Once you get the current state in the gamepad object, you can access the buttons via &lt;code&gt;.buttons[]&lt;/code&gt; and sticks via &lt;code&gt;.axes[]&lt;/code&gt; arrays. Here&#39;s a visual summary of what they correspond to:&lt;/p&gt;
&lt;figure&gt;
&lt;img alt=&quot;Gamepad Diagram&quot; decoding=&quot;async&quot; height=&quot;503&quot; loading=&quot;lazy&quot; sizes=&quot;(min-width: 800px) 800px, calc(100vw - 48px)&quot; src=&quot;https://web-dev.imgix.net/image/T4FyVKpzu4WKF1kBNvXepbi08t52/e0FBSNiQGkWsx45BgIMA.png?auto=format&quot; srcset=&quot;https://web-dev.imgix.net/image/T4FyVKpzu4WKF1kBNvXepbi08t52/e0FBSNiQGkWsx45BgIMA.png?auto=format&amp;w=200 200w, https://web-dev.imgix.net/image/T4FyVKpzu4WKF1kBNvXepbi08t52/e0FBSNiQGkWsx45BgIMA.png?auto=format&amp;w=228 228w, https://web-dev.imgix.net/image/T4FyVKpzu4WKF1kBNvXepbi08t52/e0FBSNiQGkWsx45BgIMA.png?auto=format&amp;w=260 260w, https://web-dev.imgix.net/image/T4FyVKpzu4WKF1kBNvXepbi08t52/e0FBSNiQGkWsx45BgIMA.png?auto=format&amp;w=296 296w, https://web-dev.imgix.net/image/T4FyVKpzu4WKF1kBNvXepbi08t52/e0FBSNiQGkWsx45BgIMA.png?auto=format&amp;w=338 338w, https://web-dev.imgix.net/image/T4FyVKpzu4WKF1kBNvXepbi08t52/e0FBSNiQGkWsx45BgIMA.png?auto=format&amp;w=385 385w, https://web-dev.imgix.net/image/T4FyVKpzu4WKF1kBNvXepbi08t52/e0FBSNiQGkWsx45BgIMA.png?auto=format&amp;w=439 439w, https://web-dev.imgix.net/image/T4FyVKpzu4WKF1kBNvXepbi08t52/e0FBSNiQGkWsx45BgIMA.png?auto=format&amp;w=500 500w, https://web-dev.imgix.net/image/T4FyVKpzu4WKF1kBNvXepbi08t52/e0FBSNiQGkWsx45BgIMA.png?auto=format&amp;w=571 571w, https://web-dev.imgix.net/image/T4FyVKpzu4WKF1kBNvXepbi08t52/e0FBSNiQGkWsx45BgIMA.png?auto=format&amp;w=650 650w, https://web-dev.imgix.net/image/T4FyVKpzu4WKF1kBNvXepbi08t52/e0FBSNiQGkWsx45BgIMA.png?auto=format&amp;w=741 741w, https://web-dev.imgix.net/image/T4FyVKpzu4WKF1kBNvXepbi08t52/e0FBSNiQGkWsx45BgIMA.png?auto=format&amp;w=845 845w, https://web-dev.imgix.net/image/T4FyVKpzu4WKF1kBNvXepbi08t52/e0FBSNiQGkWsx45BgIMA.png?auto=format&amp;w=964 964w, https://web-dev.imgix.net/image/T4FyVKpzu4WKF1kBNvXepbi08t52/e0FBSNiQGkWsx45BgIMA.png?auto=format&amp;w=1098 1098w, https://web-dev.imgix.net/image/T4FyVKpzu4WKF1kBNvXepbi08t52/e0FBSNiQGkWsx45BgIMA.png?auto=format&amp;w=1252 1252w, https://web-dev.imgix.net/image/T4FyVKpzu4WKF1kBNvXepbi08t52/e0FBSNiQGkWsx45BgIMA.png?auto=format&amp;w=1428 1428w, https://web-dev.imgix.net/image/T4FyVKpzu4WKF1kBNvXepbi08t52/e0FBSNiQGkWsx45BgIMA.png?auto=format&amp;w=1600 1600w&quot; width=&quot;800&quot; /&gt;
&lt;figcaption&gt;Gamepad Diagram&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;p&gt;The spec asks the browser to map first sixteen buttons and four axes to:&lt;/p&gt;
&lt;div&gt;&lt;pre class=&quot;language-js&quot;&gt;&lt;code class=&quot;language-js&quot;&gt;gamepad&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token constant&quot;&gt;BUTTONS&lt;/span&gt; &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 constant&quot;&gt;FACE_1&lt;/span&gt;&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;span class=&quot;token comment&quot;&gt;// Face (main) buttons&lt;/span&gt;&lt;br /&gt;    &lt;span class=&quot;token constant&quot;&gt;FACE_2&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;span class=&quot;token constant&quot;&gt;FACE_3&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;2&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;&lt;br /&gt;    &lt;span class=&quot;token constant&quot;&gt;FACE_4&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;3&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;&lt;br /&gt;    &lt;span class=&quot;token constant&quot;&gt;LEFT_SHOULDER&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;4&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token comment&quot;&gt;// Top shoulder buttons&lt;/span&gt;&lt;br /&gt;    &lt;span class=&quot;token constant&quot;&gt;RIGHT_SHOULDER&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;5&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;&lt;br /&gt;    &lt;span class=&quot;token constant&quot;&gt;LEFT_SHOULDER_BOTTOM&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;6&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token comment&quot;&gt;// Bottom shoulder buttons&lt;/span&gt;&lt;br /&gt;    &lt;span class=&quot;token constant&quot;&gt;RIGHT_SHOULDER_BOTTOM&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;7&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;&lt;br /&gt;    &lt;span class=&quot;token constant&quot;&gt;SELECT&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;8&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;&lt;br /&gt;    &lt;span class=&quot;token constant&quot;&gt;START&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;9&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;&lt;br /&gt;    &lt;span class=&quot;token constant&quot;&gt;LEFT_ANALOGUE_STICK&lt;/span&gt;&lt;span class=&quot;token operator&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 comment&quot;&gt;// Analogue sticks (if depressible)&lt;/span&gt;&lt;br /&gt;    &lt;span class=&quot;token constant&quot;&gt;RIGHT_ANALOGUE_STICK&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;11&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;&lt;br /&gt;    &lt;span class=&quot;token constant&quot;&gt;PAD_TOP&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;12&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token comment&quot;&gt;// Directional (discrete) pad&lt;/span&gt;&lt;br /&gt;    &lt;span class=&quot;token constant&quot;&gt;PAD_BOTTOM&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;13&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;&lt;br /&gt;    &lt;span class=&quot;token constant&quot;&gt;PAD_LEFT&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;14&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;&lt;br /&gt;    &lt;span class=&quot;token constant&quot;&gt;PAD_RIGHT&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;15&lt;/span&gt;&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;br /&gt;&lt;br /&gt;gamepad&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token constant&quot;&gt;AXES&lt;/span&gt; &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 constant&quot;&gt;LEFT_ANALOGUE_HOR&lt;/span&gt;&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 constant&quot;&gt;LEFT_ANALOGUE_VERT&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;span class=&quot;token constant&quot;&gt;RIGHT_ANALOGUE_HOR&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;2&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;&lt;br /&gt;    &lt;span class=&quot;token constant&quot;&gt;RIGHT_ANALOGUE_VERT&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;3&lt;/span&gt;&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;/code&gt;&lt;/pre&gt;
&lt;/div&gt;&lt;p&gt;The extra buttons and axes will be appended to the ones above. Please note that neither sixteen buttons nor four axes are guaranteed, though – be ready for some of them to simply be undefined.&lt;/p&gt;
&lt;p&gt;The buttons can take values from 0.0 (not pressed) to 1.0 (pressed completely). The axes go from -1.0 (completely left or up) through 0.0 (center) to 1.0 (completely right or down).&lt;/p&gt;
&lt;h3 id=&quot;analogue-or-discrete&quot;&gt;Analogue or discrete? &lt;a class=&quot;headline-link&quot; href=&quot;https://web.dev/doodles-gamepad/#analogue-or-discrete&quot;&gt;#&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;Ostensibly, every button could be an analogue one – this is somewhat common for shoulder buttons, for example. Therefore, it is best to set a threshold rather than just compare it bluntly to 1.00 (what if an analogue button happens to be slightly dirty? It might never reach 1.00). In our doodle we do it this way:&lt;/p&gt;
&lt;div&gt;&lt;pre class=&quot;language-js&quot;&gt;&lt;code class=&quot;language-js&quot;&gt;gamepad&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token constant&quot;&gt;ANALOGUE_BUTTON_THRESHOLD&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;.5&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;&lt;br /&gt;&lt;br /&gt;gamepad&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function-variable function&quot;&gt;buttonPressed_&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;function&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token parameter&quot;&gt;pad&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; buttonId&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;return&lt;/span&gt; pad&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;buttons&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;buttonId&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;&amp;amp;&amp;amp;&lt;/span&gt;&lt;br /&gt;            &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;pad&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;buttons&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;buttonId&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;&gt;&lt;/span&gt; gamepad&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token constant&quot;&gt;ANALOGUE_BUTTON_THRESHOLD&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;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;&lt;p&gt;You can do the same to turn analogue sticks into digital joysticks. Sure, there is always the digital pad (d-pad), but your gamepad might not have one. Here&#39;s our code to handle that:&lt;/p&gt;
&lt;div&gt;&lt;pre class=&quot;language-js&quot;&gt;&lt;code class=&quot;language-js&quot;&gt;gamepad&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token constant&quot;&gt;AXIS_THRESHOLD&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;.75&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;&lt;br /&gt;&lt;br /&gt;gamepad&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function-variable function&quot;&gt;stickMoved_&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;function&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token parameter&quot;&gt;pad&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; axisId&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; negativeDirection&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;&lt;span class=&quot;token keyword&quot;&gt;typeof&lt;/span&gt; pad&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;axes&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;axisId&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;==&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&#39;undefined&#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;    &lt;span class=&quot;token keyword&quot;&gt;return&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;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;negativeDirection&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; pad&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;axes&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;axisId&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;&amp;lt;&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;-&lt;/span&gt;gamepad&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token constant&quot;&gt;AXIS_THRESHOLD&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 keyword&quot;&gt;return&lt;/span&gt; pad&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;axes&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;axisId&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;&gt;&lt;/span&gt; gamepad&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token constant&quot;&gt;AXIS_THRESHOLD&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;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;&lt;h2 id=&quot;button-presses-and-stick-movements&quot;&gt;Button presses and stick movements &lt;a class=&quot;headline-link&quot; href=&quot;https://web.dev/doodles-gamepad/#button-presses-and-stick-movements&quot;&gt;#&lt;/a&gt;&lt;/h2&gt;
&lt;h3 id=&quot;events-2&quot;&gt;Events &lt;a class=&quot;headline-link&quot; href=&quot;https://web.dev/doodles-gamepad/#events-2&quot;&gt;#&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;In some cases, like a flight simulator game, continuously checking and reacting to stick positions or button presses makes more sense… but for things like Hurdles 2012 doodle? You might wonder: Why do I need to check for buttons every single frame? Why can&#39;t I get events like I do for keyboard or mouse up/down?&lt;/p&gt;
&lt;p&gt;Good news is, you can. Bad news is – in the future. It&#39;s in the spec, but not implemented in any browser yet.&lt;/p&gt;
&lt;h3 id=&quot;polling-2&quot;&gt;Polling &lt;a class=&quot;headline-link&quot; href=&quot;https://web.dev/doodles-gamepad/#polling-2&quot;&gt;#&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;In the meantime, your way out is comparing the current and previous state, and calling functions if you see any difference. For example:&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;if&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;buttonPressed&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;pad&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 operator&quot;&gt;!=&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;buttonPressed&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;oldPad&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;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;&lt;br /&gt;    &lt;span class=&quot;token function&quot;&gt;buttonEvent&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 function&quot;&gt;buttonPressed&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;pad&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 operator&quot;&gt;?&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&#39;down&#39;&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&#39;up&#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;&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;&lt;aside class=&quot;aside flow bg-state-info-bg color-state-info-text&quot;&gt;&lt;div class=&quot; flow&quot;&gt; Compare timestamps of the current and previously polled gamepad data (the timestamp field). If they&#39;re defined (Firefox doesn&#39;t support it yet) and identical, it means the state didn&#39;t change from the last time you looked at it, and no further work is necessary. From the tester source code: &lt;/div&gt;&lt;/aside&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;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 keyword&quot;&gt;in&lt;/span&gt; gamepadSupport&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;gamepads&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; gamepad &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; gamepadSupport&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;gamepads&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;br /&gt;    &lt;span class=&quot;token comment&quot;&gt;// Don&#39;t do anything if the current timestamp is the same as previous&lt;/span&gt;&lt;br /&gt;    &lt;span class=&quot;token comment&quot;&gt;// one, which means that the state of the gamepad hasn&#39;t changed.&lt;/span&gt;&lt;br /&gt;    &lt;span class=&quot;token comment&quot;&gt;// This is only supported by Chrome right now, so the first check&lt;/span&gt;&lt;br /&gt;    &lt;span class=&quot;token comment&quot;&gt;// makes sure we&#39;re not doing anything if the timestamps are empty&lt;/span&gt;&lt;br /&gt;    &lt;span class=&quot;token comment&quot;&gt;// or undefined.&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;gamepadSupport&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;prevTimestamps&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 operator&quot;&gt;&amp;amp;&amp;amp;&lt;/span&gt;&lt;br /&gt;        &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;gamepad&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;timestamp &lt;span class=&quot;token operator&quot;&gt;==&lt;/span&gt; gamepadSupport&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;prevTimestamps&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;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;continue&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;    gamepadSupport&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;prevTimestamps&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 operator&quot;&gt;=&lt;/span&gt; gamepad&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;timestamp&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;&lt;br /&gt;&lt;br /&gt;    gamepadSupport&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;updateDisplay&lt;/span&gt;&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 punctuation&quot;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;&lt;h3 id=&quot;the-keyboard-first-approach-in-the-hurdles-2012-doodle&quot;&gt;The keyboard-first approach in the Hurdles 2012 doodle &lt;a class=&quot;headline-link&quot; href=&quot;https://web.dev/doodles-gamepad/#the-keyboard-first-approach-in-the-hurdles-2012-doodle&quot;&gt;#&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;Since without a gamepad, our today&#39;s doodle&#39;s preferred input method is the keyboard, we decided for the gamepad to emulate it rather closely. This meant three decisions:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;The doodle only needs three buttons – two for running, and one for jumping – but the gamepad is likely to have many more. We therefore mapped all the sixteen known buttons and two known sticks onto those three logical functions in a way we thought made most sense, so that people could run by: alternating A/B buttons, alternating shoulder buttons, pressing left/right on the d-pad, or swinging either stick violently left and right (some of those will, of course, be more efficient than the others). For example:&lt;/p&gt;
&lt;div&gt;&lt;pre class=&quot;language-js&quot;&gt;&lt;code class=&quot;language-js&quot;&gt;newState&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;gamepad&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token constant&quot;&gt;STATES&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token constant&quot;&gt;LEFT&lt;/span&gt;&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;    gamepad&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;buttonPressed_&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;pad&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; gamepad&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token constant&quot;&gt;BUTTONS&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token constant&quot;&gt;PAD_LEFT&lt;/span&gt;&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;    gamepad&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;stickMoved_&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;pad&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; gamepad&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token constant&quot;&gt;AXES&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token constant&quot;&gt;LEFT_ANALOGUE_HOR&lt;/span&gt;&lt;span class=&quot;token punctuation&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 operator&quot;&gt;||&lt;/span&gt;&lt;br /&gt;    gamepad&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;stickMoved_&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;pad&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; gamepad&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token constant&quot;&gt;AXES&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token constant&quot;&gt;RIGHT_ANALOGUE_HOR&lt;/span&gt;&lt;span class=&quot;token punctuation&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;br /&gt;&lt;br /&gt;newState&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;gamepad&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token constant&quot;&gt;STATES&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token constant&quot;&gt;PRIMARY_BUTTON&lt;/span&gt;&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;    gamepad&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;buttonPressed_&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;pad&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; gamepad&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token constant&quot;&gt;BUTTONS&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token constant&quot;&gt;FACE_1&lt;/span&gt;&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;    gamepad&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;buttonPressed_&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;pad&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; gamepad&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token constant&quot;&gt;BUTTONS&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token constant&quot;&gt;LEFT_SHOULDER&lt;/span&gt;&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;    gamepad&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;buttonPressed_&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;pad&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; gamepad&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token constant&quot;&gt;BUTTONS&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token constant&quot;&gt;LEFT_SHOULDER_BOTTOM&lt;/span&gt;&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;    gamepad&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;buttonPressed_&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;pad&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; gamepad&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token constant&quot;&gt;BUTTONS&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token constant&quot;&gt;SELECT&lt;/span&gt;&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;    gamepad&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;buttonPressed_&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;pad&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; gamepad&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token constant&quot;&gt;BUTTONS&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token constant&quot;&gt;START&lt;/span&gt;&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;    gamepad&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;buttonPressed_&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;pad&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; gamepad&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token constant&quot;&gt;BUTTONS&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token constant&quot;&gt;LEFT_ANALOGUE_STICK&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;/li&gt;
&lt;li&gt;
&lt;p&gt;We treated each analogue input as a discrete one, using the threshold functions described previously.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;We went as far as bolting the gamepad input onto the doodle, instead of baking it in – our polling loop actually synthesizes necessary keydown and keyup events (with a proper keyCode) and sends them back to DOM:&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;// Create and dispatch a corresponding key event.&lt;/span&gt;&lt;br /&gt;&lt;span class=&quot;token keyword&quot;&gt;var&lt;/span&gt; event &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;createEvent&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&#39;Event&#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;&lt;span class=&quot;token keyword&quot;&gt;var&lt;/span&gt; eventName &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; down &lt;span class=&quot;token operator&quot;&gt;?&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&#39;keydown&#39;&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&#39;keyup&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;&lt;br /&gt;event&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;initEvent&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;eventName&lt;span class=&quot;token punctuation&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 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;br /&gt;event&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;keyCode &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; gamepad&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;stateToKeyCodeMap_&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;state&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;gamepad&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;containerElement_&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;dispatchEvent&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;event&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;/li&gt;
&lt;/ol&gt;
&lt;p&gt;And that&#39;s all there is to it!&lt;/p&gt;
&lt;h2 id=&quot;tips-and-tricks&quot;&gt;Tips and tricks &lt;a class=&quot;headline-link&quot; href=&quot;https://web.dev/doodles-gamepad/#tips-and-tricks&quot;&gt;#&lt;/a&gt;&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;Remember that the gamepad won&#39;t be visible in your browser at all before a button is pressed.&lt;/li&gt;
&lt;li&gt;If you are testing the gamepad in different browsers simultaneously, note that only one of them can sense the controller. If you&#39;re not receiving any events, make sure to close the other pages that might be using it. Plus, from our experience, sometimes a browser can &amp;quot;hold onto&amp;quot; the gamepad even if you close the tab or quit the browser itself. Restarting the system is sometimes the only way to fix things.&lt;/li&gt;
&lt;li&gt;As always, use &lt;a href=&quot;https://tools.google.com/dlpage/chromesxs/&quot; rel=&quot;noopener&quot;&gt;Chrome Canary&lt;/a&gt; and the equivalents for other browsers to make sure you&#39;re experiencing the best support – and then act appropriately if you see older versions behaving differently.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id=&quot;the-future&quot;&gt;The Future &lt;a class=&quot;headline-link&quot; href=&quot;https://web.dev/doodles-gamepad/#the-future&quot;&gt;#&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;We hope this helps to shed some light on this new API – still a bit precarious, but already a lot of fun.&lt;/p&gt;
&lt;p&gt;In addition to the missing pieces of the API (e.g. events) and broader browser support, we&#39;re also hoping to eventually see things like rumble control, access to built-in gyroscopes, etc. And, more support for different types of gamepads – please &lt;a href=&quot;https://code.google.com/p/chromium/issues/entry?template=Defect%20report%20from%20user&amp;amp;cc=scottmg@chromium.org&amp;amp;labels=Type-Feature,Pri-2,Area-Internals,Feature-Gamepad&quot; rel=&quot;noopener&quot;&gt;file a bug against Chrome&lt;/a&gt; and/or &lt;a href=&quot;https://bugzilla.mozilla.org/&quot; rel=&quot;noopener&quot;&gt;file a bug against Firefox&lt;/a&gt; if you see find one that works incorrectly or not at all.&lt;/p&gt;
&lt;p&gt;But before that, go and play with our &lt;a href=&quot;http://www.google.com/doodles/hurdles-2012&quot; rel=&quot;noopener&quot;&gt;Hurdles 2012 doodle&lt;/a&gt; and see how much more fun it is on the gamepad. Oh, did you just say you could do better than 10.7 seconds? Bring it.&lt;/p&gt;
&lt;h2 id=&quot;further-reading&quot;&gt;Further reading &lt;a class=&quot;headline-link&quot; href=&quot;https://web.dev/doodles-gamepad/#further-reading&quot;&gt;#&lt;/a&gt;&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;http://dvcs.w3.org/hg/gamepad/raw-file/default/gamepad.html&quot; rel=&quot;noopener&quot;&gt;W3C Gamepad API specification&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://developer.mozilla.org/API/Gamepad/Using_Gamepad_API&quot; rel=&quot;noopener&quot;&gt;MDN Gamepad API Docs&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
</content>
    <author>
      <name>Marcin Wichary</name>
    </author>
  </entry>
  
  <entry>
    <title>Case Study - Building the Stanisław Lem Google doodle</title>
    <link href="https://web.dev/doodles-lem/"/>
    <updated>2011-08-06T00:00:00Z</updated>
    <id>https://web.dev/doodles-lem/</id>
    <content type="html" mode="escaped">&lt;h2 id=&quot;hello,-strange-world&quot;&gt;Hello, (strange) world &lt;a class=&quot;headline-link&quot; href=&quot;https://web.dev/doodles-lem/#hello,-strange-world&quot;&gt;#&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;The Google homepage is a fascinating environment to code within. It comes
with many challenging restrictions: particular focus on speed and latency,
having to cater to all sorts of browsers and work under various
circumstances, and… yes, surprise and delight.&lt;/p&gt;
&lt;p&gt;I’m talking about &lt;a href=&quot;http://www.google.com/doodles/&quot; rel=&quot;noopener&quot;&gt;Google doodles&lt;/a&gt;, the special illustrations that occasionally replace our logo.
And while my relationship with pens and brushes has long had that
distinctive flavour of a restraining order, I often contribute to the
interactive ones.&lt;/p&gt;
&lt;p&gt;Every interactive doodle I coded (&lt;a href=&quot;http://www.google.com/doodles/30th-anniversary-of-pac-man&quot; rel=&quot;noopener&quot;&gt;Pac-Man&lt;/a&gt;, &lt;a href=&quot;http://www.google.com/doodles/jules-vernes-183rd-birthday&quot; rel=&quot;noopener&quot;&gt;Jules Verne&lt;/a&gt;, &lt;a href=&quot;http://www.google.com/doodles/160th-anniversary-of-the-first-worlds-fair&quot; rel=&quot;noopener&quot;&gt;World’s Fair&lt;/a&gt;)– and many I helped with – were in equal parts futuristic and anachronistic: great opportunities for pie-in-the-sky applications of cutting-edge Web features… and gritty pragmatism of cross-browser
compatibility.&lt;/p&gt;
&lt;p&gt;We learn a lot from each interactive doodle, and the recent Stanisław Lem mini-game was no exception, with its 17,000 lines of JavaScript code
trying many things for the first time in doodle history. Today, I want to share that code with you – perhaps you&#39;ll find something interesting
there, or point out my mistakes – and talk a bit about it.&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://code.google.com/p/stanislaw-lem-google-doodle/&quot; rel=&quot;noopener&quot;&gt;View Stanisław Lem doodle code »&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;A thing worth keeping in mind is that Google&#39;s homepage is not a place for
tech demos. With our doodles, we want to celebrate specific people and
events, and we want to do that using the best art and the best
technologies we can summon – but never celebrate technology for
technology&#39;s sake. This means carefully looking at whatever part of
broadly understood HTML5 is available, and whether it helps us make the
doodle better without distracting from it or overshadowing it.&lt;/p&gt;
&lt;p&gt;So, let&#39;s go through some of the modern Web technologies that found their
place – and some that didn&#39;t – in the Stanisław Lem doodle.&lt;/p&gt;
&lt;h2 id=&quot;graphics-via-dom-and-canvas&quot;&gt;Graphics via DOM and canvas &lt;a class=&quot;headline-link&quot; href=&quot;https://web.dev/doodles-lem/#graphics-via-dom-and-canvas&quot;&gt;#&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://developer.mozilla.org/HTML/Canvas&quot; rel=&quot;noopener&quot;&gt;Canvas&lt;/a&gt; is
powerful and created for exactly the kind of things we wanted to do in
this doodle. However, some of the older browsers we cared about didn&#39;t
support it – and even though I am literally sharing an office with the
person who put together an otherwise excellent &lt;a href=&quot;http://excanvas.sourceforge.net/&quot; rel=&quot;noopener&quot;&gt;excanvas&lt;/a&gt;, I decided to choose
a different way.&lt;/p&gt;
&lt;p&gt;I put together a
graphic engine that abstracts away
graphic primitives called “rects,” and then renders them using either
canvas, DOM if canvas is unavailable.&lt;/p&gt;
&lt;p&gt;This approach comes with some interesting challenges – for example, moving
or changing an object in DOM has immediate consequences, whereas
for canvas there’s a specific moment when everything’s drawn at the same
time. (I decided to have just one canvas, and clear it and draw from
scratch with every frame. Too many, literally, moving parts on one hand –
and on the other not enough complexity to warrant splitting into multiple
overlapping canvases and updating them selectively.)&lt;/p&gt;
&lt;p&gt;Unfortunately, switching to canvas is not as simple as just mirroring CSS
backgrounds with &lt;code&gt;drawImage()&lt;/code&gt;: you lose a number of things
that come for free when putting things together
via DOM – most importantly layering with z-indexes, and mouse events.&lt;/p&gt;
&lt;p&gt;I already abstracted away the z-index with a concept called “planes.” The
doodle defined a number of plane – from the sky far behind, to the mouse
pointer in front of everything – and every actor within the doodle had to
decide which one it belonged to (small plus/minus corrections within a
plane were possible by using &lt;code&gt;planeCorrection&lt;/code&gt;).&lt;/p&gt;
&lt;p&gt;Rendering through DOM, the planes are simply translated into z-index.
But if we render via canvas, we need to sort rects based on
their planes before drawing them. Since it’s
costly to do this every time, the order is recalculated only when an actor
is added or when it moves to another plane.&lt;/p&gt;
&lt;p&gt;For mouse events, I abstracted that too… sort of. For both DOM and canvas,
I used additional completely transparent floating DOM elements with high z-index, whose function is only to react to mouse over/out, clicks and
taps.&lt;/p&gt;
&lt;p&gt;One of the things we wanted to try with this doodle was breaking the
fourth wall. The above engine allowed us to combine canvas-based actors
with DOM-based actors. For example, the explosions in the finale are both
in canvas for in-universe objects and in DOM for the rest of the Google homepage. The bird, normally flying around and clipped by our
jagged mask like any other actor, decides to stay out of trouble during
the shooting level, and sits on the I’m Feeling Lucky button. The way it’s
done is for the bird to leave canvas and become a DOM
element (and vice versa later), which I hoped to be completely
transparent to our visitors.&lt;/p&gt;
&lt;h2 id=&quot;the-frame-rate&quot;&gt;The frame rate &lt;a class=&quot;headline-link&quot; href=&quot;https://web.dev/doodles-lem/#the-frame-rate&quot;&gt;#&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;Knowing the current frame rate, and reacting to when it&#39;s too slow (and
too fast!) was an important part of our engine. Since the browsers don&#39;t
report back the frame rate, we have to calculate it ourselves.&lt;/p&gt;
&lt;p&gt;I started with using &lt;a href=&quot;https://developer.mozilla.org/en/DOM/window.requestAnimationFrame&quot; rel=&quot;noopener&quot;&gt;requestAnimationFrame&lt;/a&gt;,
falling back to the
old-fashioned &lt;code&gt;setTimeout&lt;/code&gt; if the former was not available.
&lt;code&gt;requestAnimationFrame&lt;/code&gt; cleverly saves the CPU in
some situations – although we are doing some of that ourselves, as will be
explained below – but also simply allows us to get a higher frame rate
than &lt;code&gt;setTimeout&lt;/code&gt;.&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; Why a repeated &lt;code&gt;setTimeout&lt;/code&gt; and not simply &lt;code&gt;setInterval&lt;/code&gt;? We found out before that when the system is lagging, &lt;code&gt;setInterval&lt;/code&gt;s become slightly unpredictable and can hog the CPU when the browser tries to catch up. If you’re using &lt;code&gt;setTimeout&lt;/code&gt;, there’s only one timer event scheduled at any given time, so there is no danger of this happening. &lt;/div&gt;&lt;/aside&gt;
&lt;p&gt;Calculating the current frame rate is simple, but is
subject to drastic changes – for example it can drop quickly when another
application hogs the computer for a while. Therefore, we calculate a
“rolling” (averaged) frame rate only across every 100 physical ticks
and make decisions based on that.&lt;/p&gt;
&lt;p&gt;What kind of decisions?&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;If the frame rate is higher than 60fps, we throttle it.
Currently, &lt;code&gt;requestAnimationFrame&lt;/code&gt; on some versions of Firefox has no
upper cap on the frame rate, and there&#39;s no point in wasting the CPU.
Note that we actually cap at 65fps, because of the rounding errors that
make the frame rate just a bit higher than 60fps on other browsers – we
don&#39;t want do start throttling that by mistake.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;If the frame rate is lower than 10fps, we simply slow
down the engine instead of dropping frames.
It’s a lose-lose proposition, but I felt that skipping frames
excessively would be more confusing than simply having a slower (but still
coherent) game. There’s another nice side effect of that –
if the system gets slow temporarily, the user won’t experience a weird
jump ahead as the engine is desperately catching up. (I did it slightly
differently for Pac-Man, but the minimum frame rate is a better
approach.)&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Lastly, we can think of simplifying graphics when the frame rate gets
dangerously low. We&#39;re not doing it for Lem doodle with the exception of
mouse pointer (more on that below), but hypothetically we could lose some of the extraneous
animations just so that the doodle feels fluid even on slower computers.&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;We also have a concept of a physical tick and a logical tick. The former comes from
&lt;code&gt;requestAnimationFrame&lt;/code&gt;/&lt;code&gt;setTimeout&lt;/code&gt;. The ratio in
normal gameplay is 1:1, but for fast-forwarding, we just add more logical
ticks per a physical tick (up to 1:5). This allows us to do all the
necessary calculations for every logical tick, but only designate the last
one to be the one updating things on the screen.&lt;/p&gt;
&lt;h2 id=&quot;benchmarking&quot;&gt;Benchmarking &lt;a class=&quot;headline-link&quot; href=&quot;https://web.dev/doodles-lem/#benchmarking&quot;&gt;#&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;An assumption can be (and indeed, early on, was) made that canvas will be
faster than DOM whenever it&#39;s available. That is not always true. While
testing, we found out that Opera 10.0–10.1 on a Mac, and Firefox on Linux
are actually faster when moving DOM elements.&lt;/p&gt;
&lt;p&gt;In the perfect world, the doodle would silently benchmark different
graphic techniques – DOM elements moved using &lt;code&gt;style.left&lt;/code&gt;
and &lt;code&gt;style.top&lt;/code&gt;,
drawing on canvas, and maybe even DOM elements moved using CSS3 transforms&lt;/p&gt;
&lt;p&gt;– and then switch to whichever one gave the highest frame rate. I started
writing code for that, but found that at least my way of benchmarking was
pretty unreliable and required a lot of time. Time that we don&#39;t have on
our homepage – we care a lot about speed and we want the doodle to show up
instantly and the gameplay to begin as soon as you click or tap.&lt;/p&gt;
&lt;p&gt;In the end, Web development sometimes boils down to having to do what you
gotta do. I looked behind my shoulder to make sure no one was looking, and
then I just hard-coded Opera 10
and Firefox out of canvas. In the next life, I will come back as a
&lt;code&gt;&amp;lt;marquee&amp;gt;&lt;/code&gt; tag.&lt;/p&gt;
&lt;h2 id=&quot;conserving-cpu&quot;&gt;Conserving CPU &lt;a class=&quot;headline-link&quot; href=&quot;https://web.dev/doodles-lem/#conserving-cpu&quot;&gt;#&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;You know that friend who comes to your house, watches the season finale of
Breaking Bad, spoils it for you and then
deletes it from your DVR? You don&#39;t want to be that guy, do you?&lt;/p&gt;
&lt;p&gt;So, yes, the worst analogy ever. But we don&#39;t want our doodle to be that
guy either – the fact we&#39;re allowed into someone&#39;s browser tab is a
privilege, and hoarding CPU cycles or distracting the user
would make us an unpleasant guest. Therefore, if no one’s playing with the doodle
(no taps, mouse clicks, mouse movements, or key presses), we want it to
eventually go to sleep.&lt;/p&gt;
&lt;p&gt;When?&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;after 18 seconds on the homepage (arcade games called this &lt;a href=&quot;http://en.wikipedia.org/wiki/Attract_mode&quot; rel=&quot;noopener&quot;&gt;the attract mode&lt;/a&gt;)&lt;/li&gt;
&lt;li&gt;after 180 seconds if the tab has focus&lt;/li&gt;
&lt;li&gt;after 30 seconds if the tab doesn’t have focus (e.g. the user switched to another window, but perhaps is still watching the doodle now in an inactive tab)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;immediately&lt;/strong&gt; if the tab becomes invisible (e.g. the user switched to
another tab in the same window – no point in wasting cycles if we can&#39;t
be seen)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;How do we know the tab currently has focus? We attach ourselves to &lt;code&gt;window.focus&lt;/code&gt; and &lt;code&gt;window.blur&lt;/code&gt; How
do we know the tab is visible? We’re using the new &lt;a href=&quot;http://code.google.com/chrome/whitepapers/pagevisibility.html&quot; rel=&quot;noopener&quot;&gt;Page Visibility API&lt;/a&gt; and
react to the appropriate event.&lt;/p&gt;
&lt;p&gt;The time outs above are more forgiving than usual for us. I adapted them
to this particular doodle, which has a lot of ambient animations (chiefly the
sky and the bird). Ideally, the time outs would be gated on in-game
interaction – e.g. right after landing, the bird could report back to the
doodle that it can go to sleep now – but I didn&#39;t implement
that in the end.&lt;/p&gt;
&lt;p&gt;Since the sky is always in motion, when falling asleep and waking up the
doodle doesn’t just stop or start – it slows down before pausing, and vice
versa for resuming, increasing or decreasing the number logical ticks per a physical tick as necessary.&lt;/p&gt;
&lt;h2 id=&quot;transitions,-transforms,-events&quot;&gt;Transitions, transforms, events &lt;a class=&quot;headline-link&quot; href=&quot;https://web.dev/doodles-lem/#transitions,-transforms,-events&quot;&gt;#&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;One of the powers of HTML has always been the fact you can make it better
yourself: if something is not good enough in the regular portfolio of HTML
and CSS, you can wrangle JavaScript into extending it. Unfortunately, it
oftentimes means having to start from scratch. CSS3 transitions are great,
but you cannot add a new transition type or use transitions to do anything
else than styling elements. Another example: CSS3 transforms are great for
DOM, but when you move to canvas, you&#39;re suddenly on your own.&lt;/p&gt;
&lt;p&gt;These issues, and more, are why Lem doodle has its own transition and
transform engine. Yeah, I know, 2000s called, etc. – the capabilities I
built in are nowhere near as powerful as CSS3, but whatever the engine
does, it does consistently and gives us much more control.&lt;/p&gt;
&lt;p&gt;I started with a simple action
(event) system – a timeline that fires events in the future without
using &lt;code&gt;setTimeout&lt;/code&gt;, since at any given point doodle time can
become divorced from physical time as it gets faster (fast forward), slower
(low frame rate or falling asleep to save CPU), or stops altogether
(waiting for images to finish loading).&lt;/p&gt;
&lt;p&gt;Transitions are just another
type of actions. In addition to basic movements and rotation, we also
support relative movements (e.g. move something 10 pixels to the right),
custom things like shivering, and also keyframe image animations.&lt;/p&gt;
&lt;p&gt;I mentioned rotations, and those are done manually too: we have sprites
for various angles for the objects that need to be rotated.
The main reason is that both CSS3 and canvas
rotations were introducing visual artifacts that we found unacceptable –
and on top of that, those artifacts varied per platform.&lt;/p&gt;
&lt;p&gt;Given that some objects which rotate are attached to other rotating
objects – one example is a robot&#39;s hand connected to the lower arm, which
itself is attached to a rotating upper arm – this meant I also needed to
create a poor man’s transform-origin in form of pivots.&lt;/p&gt;
&lt;p&gt;All of this is a solid amount of work that ultimately covers the ground
already taken care of by HTML5 – but sometimes native support is not good
enough and it&#39;s that time for wheel reinvention.&lt;/p&gt;
&lt;h2 id=&quot;dealing-with-images-and-sprites&quot;&gt;Dealing with images and sprites &lt;a class=&quot;headline-link&quot; href=&quot;https://web.dev/doodles-lem/#dealing-with-images-and-sprites&quot;&gt;#&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;An engine is not just for running the doodle – it&#39;s also for working on
it. I shared some debug parameters above: you can find the rest in
&lt;code&gt;engine.readDebugParams&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;http://www.alistapart.com/articles/sprites&quot; rel=&quot;noopener&quot;&gt;Spriting&lt;/a&gt;
is a well-known technique that we too use for doodles. It allows us to
save bytes and decrease load times, plus it makes pre-loading easier.
However, it also makes development harder – every change to imagery would
require re-spriting (largely automated, but still cumbersome). Therefore,
the engine supports running on raw images for development as well as
sprites for production via &lt;code&gt;engine.useSprites&lt;/code&gt; – both are
included with the source code.&lt;/p&gt;
&lt;figure&gt;
&lt;img alt=&quot;Pac-Man doodle&quot; decoding=&quot;async&quot; height=&quot;280&quot; loading=&quot;lazy&quot; sizes=&quot;(min-width: 786px) 786px, calc(100vw - 48px)&quot; src=&quot;https://web-dev.imgix.net/image/T4FyVKpzu4WKF1kBNvXepbi08t52/3DQCbA7qIdPRtxP5KhYe.png?auto=format&quot; srcset=&quot;https://web-dev.imgix.net/image/T4FyVKpzu4WKF1kBNvXepbi08t52/3DQCbA7qIdPRtxP5KhYe.png?auto=format&amp;w=200 200w, https://web-dev.imgix.net/image/T4FyVKpzu4WKF1kBNvXepbi08t52/3DQCbA7qIdPRtxP5KhYe.png?auto=format&amp;w=228 228w, https://web-dev.imgix.net/image/T4FyVKpzu4WKF1kBNvXepbi08t52/3DQCbA7qIdPRtxP5KhYe.png?auto=format&amp;w=260 260w, https://web-dev.imgix.net/image/T4FyVKpzu4WKF1kBNvXepbi08t52/3DQCbA7qIdPRtxP5KhYe.png?auto=format&amp;w=296 296w, https://web-dev.imgix.net/image/T4FyVKpzu4WKF1kBNvXepbi08t52/3DQCbA7qIdPRtxP5KhYe.png?auto=format&amp;w=338 338w, https://web-dev.imgix.net/image/T4FyVKpzu4WKF1kBNvXepbi08t52/3DQCbA7qIdPRtxP5KhYe.png?auto=format&amp;w=385 385w, https://web-dev.imgix.net/image/T4FyVKpzu4WKF1kBNvXepbi08t52/3DQCbA7qIdPRtxP5KhYe.png?auto=format&amp;w=439 439w, https://web-dev.imgix.net/image/T4FyVKpzu4WKF1kBNvXepbi08t52/3DQCbA7qIdPRtxP5KhYe.png?auto=format&amp;w=500 500w, https://web-dev.imgix.net/image/T4FyVKpzu4WKF1kBNvXepbi08t52/3DQCbA7qIdPRtxP5KhYe.png?auto=format&amp;w=571 571w, https://web-dev.imgix.net/image/T4FyVKpzu4WKF1kBNvXepbi08t52/3DQCbA7qIdPRtxP5KhYe.png?auto=format&amp;w=650 650w, https://web-dev.imgix.net/image/T4FyVKpzu4WKF1kBNvXepbi08t52/3DQCbA7qIdPRtxP5KhYe.png?auto=format&amp;w=741 741w, https://web-dev.imgix.net/image/T4FyVKpzu4WKF1kBNvXepbi08t52/3DQCbA7qIdPRtxP5KhYe.png?auto=format&amp;w=845 845w, https://web-dev.imgix.net/image/T4FyVKpzu4WKF1kBNvXepbi08t52/3DQCbA7qIdPRtxP5KhYe.png?auto=format&amp;w=964 964w, https://web-dev.imgix.net/image/T4FyVKpzu4WKF1kBNvXepbi08t52/3DQCbA7qIdPRtxP5KhYe.png?auto=format&amp;w=1098 1098w, https://web-dev.imgix.net/image/T4FyVKpzu4WKF1kBNvXepbi08t52/3DQCbA7qIdPRtxP5KhYe.png?auto=format&amp;w=1252 1252w, https://web-dev.imgix.net/image/T4FyVKpzu4WKF1kBNvXepbi08t52/3DQCbA7qIdPRtxP5KhYe.png?auto=format&amp;w=1428 1428w, https://web-dev.imgix.net/image/T4FyVKpzu4WKF1kBNvXepbi08t52/3DQCbA7qIdPRtxP5KhYe.png?auto=format&amp;w=1572 1572w&quot; width=&quot;786&quot; /&gt;
&lt;figcaption&gt;
    Sprites used by the
    &lt;a href=&quot;http://www.google.com/pacman/&quot;&gt;Pac-Man doodle&lt;/a&gt;.
&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;p&gt;We also support pre-loading images
as we go along and halting the doodle if the images didn’t load in time
– complete with a faux progress bar! (Faux because, unfortunately, not even HTML5
can tell us how much of an image file has already been loaded.)&lt;/p&gt;
&lt;figure&gt;
&lt;img alt=&quot;A screenshot of loading graphic with the rigged progress bar.&quot; decoding=&quot;async&quot; height=&quot;604&quot; loading=&quot;lazy&quot; sizes=&quot;(min-width: 438px) 438px, calc(100vw - 48px)&quot; src=&quot;https://web-dev.imgix.net/image/T4FyVKpzu4WKF1kBNvXepbi08t52/xCxoSewh2txsVbYlaV7w.png?auto=format&quot; srcset=&quot;https://web-dev.imgix.net/image/T4FyVKpzu4WKF1kBNvXepbi08t52/xCxoSewh2txsVbYlaV7w.png?auto=format&amp;w=200 200w, https://web-dev.imgix.net/image/T4FyVKpzu4WKF1kBNvXepbi08t52/xCxoSewh2txsVbYlaV7w.png?auto=format&amp;w=228 228w, https://web-dev.imgix.net/image/T4FyVKpzu4WKF1kBNvXepbi08t52/xCxoSewh2txsVbYlaV7w.png?auto=format&amp;w=260 260w, https://web-dev.imgix.net/image/T4FyVKpzu4WKF1kBNvXepbi08t52/xCxoSewh2txsVbYlaV7w.png?auto=format&amp;w=296 296w, https://web-dev.imgix.net/image/T4FyVKpzu4WKF1kBNvXepbi08t52/xCxoSewh2txsVbYlaV7w.png?auto=format&amp;w=338 338w, https://web-dev.imgix.net/image/T4FyVKpzu4WKF1kBNvXepbi08t52/xCxoSewh2txsVbYlaV7w.png?auto=format&amp;w=385 385w, https://web-dev.imgix.net/image/T4FyVKpzu4WKF1kBNvXepbi08t52/xCxoSewh2txsVbYlaV7w.png?auto=format&amp;w=439 439w, https://web-dev.imgix.net/image/T4FyVKpzu4WKF1kBNvXepbi08t52/xCxoSewh2txsVbYlaV7w.png?auto=format&amp;w=500 500w, https://web-dev.imgix.net/image/T4FyVKpzu4WKF1kBNvXepbi08t52/xCxoSewh2txsVbYlaV7w.png?auto=format&amp;w=571 571w, https://web-dev.imgix.net/image/T4FyVKpzu4WKF1kBNvXepbi08t52/xCxoSewh2txsVbYlaV7w.png?auto=format&amp;w=650 650w, https://web-dev.imgix.net/image/T4FyVKpzu4WKF1kBNvXepbi08t52/xCxoSewh2txsVbYlaV7w.png?auto=format&amp;w=741 741w, https://web-dev.imgix.net/image/T4FyVKpzu4WKF1kBNvXepbi08t52/xCxoSewh2txsVbYlaV7w.png?auto=format&amp;w=845 845w, https://web-dev.imgix.net/image/T4FyVKpzu4WKF1kBNvXepbi08t52/xCxoSewh2txsVbYlaV7w.png?auto=format&amp;w=876 876w&quot; width=&quot;438&quot; /&gt;
&lt;figcaption&gt;
    A screenshot of loading graphic with the rigged progress bar.
&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;p&gt;For some scenes, we use more than one sprite not as much to speed up
loading using parallel connections, but simply because of &lt;a href=&quot;http://developer.apple.com/library/IOs/#documentation/AppleApplications/Reference/SafariWebContent/CreatingContentforSafarioniPhone/CreatingContentforSafarioniPhone.html#//apple_ref/doc/uid/TP40006482-SW15&quot; rel=&quot;noopener&quot;&gt;3/5 million pixel limitation for images on iOS&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;Where does HTML5 fit into all this? There&#39;s not much of it above, but the
tool I wrote for spriting/cropping was all new Web tech: canvas, &lt;a href=&quot;https://developer.mozilla.org//DOM/Blob&quot; rel=&quot;noopener&quot;&gt;blobs&lt;/a&gt;,
&lt;a href=&quot;http://updates.html5rocks.com/2011/08/Downloading-resources-in-HTML5-a-download&quot; rel=&quot;noopener&quot;&gt;a[download]&lt;/a&gt;.
One of the exciting things about HTML is that it
slowly subsumes things that previously had to be done outside of the
browser; the only part we needed do there was
optimizing PNG files.&lt;/p&gt;
&lt;h2 id=&quot;saving-state-in-between-games&quot;&gt;Saving state in between games &lt;a class=&quot;headline-link&quot; href=&quot;https://web.dev/doodles-lem/#saving-state-in-between-games&quot;&gt;#&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;Lem&#39;s worlds always felt big and alive and realistic. His stories
typically started without much in terms of explanation, the first page
starting in medias res, with the reader having to find her or his way
around.&lt;/p&gt;
&lt;p&gt;The Cyberiad was no exception and we wanted to replicate that feeling for
the doodle. We start with trying not to over-explain the story. Another
large part is randomization which we felt befitted the mechanic nature of
the universe of the book; we have a number of helper functions dealing with randomness
that we use in many, many places.&lt;/p&gt;
&lt;p&gt;We also wanted to increase replayability in other ways. For that, we
needed to know how many times the doodle was finished before. The
historically correct technological solution to that is a cookie, but that
doesn&#39;t work for Google homepage – every cookie increases every page&#39;s
payload, and again, we care quite a lot about speed and latency.&lt;/p&gt;
&lt;p&gt;Fortunately, HTML5 gives us &lt;a href=&quot;https://developer.mozilla.org/DOM/Storage&quot; rel=&quot;noopener&quot;&gt;Web Storage&lt;/a&gt;, trivial in use, allowing us to save and recall the
general play count and the last scene played by the user – with much more
grace than cookies would ever allow for.&lt;/p&gt;
&lt;p&gt;What do we do with this information?&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;we show a fast-forward button, allowing to zip through the cutscenes the user already saw before&lt;/li&gt;
&lt;li&gt;we show different N items during the finale&lt;/li&gt;
&lt;li&gt;we slightly increase the difficulty of the shooting level&lt;/li&gt;
&lt;li&gt;we show a little easter egg probability dragon from a different story on your third and subsequent plays&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;There is a number of debug parameters controlling this:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;?doodle-debug&amp;amp;doodle-first-run&lt;/code&gt; – pretend it’s a first run&lt;/li&gt;
&lt;li&gt;&lt;code&gt;?doodle-debug&amp;amp;doodle-second-run&lt;/code&gt; – pretend it’s a second run&lt;/li&gt;
&lt;li&gt;&lt;code&gt;?doodle-debug&amp;amp;doodle-old-run&lt;/code&gt; – pretend it’s an old run&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id=&quot;touch-devices&quot;&gt;Touch devices &lt;a class=&quot;headline-link&quot; href=&quot;https://web.dev/doodles-lem/#touch-devices&quot;&gt;#&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;We wanted the doodle to feel right at home on touch devices – the most
modern ones are powerful enough so that the doodle runs really well, and
experiencing the game via tapping is so
much more fun than with clicking.&lt;/p&gt;
&lt;p&gt;Some upfront changes to the user experience needed to be made. Originally, our
mouse pointer was the only place that communicated a
cutscene/non-interactive part is taking place. We later added a little
indicator in the lower-right corner, so we didn&#39;t have to rely on mouse
pointer alone (given that those don&#39;t exist on touch devices).&lt;/p&gt;
&lt;figure&gt;
&lt;table&gt;
    &lt;thead&gt;
        &lt;tr&gt;
            &lt;th&gt;&lt;/th&gt;
            &lt;th&gt;Normal&lt;/th&gt;
            &lt;th&gt;Busy&lt;/th&gt;
            &lt;th&gt;Clickable&lt;/th&gt;
            &lt;th&gt;Clicked&lt;/th&gt;
        &lt;/tr&gt;
    &lt;/thead&gt;&lt;tbody&gt;
        &lt;tr&gt;
            &lt;td&gt;Work-in-progress&lt;/td&gt;
            &lt;td&gt;&lt;figure&gt;&lt;img alt=&quot;Work-in-progress normal pointer&quot; decoding=&quot;async&quot; height=&quot;43&quot; loading=&quot;lazy&quot; sizes=&quot;(min-width: 32px) 32px, calc(100vw - 48px)&quot; src=&quot;https://web-dev.imgix.net/image/T4FyVKpzu4WKF1kBNvXepbi08t52/hZgE7oLpWwvEfIkWPqb0.png?auto=format&quot; srcset=&quot;https://web-dev.imgix.net/image/T4FyVKpzu4WKF1kBNvXepbi08t52/hZgE7oLpWwvEfIkWPqb0.png?auto=format&amp;w=32 32w, https://web-dev.imgix.net/image/T4FyVKpzu4WKF1kBNvXepbi08t52/hZgE7oLpWwvEfIkWPqb0.png?auto=format&amp;w=36 36w, https://web-dev.imgix.net/image/T4FyVKpzu4WKF1kBNvXepbi08t52/hZgE7oLpWwvEfIkWPqb0.png?auto=format&amp;w=42 42w, https://web-dev.imgix.net/image/T4FyVKpzu4WKF1kBNvXepbi08t52/hZgE7oLpWwvEfIkWPqb0.png?auto=format&amp;w=47 47w, https://web-dev.imgix.net/image/T4FyVKpzu4WKF1kBNvXepbi08t52/hZgE7oLpWwvEfIkWPqb0.png?auto=format&amp;w=54 54w, https://web-dev.imgix.net/image/T4FyVKpzu4WKF1kBNvXepbi08t52/hZgE7oLpWwvEfIkWPqb0.png?auto=format&amp;w=62 62w, https://web-dev.imgix.net/image/T4FyVKpzu4WKF1kBNvXepbi08t52/hZgE7oLpWwvEfIkWPqb0.png?auto=format&amp;w=64 64w&quot; width=&quot;32&quot; /&gt;&lt;/figure&gt;&lt;/td&gt;
            &lt;td&gt;&lt;figure&gt;&lt;img alt=&quot;Work-in-progress busy pointer&quot; decoding=&quot;async&quot; height=&quot;42&quot; loading=&quot;lazy&quot; sizes=&quot;(min-width: 32px) 32px, calc(100vw - 48px)&quot; src=&quot;https://web-dev.imgix.net/image/T4FyVKpzu4WKF1kBNvXepbi08t52/tws1jNd1hvUjjqOWa1zC.png?auto=format&quot; srcset=&quot;https://web-dev.imgix.net/image/T4FyVKpzu4WKF1kBNvXepbi08t52/tws1jNd1hvUjjqOWa1zC.png?auto=format&amp;w=32 32w, https://web-dev.imgix.net/image/T4FyVKpzu4WKF1kBNvXepbi08t52/tws1jNd1hvUjjqOWa1zC.png?auto=format&amp;w=36 36w, https://web-dev.imgix.net/image/T4FyVKpzu4WKF1kBNvXepbi08t52/tws1jNd1hvUjjqOWa1zC.png?auto=format&amp;w=42 42w, https://web-dev.imgix.net/image/T4FyVKpzu4WKF1kBNvXepbi08t52/tws1jNd1hvUjjqOWa1zC.png?auto=format&amp;w=47 47w, https://web-dev.imgix.net/image/T4FyVKpzu4WKF1kBNvXepbi08t52/tws1jNd1hvUjjqOWa1zC.png?auto=format&amp;w=54 54w, https://web-dev.imgix.net/image/T4FyVKpzu4WKF1kBNvXepbi08t52/tws1jNd1hvUjjqOWa1zC.png?auto=format&amp;w=62 62w, https://web-dev.imgix.net/image/T4FyVKpzu4WKF1kBNvXepbi08t52/tws1jNd1hvUjjqOWa1zC.png?auto=format&amp;w=64 64w&quot; width=&quot;32&quot; /&gt;&lt;/figure&gt;&lt;/td&gt;
            &lt;td&gt;&lt;figure&gt;&lt;img alt=&quot;Work-in-progress clickable pointer&quot; decoding=&quot;async&quot; height=&quot;43&quot; loading=&quot;lazy&quot; sizes=&quot;(min-width: 32px) 32px, calc(100vw - 48px)&quot; src=&quot;https://web-dev.imgix.net/image/T4FyVKpzu4WKF1kBNvXepbi08t52/Yha8mb7Q31upUtHRhskW.png?auto=format&quot; srcset=&quot;https://web-dev.imgix.net/image/T4FyVKpzu4WKF1kBNvXepbi08t52/Yha8mb7Q31upUtHRhskW.png?auto=format&amp;w=32 32w, https://web-dev.imgix.net/image/T4FyVKpzu4WKF1kBNvXepbi08t52/Yha8mb7Q31upUtHRhskW.png?auto=format&amp;w=36 36w, https://web-dev.imgix.net/image/T4FyVKpzu4WKF1kBNvXepbi08t52/Yha8mb7Q31upUtHRhskW.png?auto=format&amp;w=42 42w, https://web-dev.imgix.net/image/T4FyVKpzu4WKF1kBNvXepbi08t52/Yha8mb7Q31upUtHRhskW.png?auto=format&amp;w=47 47w, https://web-dev.imgix.net/image/T4FyVKpzu4WKF1kBNvXepbi08t52/Yha8mb7Q31upUtHRhskW.png?auto=format&amp;w=54 54w, https://web-dev.imgix.net/image/T4FyVKpzu4WKF1kBNvXepbi08t52/Yha8mb7Q31upUtHRhskW.png?auto=format&amp;w=62 62w, https://web-dev.imgix.net/image/T4FyVKpzu4WKF1kBNvXepbi08t52/Yha8mb7Q31upUtHRhskW.png?auto=format&amp;w=64 64w&quot; width=&quot;32&quot; /&gt;&lt;/figure&gt;&lt;/td&gt;
            &lt;td&gt;&lt;figure&gt;&lt;img alt=&quot;Work-in-progress clicked pointer&quot; decoding=&quot;async&quot; height=&quot;43&quot; loading=&quot;lazy&quot; sizes=&quot;(min-width: 32px) 32px, calc(100vw - 48px)&quot; src=&quot;https://web-dev.imgix.net/image/T4FyVKpzu4WKF1kBNvXepbi08t52/1VWNZcqj8ApJQOhQZiAP.png?auto=format&quot; srcset=&quot;https://web-dev.imgix.net/image/T4FyVKpzu4WKF1kBNvXepbi08t52/1VWNZcqj8ApJQOhQZiAP.png?auto=format&amp;w=32 32w, https://web-dev.imgix.net/image/T4FyVKpzu4WKF1kBNvXepbi08t52/1VWNZcqj8ApJQOhQZiAP.png?auto=format&amp;w=36 36w, https://web-dev.imgix.net/image/T4FyVKpzu4WKF1kBNvXepbi08t52/1VWNZcqj8ApJQOhQZiAP.png?auto=format&amp;w=42 42w, https://web-dev.imgix.net/image/T4FyVKpzu4WKF1kBNvXepbi08t52/1VWNZcqj8ApJQOhQZiAP.png?auto=format&amp;w=47 47w, https://web-dev.imgix.net/image/T4FyVKpzu4WKF1kBNvXepbi08t52/1VWNZcqj8ApJQOhQZiAP.png?auto=format&amp;w=54 54w, https://web-dev.imgix.net/image/T4FyVKpzu4WKF1kBNvXepbi08t52/1VWNZcqj8ApJQOhQZiAP.png?auto=format&amp;w=62 62w, https://web-dev.imgix.net/image/T4FyVKpzu4WKF1kBNvXepbi08t52/1VWNZcqj8ApJQOhQZiAP.png?auto=format&amp;w=64 64w&quot; width=&quot;32&quot; /&gt;&lt;/figure&gt;&lt;/td&gt;
        &lt;/tr&gt;
        &lt;tr&gt;
            &lt;td&gt;Final&lt;/td&gt;
            &lt;td&gt;&lt;figure&gt;&lt;img alt=&quot;Final normal pointer&quot; decoding=&quot;async&quot; height=&quot;36&quot; loading=&quot;lazy&quot; sizes=&quot;(min-width: 32px) 32px, calc(100vw - 48px)&quot; src=&quot;https://web-dev.imgix.net/image/T4FyVKpzu4WKF1kBNvXepbi08t52/Vq3EgAkFzGls8fjlKZUr.png?auto=format&quot; srcset=&quot;https://web-dev.imgix.net/image/T4FyVKpzu4WKF1kBNvXepbi08t52/Vq3EgAkFzGls8fjlKZUr.png?auto=format&amp;w=32 32w, https://web-dev.imgix.net/image/T4FyVKpzu4WKF1kBNvXepbi08t52/Vq3EgAkFzGls8fjlKZUr.png?auto=format&amp;w=36 36w, https://web-dev.imgix.net/image/T4FyVKpzu4WKF1kBNvXepbi08t52/Vq3EgAkFzGls8fjlKZUr.png?auto=format&amp;w=42 42w, https://web-dev.imgix.net/image/T4FyVKpzu4WKF1kBNvXepbi08t52/Vq3EgAkFzGls8fjlKZUr.png?auto=format&amp;w=47 47w, https://web-dev.imgix.net/image/T4FyVKpzu4WKF1kBNvXepbi08t52/Vq3EgAkFzGls8fjlKZUr.png?auto=format&amp;w=54 54w, https://web-dev.imgix.net/image/T4FyVKpzu4WKF1kBNvXepbi08t52/Vq3EgAkFzGls8fjlKZUr.png?auto=format&amp;w=62 62w, https://web-dev.imgix.net/image/T4FyVKpzu4WKF1kBNvXepbi08t52/Vq3EgAkFzGls8fjlKZUr.png?auto=format&amp;w=64 64w&quot; width=&quot;32&quot; /&gt;v&lt;/figure&gt;&lt;/td&gt;
            &lt;td&gt;&lt;figure&gt;&lt;img alt=&quot;Final busy pointer&quot; decoding=&quot;async&quot; height=&quot;37&quot; loading=&quot;lazy&quot; sizes=&quot;(min-width: 32px) 32px, calc(100vw - 48px)&quot; src=&quot;https://web-dev.imgix.net/image/T4FyVKpzu4WKF1kBNvXepbi08t52/5MKryf69dmSOEUQEY57x.png?auto=format&quot; srcset=&quot;https://web-dev.imgix.net/image/T4FyVKpzu4WKF1kBNvXepbi08t52/5MKryf69dmSOEUQEY57x.png?auto=format&amp;w=32 32w, https://web-dev.imgix.net/image/T4FyVKpzu4WKF1kBNvXepbi08t52/5MKryf69dmSOEUQEY57x.png?auto=format&amp;w=36 36w, https://web-dev.imgix.net/image/T4FyVKpzu4WKF1kBNvXepbi08t52/5MKryf69dmSOEUQEY57x.png?auto=format&amp;w=42 42w, https://web-dev.imgix.net/image/T4FyVKpzu4WKF1kBNvXepbi08t52/5MKryf69dmSOEUQEY57x.png?auto=format&amp;w=47 47w, https://web-dev.imgix.net/image/T4FyVKpzu4WKF1kBNvXepbi08t52/5MKryf69dmSOEUQEY57x.png?auto=format&amp;w=54 54w, https://web-dev.imgix.net/image/T4FyVKpzu4WKF1kBNvXepbi08t52/5MKryf69dmSOEUQEY57x.png?auto=format&amp;w=62 62w, https://web-dev.imgix.net/image/T4FyVKpzu4WKF1kBNvXepbi08t52/5MKryf69dmSOEUQEY57x.png?auto=format&amp;w=64 64w&quot; width=&quot;32&quot; /&gt;&lt;/figure&gt;&lt;/td&gt;
            &lt;td&gt;&lt;figure&gt;&lt;img alt=&quot;Final clickable pointer&quot; decoding=&quot;async&quot; height=&quot;43&quot; loading=&quot;lazy&quot; sizes=&quot;(min-width: 32px) 32px, calc(100vw - 48px)&quot; src=&quot;https://web-dev.imgix.net/image/T4FyVKpzu4WKF1kBNvXepbi08t52/cdxKyJoAQJvVBnqq8vwj.png?auto=format&quot; srcset=&quot;https://web-dev.imgix.net/image/T4FyVKpzu4WKF1kBNvXepbi08t52/cdxKyJoAQJvVBnqq8vwj.png?auto=format&amp;w=32 32w, https://web-dev.imgix.net/image/T4FyVKpzu4WKF1kBNvXepbi08t52/cdxKyJoAQJvVBnqq8vwj.png?auto=format&amp;w=36 36w, https://web-dev.imgix.net/image/T4FyVKpzu4WKF1kBNvXepbi08t52/cdxKyJoAQJvVBnqq8vwj.png?auto=format&amp;w=42 42w, https://web-dev.imgix.net/image/T4FyVKpzu4WKF1kBNvXepbi08t52/cdxKyJoAQJvVBnqq8vwj.png?auto=format&amp;w=47 47w, https://web-dev.imgix.net/image/T4FyVKpzu4WKF1kBNvXepbi08t52/cdxKyJoAQJvVBnqq8vwj.png?auto=format&amp;w=54 54w, https://web-dev.imgix.net/image/T4FyVKpzu4WKF1kBNvXepbi08t52/cdxKyJoAQJvVBnqq8vwj.png?auto=format&amp;w=62 62w, https://web-dev.imgix.net/image/T4FyVKpzu4WKF1kBNvXepbi08t52/cdxKyJoAQJvVBnqq8vwj.png?auto=format&amp;w=64 64w&quot; width=&quot;32&quot; /&gt;&lt;/figure&gt;&lt;/td&gt;
            &lt;td&gt;&lt;figure&gt;&lt;img alt=&quot;Final clicked pointer&quot; decoding=&quot;async&quot; height=&quot;43&quot; loading=&quot;lazy&quot; sizes=&quot;(min-width: 32px) 32px, calc(100vw - 48px)&quot; src=&quot;https://web-dev.imgix.net/image/T4FyVKpzu4WKF1kBNvXepbi08t52/7fOZa4xdr234Z0CAZlK8.png?auto=format&quot; srcset=&quot;https://web-dev.imgix.net/image/T4FyVKpzu4WKF1kBNvXepbi08t52/7fOZa4xdr234Z0CAZlK8.png?auto=format&amp;w=32 32w, https://web-dev.imgix.net/image/T4FyVKpzu4WKF1kBNvXepbi08t52/7fOZa4xdr234Z0CAZlK8.png?auto=format&amp;w=36 36w, https://web-dev.imgix.net/image/T4FyVKpzu4WKF1kBNvXepbi08t52/7fOZa4xdr234Z0CAZlK8.png?auto=format&amp;w=42 42w, https://web-dev.imgix.net/image/T4FyVKpzu4WKF1kBNvXepbi08t52/7fOZa4xdr234Z0CAZlK8.png?auto=format&amp;w=47 47w, https://web-dev.imgix.net/image/T4FyVKpzu4WKF1kBNvXepbi08t52/7fOZa4xdr234Z0CAZlK8.png?auto=format&amp;w=54 54w, https://web-dev.imgix.net/image/T4FyVKpzu4WKF1kBNvXepbi08t52/7fOZa4xdr234Z0CAZlK8.png?auto=format&amp;w=62 62w, https://web-dev.imgix.net/image/T4FyVKpzu4WKF1kBNvXepbi08t52/7fOZa4xdr234Z0CAZlK8.png?auto=format&amp;w=64 64w&quot; width=&quot;32&quot; /&gt;&lt;/figure&gt;&lt;/td&gt;
        &lt;/tr&gt;
    &lt;/tbody&gt;
&lt;/table&gt;
&lt;figcaption&gt;
    Mouse pointers during development, and final equivalents.
&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;p&gt;Most of the stuff worked out of the box. However, quick impromptu
usability tests of our touch experience showed two problems: some of the
targets were too hard to press, and quick taps were ignored since we just
overrode mouse click events.&lt;/p&gt;
&lt;p&gt;Having separate clickable transparent DOM elements helped a lot here, as I
could resize them independently of the visuals. I introduced extra
15-pixel padding for touch devices and used it whenever clickable elements were created.
(I added 5-pixel padding for mouse environments too, just
to make Mr. Fitts happy.)&lt;/p&gt;
&lt;p&gt;As for the other problem, I just made sure to attach and test proper touch
start and end handlers, instead of relying on mouse click.&lt;/p&gt;
&lt;p&gt;We’re also using more modern style properties to remove some touch
features that WebKit browsers add by default (tap highlight, tap callout).&lt;/p&gt;
&lt;p&gt;And how do we detect whether a given device running the doodle supports
touch? Lazily. Instead of figuring it out a
priori, we just used our combined IQs to deduct that the device
supports touch… after we get the first touch start event.&lt;/p&gt;
&lt;h2 id=&quot;customizing-the-mouse-pointer&quot;&gt;Customizing the mouse pointer &lt;a class=&quot;headline-link&quot; href=&quot;https://web.dev/doodles-lem/#customizing-the-mouse-pointer&quot;&gt;#&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;But not everything is touch-based. One of our guiding principles was to
put as many things as we could within the universe of the doodle. The
little sidebar UI (fast-forward, question mark), the tooltip, and even,
yes, the mouse pointer.&lt;/p&gt;
&lt;p&gt;How to customize a mouse pointer? Some browsers allow changing the
mouse cursor by linking to a bespoke image file. However, this is not
supported well and it’s also somewhat restrictive.&lt;/p&gt;
&lt;p&gt;If not this, then what? Well, why not making a mouse pointer just another
actor in the doodle? This works, but comes with a number of
caveats, chiefly:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;you need to be able to remove the native mouse pointer&lt;/li&gt;
&lt;li&gt;you need to be pretty good at keeping your mouse pointer in sync with
the “real” one&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The former is tricky. CSS3 allows for &lt;code&gt;cursor: none&lt;/code&gt;, but it
too is not supported in some browsers. We needed to resort to some
gymnastics: using empty &lt;code&gt;.cur&lt;/code&gt;
file as a fallback, specifying concrete
behavior for some browsers, and even hard-coding others out of the
experience whatsoever.&lt;/p&gt;
&lt;p&gt;The other is relatively trivial on its face, but
with the mouse pointer being just another part of the universe of the
doodle, it will inherit all its problems too. The biggest one? If the
frame rate of the doodle is low, the frame rate of the mouse pointer will
be low too – and that has dire consequences since the mouse pointer, being
a natural extension of your hand, needs to feel responsive no matter what.
(People who used Commodore Amiga in their past are now nodding
vigorously.)&lt;/p&gt;
&lt;p&gt;One somewhat complex solution to that problem is decoupling the mouse
pointer from the regular update loop. We did just that – in an alternate
universe where I don&#39;t need to sleep. A simpler solution for this one?
Just reverting to the native mouse pointer
if the rolling frame rate drops
below 20fps. (This is where the rolling
frame rate comes in handy. If we reacted to the current frame rate, and if
it happened to oscillate around 20fps, the user would see the custom mouse
pointer hiding and showing all the time.) This brings us to:&lt;/p&gt;
&lt;figure&gt;
&lt;table&gt;
    &lt;thead&gt;
        &lt;tr&gt;
            &lt;th&gt;Frame rate range&lt;/th&gt;
            &lt;th&gt;Behaviour&lt;/th&gt;
        &lt;/tr&gt;
    &lt;/thead&gt;&lt;tbody&gt;
        &lt;tr&gt;
            &lt;td&gt; &gt;10fps&lt;/td&gt;
            &lt;td&gt; Slow down the game so that more frames are not dropped.&lt;/td&gt;
        &lt;/tr&gt;
        &lt;tr&gt;
            &lt;td&gt;10–20fps&lt;/td&gt;
            &lt;td&gt;Use native mouse pointer instead of custom one.&lt;/td&gt;
        &lt;/tr&gt;
        &lt;tr&gt;
            &lt;td&gt;20–60fps&lt;/td&gt;
            &lt;td&gt;Normal operation.&lt;/td&gt;
        &lt;/tr&gt;
        &lt;tr&gt;
            &lt;td&gt;&gt;60fps&lt;/td&gt;
            &lt;td&gt;Throttle so that the frame rate doesn&#39;t exceed this value.&lt;/td&gt;
        &lt;/tr&gt;
    &lt;/tbody&gt;
&lt;/table&gt;
&lt;figcaption&gt;
    Summary of frame rate-dependent behaviour.
&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;p&gt;Oh, and our mouse pointer is dark on a Mac, but white on a PC. Why? Because platform wars need fuel even in
fictional universes.&lt;/p&gt;
&lt;h2 id=&quot;conclusion&quot;&gt;Conclusion &lt;a class=&quot;headline-link&quot; href=&quot;https://web.dev/doodles-lem/#conclusion&quot;&gt;#&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;This is not a perfect engine, but it doesn&#39;t try to be one. It was
developed alongside the Lem doodle, and is very specific to it. That&#39;s
okay. “Premature optimization is the root of all evil,” as Don Knuth
famously said, and I don&#39;t believe writing an engine in isolation first,
and only applying it later makes sense – the practice informs theory just
as much as theory informs practice. In my case, code was thrown away,
several parts rewritten over and over again, and many common pieces
noticed post, rather than ante factum. But in the end, what we have here
allowed us to do what we wanted – celebrate the career of Stanisław Lem
and the drawings by Daniel Mróz in the best way we could think of.&lt;/p&gt;
&lt;p&gt;I hope the above sheds light on some of the design choices and trade-offs
that we needed to make – and how we used HTML5 in a specific, real-life
scenario. Now, play with the source code, take it for the spin, and let us
know what you think.&lt;/p&gt;
&lt;p&gt;I did that myself – this below was live in the last days, counting down to
the early hours of the 23th of November 2011 in Russia, which was the
first time zone that saw the Lem doodle. A goofy thing, perhaps, but just
like doodles, things that appear insignificant sometimes have a deeper
meaning – this counter was really a nice “stress test” for the engine.&lt;/p&gt;
&lt;figure&gt;
&lt;img alt=&quot;A screenshot of Lem doodle in-universe countdown clock.&quot; decoding=&quot;async&quot; height=&quot;300&quot; loading=&quot;lazy&quot; sizes=&quot;(min-width: 800px) 800px, calc(100vw - 48px)&quot; src=&quot;https://web-dev.imgix.net/image/T4FyVKpzu4WKF1kBNvXepbi08t52/OBjGBgmdt7UmytyNhTOp.png?auto=format&quot; srcset=&quot;https://web-dev.imgix.net/image/T4FyVKpzu4WKF1kBNvXepbi08t52/OBjGBgmdt7UmytyNhTOp.png?auto=format&amp;w=200 200w, https://web-dev.imgix.net/image/T4FyVKpzu4WKF1kBNvXepbi08t52/OBjGBgmdt7UmytyNhTOp.png?auto=format&amp;w=228 228w, https://web-dev.imgix.net/image/T4FyVKpzu4WKF1kBNvXepbi08t52/OBjGBgmdt7UmytyNhTOp.png?auto=format&amp;w=260 260w, https://web-dev.imgix.net/image/T4FyVKpzu4WKF1kBNvXepbi08t52/OBjGBgmdt7UmytyNhTOp.png?auto=format&amp;w=296 296w, https://web-dev.imgix.net/image/T4FyVKpzu4WKF1kBNvXepbi08t52/OBjGBgmdt7UmytyNhTOp.png?auto=format&amp;w=338 338w, https://web-dev.imgix.net/image/T4FyVKpzu4WKF1kBNvXepbi08t52/OBjGBgmdt7UmytyNhTOp.png?auto=format&amp;w=385 385w, https://web-dev.imgix.net/image/T4FyVKpzu4WKF1kBNvXepbi08t52/OBjGBgmdt7UmytyNhTOp.png?auto=format&amp;w=439 439w, https://web-dev.imgix.net/image/T4FyVKpzu4WKF1kBNvXepbi08t52/OBjGBgmdt7UmytyNhTOp.png?auto=format&amp;w=500 500w, https://web-dev.imgix.net/image/T4FyVKpzu4WKF1kBNvXepbi08t52/OBjGBgmdt7UmytyNhTOp.png?auto=format&amp;w=571 571w, https://web-dev.imgix.net/image/T4FyVKpzu4WKF1kBNvXepbi08t52/OBjGBgmdt7UmytyNhTOp.png?auto=format&amp;w=650 650w, https://web-dev.imgix.net/image/T4FyVKpzu4WKF1kBNvXepbi08t52/OBjGBgmdt7UmytyNhTOp.png?auto=format&amp;w=741 741w, https://web-dev.imgix.net/image/T4FyVKpzu4WKF1kBNvXepbi08t52/OBjGBgmdt7UmytyNhTOp.png?auto=format&amp;w=845 845w, https://web-dev.imgix.net/image/T4FyVKpzu4WKF1kBNvXepbi08t52/OBjGBgmdt7UmytyNhTOp.png?auto=format&amp;w=964 964w, https://web-dev.imgix.net/image/T4FyVKpzu4WKF1kBNvXepbi08t52/OBjGBgmdt7UmytyNhTOp.png?auto=format&amp;w=1098 1098w, https://web-dev.imgix.net/image/T4FyVKpzu4WKF1kBNvXepbi08t52/OBjGBgmdt7UmytyNhTOp.png?auto=format&amp;w=1252 1252w, https://web-dev.imgix.net/image/T4FyVKpzu4WKF1kBNvXepbi08t52/OBjGBgmdt7UmytyNhTOp.png?auto=format&amp;w=1428 1428w, https://web-dev.imgix.net/image/T4FyVKpzu4WKF1kBNvXepbi08t52/OBjGBgmdt7UmytyNhTOp.png?auto=format&amp;w=1600 1600w&quot; width=&quot;800&quot; /&gt;
&lt;figcaption&gt;
    A screenshot of Lem doodle in-universe countdown clock.
&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;p&gt;And that&#39;s one way of looking at the life of a Google doodle – months of
work, weeks of testing, 48 hours of baking it in, all for something that
people play for five minutes. Every one of those thousands of JavaScript
lines is hoping that those 5 minutes will be time well spent. Enjoy.&lt;/p&gt;
</content>
    <author>
      <name>Marcin Wichary</name>
    </author>
  </entry>
</feed>
