Sunday, January 6, 2013

Animating CSS3 3D Transforms: Performance with Many Elements in the DOM

For the New Year, I wanted to try making a firework effect in the browser. The idea I had was to create a containing DIV which represented a single firework. Within that DIV, each "spark" consists of a placeholder DIV with the actual DIV representing the spark in the top left corner. Each placeholder is then rotated around relative to the center on the Z and Y-axis. Once setup, the bursting of the firework can be controlled by resizing the placeholders. The spark would stick in the corner of the placeholder and the browser would do the work to figure out where in 3D space everything needed to exist. The approach works, however, only in certain browsers and, at least in the ones I tested, only smoothly in Chrome. However, I decided to write up what I did and use it as a reference as browser technology continues to improve.

To begin, we need some CSS to define the different components. The important part is configuring the 3D elements and define the actual individual embers of the exploding firework:



.boom {
position: absolute;
height: 300px;
width: 300px;
top: 300px;
left: 300px;

perspective: 500px;
}

.boom > div {
position: absolute;

transform-origin: 50% 50% 0px;
transform-style: preserve-3d;
}

.place {
top: 0;
left: 0;
height: 0;
width: 0;
}

.spark {
width: 4px;
height: 3px;
border-radius: 5px;
background-color: #f00;
color: #f00;
}


Here, the perspective style enables the 3D effect by establishing a vanishing point. The pereserve-3d setting ensures that same point is used by all the children. In the final animation, all the HTML is generated dynamically. Each boom element is added to the DOM with all its child elements, animated, and then removed. However, in the initial test, I created a static boom DIV and dynamically added the place and spark elements to it. Below is a snippet of parts of the resulting HTML that is generated. All of the transforms have randomized positioning to ensure the resulting sphere is not too uniform.


<div class="boom">
<div style="transform: translateX(-39px)
rotateY(0deg)
rotateZ(24deg);"
class="place">
<div style="transform: rotateY(0deg);" class="spark"></div>
</div>

<div style="transform: translateX(-40px)
rotateY(0deg)
rotateZ(47deg);"
class="place">
<div style="transform: rotateY(0deg);" class="spark"></div>
</div>

...

<div style="transform: translateX(-26px)
rotateY(20deg)
rotateZ(-6deg);"
class="place">
<div style="transform: rotateY(-20deg);" class="spark"></div>
</div>

<div style="transform: translateX(-26px)
rotateY(20deg)
rotateZ(23deg);"
class="place">
<div style="transform: rotateY(-20deg);" class="spark"></div>
</div>

</div>


Notice in the last two place elements, the spark element has a counter-rotation on the Y-axis to cancel out the rotation on the parent place element. Without this, the rotated sparks would gradually squish until they disappeared. This is because the DIV is flat and as it rotates around the Y-axis, at the 90 degree point, all you see is the edge which as no size. The counter-rotation ensures the spark is always facing out toward the screen.

Finally, the place elements have no initial height or width. These will be animated to make the explosion effect. The code to generate the HTML and set all the 3D transforms is as follows:



var $b = $('.boom');
var i,
cnt = 150,
x = 0, y = 0, z = 0,
incZ = 40,
incY = 20;


// Make a bunch of sparks to assemble the
// firework.
for (i=0;i<cnt;i++)
{
// Rotate each spark container (.place) on the
// Z and Y axis. Everything needs a little
// random staggering to avoid looking too uniform

z += Math.floor(incZ + Math.random() * 10 - 20);
if ( z > 360 )
{
y += incY;
z = Math.floor(Math.random() * 5 - 10);

if ( y > 360 )
y = 0;
}

// Shifting a little on the X-axis helps the
// staggering effect even more.
x = Math.floor(Math.random() * 20 - 40);

// Now create the actual spark. It will be contained by
// the .place wrapper which will be what is actual resized and moved
// around during the animation.

// The spark does need to be rotated in the negative Y direction
// to compensate for the Y rotation on the container, otherwise,
// it will appear like a line in some cases since it will be
// rotated on its side.

$('<div class="place"><div class="spark"></div></div>')
.appendTo($b)
.css('transform', 'translateX('+x+'px) rotateY('+y+'deg) rotateZ('+z+'deg)')
.find('.spark')
.css('transform', 'rotateY(-'+y+'deg)');
}



Most of this code is spent creating the randomized positions. The final part of the loop actually creates the elements, sets their classes, and initializes their 3D location.

The only remaining step is to animate the expansion of all the place elements so the sparks fly outward. In my sandbox, I added sliders to allow stepping through different parts of the effect. In the final animation, I broke it into two parts - the initial explosion and the dissipation of the embers.


// slider1 represents the explosion part of the firework
// when it rapidly expands to its final size.

$('#slider1').slider({
slide: function ( e, ui ) {
var pos = ui.value / 100,
pos2 = $('#slider2').slider('value') / 100;

$('.place').css({
top: -150 * pos + 30 * pos2,
left: -100 * pos,
width: 200 * pos,
height: 200 * pos + 90 * pos2
});
}
});


// slider2 represents the fading part of the firework
// as it starts to be affected by gravity and the sparks
// fizzle away.

$('#slider2').slider({
orientation: 'vertical',
slide: function ( e, ui ) {
var pos2 = ui.value / 100,
pos = $('#slider1').slider('value') / 100;

$('.place').css({
top: -150 * pos + 30 * pos2,
height: 200 * pos + 90 * pos2
});
}
});


Here you can see that by adjusting the position and size of each place DIV, forces the browser to render the 3D portion of the position which creates the look of an exploding sphere. By slightly offsetting the adjustments to top/height values, the firework will appear to be still traveling up while exploding and then slowly falls while the sparks fizzle away.

The animated version uses the GreenSock Animation Library to attach animations to these styles on each place element and adds them to a timeline so they can be sequenced and more easily controlled. It also has timings to fire several fireworks at a time - slightly staggered. The whole effect creates between 600 - 900 DIVs on each firing sequence. Combined this with the 3D styles and the browser is doing a lot of work.

I used the sandbox version as a reference to remove the animation from the equation. If you use Firefox, you can see it is a lot choppier to resize the firework with the sliders compared to Chrome. Once you start animating these elements, Firefox gets quite slow and drops many frames while Chrome is nice and smooth (although, that will depend on how the computer your using to view it). Its clear, at this point, that Chrome has the lead in both CSS3 3D features and the actual rendering performance. It will be interesting to see how this demo improves as more browsers implement the features and tweak their rendering engines to increase performance.