Recently, Photon Storm has been commissioned to create a couple of games in which the player ‘looks through’ an image, revealing a hidden scene below. This feature works when the player moves the mouse or a finger on a touchscreen, guiding a visual aid that allows them to peer through one image into another.
This is perhaps easier to understand from the screen shot below:
(Game: Old Jack’s Boat by Photon Storm for the BBC / CBeebies)
In the screen shot, you can see the circular ‘porthole’ object. As you move it around, you can see into a scene in which various fish are all swimming past, and you must search for the one displayed in the top left bubble. It’s a simple game aimed at young children, but it posed an interesting technical challenge for us: How do you render an animated scene into a non-rectangular viewport?
We came up with two methods, each with their own strengths and weaknesses.
The first approach is to use a method of the Canvas context called clip. This works by defining a path, drawing a shape to it and then calling clip. As a result, anything drawn to the canvas after this point is automatically clipped to the shape you defined until you next restore the context. The code flow looks like this:
context.save(); context.beginPath(); context.arc(); context.clip(); // draw the hidden image / objects context.restore(); // draw anything that needs to appear on-top (like game UI)
First start by saving the context state. The call to arc requires an x/y coordinate, a radius and a starting and ending angle. In the case of our game we used the mouse/touch coordinate as the centre point of our circle and a radius of 90 pixels which matched the dimensions of our porthole graphic. If you specify the start angle as 0 and the end angle as Math.PI * 2 then you get a complete circle drawn:
context.arc(x, y, 90, 0, Math.PI * 2, false);
Once your shape is complete, you call context.clip. From that point on, anything that is drawn to the canvas is clipped to be within the region of the shape you specified. Obviously, in our game, we just needed a simple circle, but you could theoretically create any shape you need using the canvas path primitives.
Rendering the hidden scene
After creating the shape and calling clip, you’re now free to render the ‘hidden’ scene to the canvas.
A nice trick to employ at this stage is to offset the x/y coordinates of the hidden objects ever so slightly. If you are using a glass object (such as a magnifying glass) as your viewport into the world, then the slight rendering offset gives the objects a sense of distortion, as if being refracted through glass.
Once the hidden scene is complete, you should restore the canvas context and carry on rendering anything that needs to appear on the top layer, such as game UI.
Pros and Cons
As with most things in HTML5, there are pros and cons to this approach. First, it is limited to only shapes that can be constructed via the context path tools, and the more complex the shape, the slower rendering becomes on mobile browsers.
Through testing, we found that where devices are GPU accelerated, clip() can sometimes cause rendering issues, or just flat-out not work (such as on the Kindle Fire HD). Of course, you should test for yourself as building HTML5 games is a constantly evolving process. What may (or may not) work one month, could have drastically changed by the next.
You can play our game Old Jack’s Boat on the Cbeebies web site to see the end result in action. Note: you will need to visit the site using a mobile web browser.
clip() is extremely easy to implement, and well supported on desktop, but what if you wanted to achieve the same effect using an image as a mask instead of a shape? For that, we needed to turn to canvas composite operations.
Canvas supports a limited number of global composite operations. These operations allow you to put the context into a different rendering mode, and the objects that are drawn to the canvas after the mode is set are then subjected to the operation. For example, if the operation is “lighter” then images drawn will appear lighter in colour. One such mode is called “destination-out,” and what it proposes to do is ‘cut out’ a source image from a destination.
This was exactly the effect we needed for a recent game, Wolfblood: The Mystery of Stoneybridge. The game comprised of 19 unique puzzles, one of which had you hunting around a dark room looking for objects that appeared in one scene, but not in the other. This took place by dragging a large image of a wolf’s head around the screen:
(Game: Wolfblood: The Mystery of Stoneybridge by Photon Storm for CBBC)
In the screen shot above, you can see that the wolf’s head is showing a view into a slightly different room layout. It worked by carefully structuring the rendering order and using the destination-out composite operator.
First, we had our two room scene images. The ‘red’ version, which is the one the player sees the most of:
And the ‘lit’ image, which is exactly the same scene but just given a different palette and lighting.
As you play the game, you move around a wolf’s head image, and this cuts a view through from the red scene to the lit and colourful one below. The destination-out operation works by looking at the black pixels in an image and using those as the ones to be displayed.
Hidden Canvas required
There can be only one global composite operation in effect on a canvas at once. So, in order to get this effect working, we created a hidden off-screen canvas the same size as our game window. By default, a canvas is transparent, so in combination with our render order, the flow went like this:
- Render the scene we want the mask to reveal to our game canvas.
- Draw the foreground image, in our case the dark room scene, to a hidden canvas.
- Draw the mask image making sure that destination-out is set first.
- This results in all non-black areas of the canvas being cleared, so what you’re left with is just the shape of the mask image.
- You then draw your hidden canvas onto your game canvas.
The wolf’s head image looks like this:
But really, it could have been any shape we wanted. The yellow eyes you saw in the screen shot were rendered on top of the scene, just before the UI items.
The joys of Android
This approach works extremely well on desktop browsers and modern mobile browsers like Safari on iOS, or mobile devices running Chrome, such as the Nexus 7 or Samsung S3. Where it falls flat is with the stock Android browser. Although the effect works, it is horrendously slow. You may be able to mitigate this slow-down by using a lower resolution canvas and cropping the draw area to just the size of the mask, but ultimately some devices just plain struggle.
But when it does work, it works really well, allowing you to draw any shaped mask around and ‘peer through’ it into another world below.
You can play Wolfblood: The Mystery of Stoneybridge on the CBBC web site. Note: you will need to visit the site using a mobile web browser. This puzzle is puzzle 7 out of 19, so a little perseverance is also required.
If you have any other techniques for achieving the same visual result, then please do share them with us.