Torch effect demo

In browser torch effect

It will come as no surprise to any of you that I quite like medieval history and fantasy based around that time, I of course also like creating games in my spare time, recently I had a thought about transferring some of the effects I might normally create for a game to websites, especially since as browser tech improves so do options for developing cool effects. Proof of concept in this case is a demo of a flickering torch that illuminates a background image. I’ve done the same thing previously in allegro and C++ so it was to an extent simply a case of porting it. There is a demo of it here, and below is how it works:

Design

The plan is simple, have an animated gif of a torch, the source of our light, then have a ring of light around it to give the effect of light, and then cover the rest of the screen in black to hide the rest of the background. The torch light needs to flicker a bit to improve the appearance of the effect.

html structure

This is actually so elegant, it’s all a series of divs with absolute positioning, that when the mouse is moved alter the position of the torch. Here is an image of the layout of the divs in the final example:

torch-layout

The html of this looks like:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<body>
<div id="torch-light">
<img src="res/flare.png" />
</div>
<div id="torch">
<img src="res/Torch.gif" />
</div>
<div id="torch-top">
</div>
<div id="torch-left">
</div>
<div id="torch-right">
</div>
<div id="torch-bottom">
</div>
</body>

The images are loaded into their parent divs, it would be possible to do them as background images if you wished but I chose to use include them in the html.

css

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
#torch,#torch-light,#torch-left,#torch-top,#torch-bottom,#torch-right
{
/* allow us to position all elements anywhere on the page */
position: absolute;
}
 
html {
/* setup the background to cover the screen regardless of resolution - go css3! */
/* The image is from http://gurgur.deviantart.com/ Who is a very talented artist */
background: url('res/background.jpg') no-repeat center center fixed;
-webkit-background-size: cover;
-moz-background-size: cover;
-o-background-size: cover;
background-size: cover;
/* prevent us from creating extra document space as the image leaves the visible area. */
overflow: hidden;
}
 
body
{
/* This is a fix for firefox which apparently has issues applying absolute positioning if the parent element does not have relative positioning */
position: relative;
}
 
#torch
{
/* hide the mouse cursor */
cursor: none;
/* ensure the image floats above the #torch-light element */
z-index: 100;
}
 
#torch-top,#torch-bottom,#torch-left,#torch-right
{
/* Make the edges black */
background-color: #000;
/* but lets not make them completely solid */
opacity: 0.9;
}
 
#torch-light
{
/* because the edges are not completely opaque we should not let the torch image be completely opaque */
opacity: 0.9;
}

The basic idea is that the all the elements can be positioned in relation to the mouse position, which we will see in the below javascript.

javascript

I’ve broken this section into two parts, one for when the torch is moved and the other is when the torch is static and we want it to wobble.

Moving the torch

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
$(document).mousemove( function( event ) {
// Get width and height of the 'viewport' (The area of screen not including scroll bars etc.)
var viewportWidth = $(document).width();
var viewportHeight = $(document).height();
 
// These are of the edge of the torch itself and are worked out relative to the mouse event position
var leftEdge = event.pageX - 31;
var topEdge = event.pageY - 80;
var rightEdge = event.pageX + 31;
var bottomEdge = event.pageY + 80;
 
// Quick check to stop the torch going off the edges of the screen
if(bottomEdge > viewportHeight)
{
bottomEdge = viewportHeight;
topEdge = viewportHeight - 160;
}
if(rightEdge > viewportWidth)
{
rightEdge = viewportWidth;
leftEdge = viewportWidth - 62;
}
 
// calculate the top left corner of the light effect then add a shift of -5 - +5 pixels which gives our flicker effect.
var flareLeft = event.pageX - 256 + (-5 + Math.floor((Math.random() * 10) + 1));
var flareTop = event.pageY - 256  + (-5 + Math.floor((Math.random() * 10) + 1));
 
// Position everything up
$("#torch-light").offset( { top: flareTop, left: flareLeft });
$("#torch").offset({ top: topEdge, left: leftEdge})
$("#torch-left").offset( {top: 0, left: 0} );
$("#torch-left").width(flareLeft);
$("#torch-left").height(viewportHeight);
$("#torch-right").offset( {top: 0, left: flareLeft+512 } );
$("#torch-right").width(viewportWidth - (flareLeft+512));
$("#torch-right").height(viewportHeight);
$("#torch-top").offset( {top: 0, left: flareLeft} );
$("#torch-top").width((512));
$("#torch-top").height(flareTop);
$("#torch-bottom").offset( {top: flareTop+512, left: flareLeft} );
$("#torch-bottom").width((512));
$("#torch-bottom").height(viewportHeight - (flareTop+512));
});

As the comments show, we get the position of the torch and move all the elements relative to it, now I’ve been slightly lazy and hard coded the dimensions of the image, obviously if this was production I’d store them in variables or even calculate them on the fly, but it’s a simple enough test.

 Applying the flickering on a timer

Nothing especially fancy, we have a function that handles the flicker:

You will notice that this code is actually a copy of the movement code except that instead of handling an event I take the current centre of the torch to be the location of the mouse (since it should be). It’s not elegant and could most certainly be refined.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
function wobbleflame()
{
var cursorOffset = $("#torch").offset();
eventpageX = cursorOffset.left + 31;
eventpageY = cursorOffset.top + 80;
var viewportWidth = $(document).width();
var viewportHeight = $(document).height();
var leftEdge = eventpageX - 31;
var topEdge = eventpageY - 80;
var rightEdge = eventpageX + 31;
var bottomEdge = eventpageY + 80;
if(bottomEdge > viewportHeight)
{
bottomEdge = viewportHeight;
topEdge = viewportHeight - 160;
}
if(rightEdge > viewportWidth)
{
rightEdge = viewportWidth;
leftEdge = viewportWidth - 62;
}
 
var flareLeft = eventpageX - 256 + (-10 + Math.floor((Math.random() * 20) + 1));
var flareTop = eventpageY - 256  + (-10 + Math.floor((Math.random() * 20) + 1));
 
$("#torch-light").offset( { top: flareTop, left: flareLeft });
$("#torch").offset({ top: topEdge, left: leftEdge})
$("#torch-left").offset( {top: 0, left: 0} );
$("#torch-left").width(flareLeft);
$("#torch-left").height(viewportHeight);
$("#torch-right").offset( {top: 0, left: flareLeft+512 } );
$("#torch-right").width(viewportWidth - (flareLeft+512));
$("#torch-right").height(viewportHeight);
$("#torch-top").offset( {top: 0, left: flareLeft} );
$("#torch-top").width((512));
$("#torch-top").height(flareTop);
$("#torch-bottom").offset( {top: flareTop+512, left: flareLeft} );
$("#torch-bottom").width((512));
$("#torch-bottom").height(viewportHeight - (flareTop+512));
}

Then we have a timer call set at 100milliseconds (which is the same as the animation rate of the torch flame):

1
window.setInterval(function () { wobbleflame(); }, 100);

That’s it!

All together you get a lovely flickering torch effect as shown here.

Improvements

Naturally it’s a little rough and ready, obvious improvements are working out the size of the torch image and to simplify the code so that the same function can be used for both parts of the code.

Note: I have now done this, the improved javascript is:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
<script language="javascript">
function updateFlame( event )
{
var torchWidth = $("#torch").width();
var torchHeight = $("#torch").height();
var ringWidth = $("#torch-light").width();
var ringHeight = $("#torch-light").height();
if(!event)
{
var cursorOffset = $("#torch").offset();
eventpageX = cursorOffset.left + (torchWidth / 2);
eventpageY = cursorOffset.top + (torchHeight / 2);
}
else
{
eventpageX = event.pageX;
eventpageY = event.pageY;
}
var viewportWidth = $(document).width();
var viewportHeight = $(document).height();
var leftEdge = eventpageX - (torchWidth / 2);
var topEdge = eventpageY - (torchHeight / 2);
var rightEdge = eventpageX + (torchWidth / 2);
var bottomEdge = eventpageY + (torchHeight / 2);
if(bottomEdge > viewportHeight)
{
bottomEdge = viewportHeight;
topEdge = viewportHeight - torchHeight;
}
if(rightEdge > viewportWidth)
{
rightEdge = viewportWidth;
leftEdge = viewportWidth - torchWidth;
}
 
var flareLeft = eventpageX - (ringWidth / 2) + (-10 + Math.floor((Math.random() * 20) + 1));
var flareTop = eventpageY - (ringHeight / 2)  + (-10 + Math.floor((Math.random() * 20) + 1));
 
$("#torch-light").offset( { top: flareTop, left: flareLeft });
$("#torch").offset({ top: topEdge, left: leftEdge})
$("#torch-left").offset( {top: 0, left: 0} );
$("#torch-left").width(flareLeft);
$("#torch-left").height(viewportHeight);
$("#torch-right").offset( {top: 0, left: flareLeft+ringWidth } );
$("#torch-right").width(viewportWidth - (flareLeft+ringWidth));
$("#torch-right").height(viewportHeight);
$("#torch-top").offset( {top: 0, left: flareLeft} );
$("#torch-top").width(ringWidth);
$("#torch-top").height(flareTop);
$("#torch-bottom").offset( {top: flareTop+ringHeight, left: flareLeft} );
$("#torch-bottom").width(ringWidth);
$("#torch-bottom").height(viewportHeight - (flareTop+ringHeight));
}
 
$(document).ready( function() {
 
window.setInterval(function () { updateFlame(); }, 100);
 
$(document).mousemove( function( event ) {
updateFlame (event);
});
});
</script>