How do you take a nebulous idea and turn it into a game — to get from technical details, to something interesting and challenging? Well recently, I found myself wondering whether CSS transitions could be used to make some kind of game. This article is about the exploration of that idea, and its development into an elegant and (as far as I know) unique kind of game-play.
The Basic Idea
The basic idea was to animate the left
and top
positions of an object, using a slow transition that the player partly controls. So, we’re going to need a playing area — let’s call that the board, and an animated object — let’s call that the ball:
<body>
<div id="board">
<span id="ball"></span>
</div>
</body>
The board has an aspect ratio of 3:2, while the ball is 5% of its width. Neither of those values are particularly crucial, they’re just what seemed most fitting — the aspect ratio was chosen so that it could (eventually) fit onto an iPhone screen, and the ball made relatively small so it has plenty of space to move around. The basic layout, with the ball at the top-left corner of the board is shown in the following demo.
The ball has negative margins, to offset it by half its own width and height, so that whatever position we set on the ball will be its center origin (e.g. the ball in that first demo is positioned at 0,0
). Here’s the CSS for that demo:
#board
{
position:relative;
display:block;
width:720px;
height:480px;
margin:24px auto 0 auto;
border-radius:2px;
background:#fff;
box-shadow:0 0 16px -2px rgba(0,0,0, 0.5);
}
#ball
{
position:absolute;
left:0;
top:0;
display:block;
width:36px;
height:36px;
margin:-18px 0 0 -18px;
border-radius:18px;
background:#f00;
box-shadow:inset 0 0 0 2px rgba(0,0,0, 0.35), 4px 10px 10px rgba(0,0,0, 0.15);
}
Ideally we would apply the board and ball sizes dynamically, based on the available window or screen space (this would be essential to port the game to mobile browsers), but to keep these examples simple, the dimensions are fixed — the board is 720×480 and the ball is 36×36.
The range of possible movement for the ball can now be described in percentage co-ordinates — from 0%,0%
at the top-left to 100%,100%
at the bottom-right. Using percentages is simpler than calculating pixels, and will allow for future flexibility in the sizes.
Now we can easily control the position by applying some simple JavaScript, that sets the left
or top
position according to directional key-presses, i.e. if the Left Arrow is pressed then set style.left
to "0"
, or if the Down Arrow is pressed then set style.top
to "100%"
:
var
ball = document.getElementById('ball'),
positions =
{
37 : ['left', '0'],
38 : ['top', '0'],
39 : ['left', '100%'],
40 : ['top', '100%']
};
document.addEventListener('keydown', function(e, data)
{
if(data = positions[e.keyCode])
{
ball.style[data[0]] = data[1];
e.preventDefault();
}
}, false);
The positions
array defines a property and value for each arrow keyCode
, and is also used in the first condition to know whether an arrow-key was pressed at all, in which case we have to use preventDefault()
to block its native action (so that the page can’t scroll at the same time). Again for the sake of simplicity, I haven’t done any feature-detection to filter older browsers. In practice we would want to pre-test the browser, to make sure that the transitions are fully supported. The following demo allows moving the ball to any corner.
Next, let’s add a slow transition
rule to animate movements. Notice the inclusion of vendor prefixes.
#ball
{
-moz-transition:all 5s ease;
-ms-transition:all 5s ease;
-o-transition:all 5s ease;
-webkit-transition:all 5s ease;
transition:all 5s ease;
}
Now the arrow-key changes don’t trigger a snap movement, they trigger a slow and gradual movement of the ball across the board. And since each key-press only changes the left
or top
position (never both), the overall effect is a novel and rather elegant kind of movement — a kind of “elasticity” that would be much more complex to script:
Try, for example, the following actions in that demo:
- Refresh the page to reset the ball
- Then press Right Arrow once
- Wait until the ball is half-way across (after 2.5 seconds)
- Then press Down Arrow once
Pressing Right Arrow will start a transition that moves the ball rightwards, then pressing Down Arrow will trigger a second transition that moves it downwards. But the second transition doesn’t affect the first, which will still be going, and the overall effect is a smooth curve — describing an arc from the top-center down to the bottom-right.
Refining the Game Play
We can now move the ball anywhere inside the board, using the arrow-keys to suggest a direction of movement. This provides control, but not full control, and therein lies the basic challenge that makes for a playable game. The amount of control we have also varies, because of the way the transitions are applied. For example, if the ball is at "left:0"
when you press the Right Arrow, it will take five seconds to reach the right-edge (as expected). However, if the ball is already at "left:80%"
when you press the Right Arrow, it will still take the full five seconds to travel that much smaller distance to the right-edge. In other words, the speed of the ball depends on how close it is to the direction you specify, when changing to that direction.
The choice of transition timing-function also makes a big difference. In these examples I’ve used the "ease"
function, which equates to the following bezier curve:
The graph shows relative speed, and illustrates how it accelerates at the start, then decelerates towards the end. So the ball will move more slowly near the start and end of the transition, and this will make it slightly easier to control at those points. In fact you could make the ball almost stand still, by rapidly and continually changing its direction.
Adding the Real Challenge
We’ve got a nice playable action now, but we still don’t have a game. There has to be something challenging — something you actually have to do within that restricted control. Perhaps we can use the same transition to add that extra something?
Since we’ve already defined the transition to apply to "all"
properties, we can simply extend the JavaScript so that each arrow-key also applies a change in background color, with a different bold color to correspond with each direction:
var
ball = document.getElementById('ball'),
positions =
{
37 : ['left', '0'],
38 : ['top', '0'],
39 : ['left', '100%'],
40 : ['top', '100%']
},
colors =
{
37 : '255,0,0',
38 : '255,255,0',
39 : '0,0,255',
40 : '0,255,255'
};
document.addEventListener('keydown', function(e, data)
{
if(data = positions[e.keyCode])
{
ball.style[data[0]] = data[1];
ball.style.backgroundColor = 'rgb(' + colors[e.keyCode] + ')';
e.preventDefault();
}
}, false);
And now, by pressing the arrow-keys, we not only change the ball’s position but also its primary color. Let’s also shift the ball’s default position to the center, and set its default color to gray (i.e. to a medium-bright color it will never have during play):
But of course, the color doesn’t instantly change, it gradually fades from one to another over the course of a single transition, passing through various intermediate shades along the way. For example, if the ball is red and then you press Right Arrow, it will change from red to blue via various shades of purple (as well as moving to the right).
Since each direction has a different color, it’s also possible for the same movement to result in different colors. For example, if you press Right Arrow then quickly press Down Arrow, the ball will travel to the bottom-right corner and fade to cyan (because cyan is mapped to down). However, if you press those keys in the opposite order (down and then right), the ball will still move to the same corner, but this time fade to blue (because blue is mapped to right). So for any given physical position, there are any number of possible shades of color that the ball might have.
And now I think, we have everything we need to make a game. If it’s difficult to fully control the ball, and difficult to get it to be a specific color, then we can create a game challenge by saying that you have to get the ball to a specific position and a specific color.
The Final Game Prototype
We’ll add a series of additional elements with different background colors — let’s call them the targets — and then add scripting that monitors the position and color of the ball. If the ball is inside a target area while it’s also the same color, then we call that a match, and the target disappears. That’s easy to describe, but it’s rather convoluted to actually script, as shown below.
var targets =
[
{ "color" : [220,180,40], "coords" : [5,5,12,35] },
{ "color" : [210,80,80], "coords" : [45,2.5,10,40] },
{ "color" : [160,90,60], "coords" : [65,5,20,20] },
{ "color" : [100,100,150], "coords" : [2.5,75,35,15] },
{ "color" : [150,70,100], "coords" : [55,65,10,20] },
{ "color" : [70,230,150], "coords" : [87.5,60,10,20] }
];
for(var len = targets.length, i = 0; i < len; i ++)
{
var target = document.createElement('div');
target.className = 'target';
target.style.left = targets[i].coords[0] + '%';
target.style.top = targets[i].coords[1] + '%';
target.style.width = targets[i].coords[2] + '%';
target.style.height = targets[i].coords[3] + '%';
target.style.backgroundColor = 'rgb(' + targets[i].color.join(',') + ')';
targets[i].target = ball.parentNode.insertBefore(target, ball);
}
var tracking = window.setInterval(function()
{
var ballcolor = window.getComputedStyle(ball).backgroundColor.replace(/[^0-9,]/g, '').split(',');
for(var n = 0; n < 3; n++)
{
ballcolor[n] = parseInt(ballcolor[n], 10);
}
for(var i = 0; i < targets.length; i ++)
{
if
(
ball.offsetLeft > targets[i].target.offsetLeft
&&
ball.offsetLeft + ball.offsetWidth < targets[i].target.offsetLeft + targets[i].target.offsetWidth
&&
ball.offsetTop > targets[i].target.offsetTop
&&
ball.offsetTop + ball.offsetHeight < targets[i].target.offsetTop + targets[i].target.offsetHeight
)
{
var match = 0;
for(var n = 0; n < 3; n ++)
{
if(Math.abs(ballcolor[n] - targets[i].color[n]) < 40)
{
match ++;
}
}
if(match === 3)
{
targets[i].target.parentNode.removeChild(targets[i].target);
targets.splice(i, 1);
if(targets.length === 0)
{
window.clearInterval(tracking);
window.setTimeout(function(){ alert('Yay!'); }, 250);
}
}
}
}
}, 62.5);
We have to allow for a certain amount of leeway when comparing the colors. We can’t expect the ball and target to be exactly the same (that would be all-but impossible), so we subtract one from the other and allow for a maximum difference. It’s because we need to do that, that the colors are applied using RGB, since RGB values are easier to work with programatically:
var match = 0;
for(var n = 0; n < 3; n ++)
{
if(Math.abs(ballcolor[n] - targets[i].color[n]) < 40)
{
match ++;
}
}
if(match === 3)
{
//... all three channels are sufficiently close
}
The tracking code itself is wrapped in a single setInterval()
loop, which (as far as I know) is the only way of continually monitoring the ball’s properties — using getComputedStyle()
along with offset properties, to get the ball’s color and position at each iteration. The interval shouldn’t be so fast as to put excessive strain on the browser, but it does still have to be fast enough to be accurate — based on the size and speed of the ball. Since the ball is 5% of the board, and moves the whole distance in five seconds, the ball will take an average 250ms
to move by its own width. So whatever proportion of that we use for the interval, will represent the maximum tracking drift as a proportion of the ball size, i.e. the maximum amount of discrepancy between the ball’s interval-calculated position, and its actual position. The speed I’ve set is 62.5ms
, which gives a maximum drift of one-quarter the ball’s size. Frankly, that’s a little faster than I’d have liked, but any slower than that will not be sufficiently accurate, and could give rise to a failure to detect valid matches.
It would all be much easier if there were some kind of per-frame callback event for CSS transitions, but there isn’t — the only event we have is a transitionend
event, which fires at the end of a transition, but that’s no use to us here.
But anyway — we have a game now! Try the finished prototype below and see how you get on — the object of the game is to match every target until the board is clear:
Beyond the Prototype
Nothing really happens when you’ve finished though, and it only has this one round! This is just a prototype, and even as it is, there are still refinements we could make. For example, if we restricted the ball’s movement so it’s not allowed to touch the edge, that would make the game-play more challenging and more edgy.
So join me soon for the second and concluding part of this article, in which we’ll look at how (and indeed, whether) we can develop this prototype further, into a finely-honed and distributable game.
In the meantime, you can download a zipfile of all this article’s demos:
James is a freelance web developer based in the UK, specialising in JavaScript application development and building accessible websites. With more than a decade's professional experience, he is a published author, a frequent blogger and speaker, and an outspoken advocate of standards-based development.