“Bubbles” canvas tutorial part 3: Getting touchy

HTML5 Bubbles 4

So far in this series we’ve set up a basic framework to get us started and then added a render loop and some actual bubbles.

This part will make the game interactive, adding mouse and touch input, and discussing some of the pitfalls that can crop up as you do so.

Starting simple: the mouse

Getting basic mouse input is easy – just add the relevant event listener to our canvas:

	function setupCanvas() {
		config = new Config(document.body.clientWidth, document.body.clientHeight);
		var canvas = document.getElementById("frame");
		canvas.width = config.width;
		canvas.height = config.height;
		canvas.addEventListener("mouseup", onClick, false);
		context = canvas.getContext("2d");
	}

This calls the onClick function whenever you click on the canvas. Now to handle that:

	function onClick(event) {
		var touchX = event.pageX;
		var touchY = event.pageY;
		for (var index = bubbles.length - 1; index >= 0; index--) {
			var bubble = bubbles[index];
			if (Math.sqrt(Math.pow(touchX - bubble.x, 2) + Math.pow(touchY - bubble.y, 2)) < bubble.radius) {
				bubbles.splice(index, 1);
				addBubble();
			}
		}
	}

The click event has two properties we’re interested in – pageX and pageY – the coordinates of the click. Once we have them we just iterate over all our bubbles and get the distance between the click coordinates and the bubble coordinates. If the distance is less than the radius then the click was inside a bubble. Remove it and add a new one. Because we don’t break out of the loop on a touch we will “pop” all bubbles that we click on in one go, so if two bubbles are overlapping we’ll pop them both at once. Note that we iterate backwards through the list of bubbles – that’s because all the removing and adding happens at the current point or higher. Iterating backwards stops that from affecting our loop.

That’s really all there is to mouse input. When a mouse button is raised on the canvas our function is called and we update our bubbles. Simple.

Touch input: not so simple

Adding touch events for mobile devices is – on the surface – as simple as adding mouse events:

		canvas.addEventListener("touchstart", onTouchStart, false);

Unfortunately you start to run into problems at that point. Firstly, touch events assume a multi-touch interface, so rather than a pair of coordinates we have an array of “touches”, each with their own coordinates:

function onTouchStart(event) {
	for (var index = 0; index < event.touches.length; index++)
		touch(event.touches[index].pageX, event.touches[index].pageY);
}

You’ll notice that while this works (possibly, depending on device) you’ll get window scrolling happening, tap-zooming, and all kinds of other touch-related browser interaction going on. Fortunately there’s an easy way of dealing with that:

function onTouchStart(event) {
	event.preventDefault();
	for (var index = 0; index < event.touches.length; index++)
		touch(event.touches[index].pageX, event.touches[index].pageY);
}

event.preventDefault() stops the default behaviour of the event so it won’t be used in handling scrolling or the like. It’s almost there. All we need now is to add a handler for “touchmove” just to be sure:

	canvas.addEventListener("touchmove", onTouchMove, false);

	function ontouchMove(event) {
		event.preventDefault();
	}

As you might have noticed above I pulled out the actual bubble popping code into new touch function so we don’t have to repeat ourselves for touch events. The full input-handling code is below:

	function setupCanvas() {
		config = new Config(document.body.clientWidth, document.body.clientHeight);
		var canvas = document.getElementById("frame");
		canvas.width = config.width;
		canvas.height = config.height;
		canvas.addEventListener("mouseup", onClick, false);
		canvas.addEventListener("touchstart", onTouchStart, false);
		canvas.addEventListener("touchmove", onTouchMove, false);
		context = canvas.getContext("2d");
	}

	function onClick(event) {
		touch(event.pageX, event.pageY);
	}
	
	function onTouchStart(event) {
		event.preventDefault();
		for (var index = 0; index < event.touches.length; index++)
			touch(event.touches[index].pageX, event.touches[index].pageY);
	}
	
	function onTouchMove(event) {
		event.preventDefault();
	}
	
	function touch(touchX, touchY) {
		for (var index = bubbles.length - 1; index >= 0; index--) {
			var bubble = bubbles[index];
			if (Math.sqrt(Math.pow(touchX - bubble.x, 2) + Math.pow(touchY - bubble.y, 2)) < bubble.radius) {
				bubbles.splice(index, 1);
				addBubble();
			}
		}
	}

Tiny bubbles for tiny fingers

This all works fine here since our bubbles are quite large. In my final version1)For given values of ‘final’ I’ve got a whole bunch of extra features like bubble drift and spawning out smaller bubbles when you pop a larger one. Going through all of that here would just muddy the waters and distract from the core points – if you want to know how they work and implement something similar yourself the code is free to read through. However, one feature did lead to me needing to add an extra bit of code that’s very useful to know.

I was finding that touch events on mobile devices worked fine for larger bubbles but usually failed terribly on smaller ones – the exact coordinates were just too imprecise. We can work around this by increasing the distance from the centre of the bubble in our “touch” check on small-screen devices.

This is done quite simply by adding a “sensitivity” parameter to our touch function, which will be larger when called from touch events and smaller on mouse events:

	function onClick(event) {
		touch(event.pageX, event.pageY, 1);
	}
	
	function onTouchStart(event) {
		event.preventDefault();
		for (var index = 0; index < event.touches.length; index++)
			touch(event.touches[index].pageX, event.touches[index].pageY, 2);
	}
	
	function onTouchMove(event) {
		event.preventDefault();
	}
	
	function touch(touchX, touchY, sensitivity) {
		for (var index = bubbles.length - 1; index >= 0; index--) {
			var bubble = bubbles[index];
			if (Math.sqrt(Math.pow(touchX - bubble.x, 2) + Math.pow(touchY - bubble.y, 2)) < (sensitivity * bubble.radius)) {
				bubbles.splice(index, 1);
				addBubble();
			}
		}
	}

Another useful trick to bear in mind when rendering on smaller screens is to make the bubbles bigger so they are physically larger despite the higher resolution. For example, a standard desktop monitor might well be 20 inches and 1600 pixels wide, whereas a phone screen might be three inches and 768 pixels wide. A fifty pixel bubble is going to be just over half an inch wide on the desktop but only a fifth of an inch wide on the phone. If you want to address this you can use the devicePixelRatio property of the window. On desktop browsers this will almost certainly be 1, whereas on most phone I’ve tested this one it is 2. Therefore you can use this ratio to increase the radius of your bubbles:

	function Bubble(x, y) {
		this.radius = 50 * window.devicePixelRatio;
		this.x = x;
		this.y = y + this.radius;
		this.speed = config.baseSpeed * ((Math.random() * 0.6) + 0.8);
	}

Gabi poppa bubble?

And there we have it. A simple bubble-popping game. For a two year-old boy, admittedly, but a game nonetheless. There’s a lot you can add – as I have, and plan to continue – but now we have the core. Bubbles appear, float, disappear when you touch them, and more appear. That’s enough to keep Baby Prime enthralled for the time being.

The finished version of today’s script can be found here.

In the next part we add some sound effects to the game, which is the single most complex part of the whole project so far and the one where the internet failed me over and over and over. If this series is of use to anyone, that’s where it will be. Stay tuned.


Feet   [ + ]

1. For given values of ‘final’

Leave a Reply

Post Navigation