Hello, (strange) world #
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.
I’m talking about Google doodles, 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.
Every interactive doodle I coded (Pac-Man, Jules Verne, World’s Fair)– 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.
A thing worth keeping in mind is that Google'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'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.
So, let's go through some of the modern Web technologies that found their place – and some that didn't – in the Stanisław Lem doodle.
Graphics via DOM and canvas #
Canvas 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't support it – and even though I am literally sharing an office with the person who put together an otherwise excellent excanvas, I decided to choose a different way.
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.
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.)
Unfortunately, switching to canvas is not as simple as just mirroring CSS backgrounds with
drawImage(): 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.
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
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.
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.
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.
The frame rate #
Knowing the current frame rate, and reacting to when it's too slow (and too fast!) was an important part of our engine. Since the browsers don't report back the frame rate, we have to calculate it ourselves.
I started with using requestAnimationFrame, falling back to the old-fashioned
setTimeout if the former was not available.
requestAnimationFrame 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
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.
What kind of decisions?
If the frame rate is higher than 60fps, we throttle it. Currently,
requestAnimationFrameon some versions of Firefox has no upper cap on the frame rate, and there'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't want do start throttling that by mistake.
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.)
Lastly, we can think of simplifying graphics when the frame rate gets dangerously low. We'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.
We also have a concept of a physical tick and a logical tick. The former comes from
setTimeout. 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.
An assumption can be (and indeed, early on, was) made that canvas will be faster than DOM whenever it'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.
In the perfect world, the doodle would silently benchmark different graphic techniques – DOM elements moved using
style.top, drawing on canvas, and maybe even DOM elements moved using CSS3 transforms
– 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'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.
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
Conserving CPU #
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't want to be that guy, do you?
So, yes, the worst analogy ever. But we don't want our doodle to be that guy either – the fact we're allowed into someone'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.
- after 18 seconds on the homepage (arcade games called this the attract mode)
- after 180 seconds if the tab has focus
- 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)
- immediately 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't be seen)
How do we know the tab currently has focus? We attach ourselves to
window.blur How do we know the tab is visible? We’re using the new Page Visibility API and react to the appropriate event.
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't implement that in the end.
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.
Transitions, transforms, events #
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.
I started with a simple action (event) system – a timeline that fires events in the future without using
setTimeout, 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).
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.
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.
Given that some objects which rotate are attached to other rotating objects – one example is a robot'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.
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's that time for wheel reinvention.
Dealing with images and sprites #
An engine is not just for running the doodle – it's also for working on it. I shared some debug parameters above: you can find the rest in
Spriting 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
engine.useSprites – both are included with the source code.
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.)
For some scenes, we use more than one sprite not as much to speed up loading using parallel connections, but simply because of 3/5 million pixel limitation for images on iOS.
Where does HTML5 fit into all this? There's not much of it above, but the tool I wrote for spriting/cropping was all new Web tech: canvas, blobs, a[download]. 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.
Saving state in between games #
Lem'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.
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.
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't work for Google homepage – every cookie increases every page's payload, and again, we care quite a lot about speed and latency.
Fortunately, HTML5 gives us Web Storage, 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.
What do we do with this information?
- we show a fast-forward button, allowing to zip through the cutscenes the user already saw before
- we show different N items during the finale
- we slightly increase the difficulty of the shooting level
- we show a little easter egg probability dragon from a different story on your third and subsequent plays
There is a number of debug parameters controlling this:
?doodle-debug&doodle-first-run– pretend it’s a first run
?doodle-debug&doodle-second-run– pretend it’s a second run
?doodle-debug&doodle-old-run– pretend it’s an old run
Touch devices #
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.
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't have to rely on mouse pointer alone (given that those don't exist on touch devices).
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.
This is not a perfect engine, but it doesn't try to be one. It was developed alongside the Lem doodle, and is very specific to it. That's okay. “Premature optimization is the root of all evil,” as Don Knuth famously said, and I don'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.
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.
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.
Last updated— Improve article