After the madness of Christmas 2014 (trust me, this industry is madness) we settle in to the quiet period… Lots of general concepting and out-of-the-box thinking, and other manager buzzwords. I generally get a bit of free time here to play with some things on my own – usually working on website designs.
One of the projects I toyed with this year was an element for a future design rather than the design its self. I tend to name everything according to some system when I’m sprouting concepts like this, and for this year I settled on moons of Saturn (essential trivia: in the past I’ve used Shakespearean characters and bars in Sydney), so this project is known to me as Mimas.
When I interviewed for my current job I had hacked together a prototype based on a brief sent to me, in the header was a simple snowfall effect, with the snow settling on the bottom of the frame. I decided to revisit that idea, but go upscale, and borrow some code from my work wrapping text in php with imagettftext etc.
So what we end up with is a falling snow animation in <canvas>, where the snow settles across the complex boundary of the foreground objects. In fact, we have two distinct collision zones.
The red line is a hard stop, but the green line is probabilistic. Once a flake crosses it it has a chance of stopping each frame, so it appears the snow is falling around the objects in 3D and makes the whole effect more realistic. To cap the whole thing off, we have animated smoke (a lot of groups of slightly different coloured circles) and subtly flickering glows in all the windows. Oooh, cosy!
Over time the effect blankets the objects, which you can see toward the end of the video above (total runtime on that is about 30 minutes, by the way).
So, how do we achieve this? Pretty easily really.
I’ll say first off you can view a live demo here – for all you eager code junkies who’ll understand the rawness of a messy prototype.
Now, on to the gubbins. All our data defining the lines is stored as percentages of the width and height of the images used in the construction to allow for resizing of the animation, the same as generating image previews, and it’s scaled at the start of the animation. Our collision testing also re-uses code from that project, a simple point-line intersection test which is all nice fast basic maths – I will confess to nabbing a well optimized version of this from the web, rather than going to work on my own!
self._prescaledmap = [ [0,0.93], [0.474,0.92], [0.474,0.99], [0.529,0.98], [0.529,0.92], [1,0.93] ]; self._prescaledpartialmap = [ [0.22285714285714287,0.9265033407572383], [0.2662857142857143,0.49888641425389757], [0.29828571428571427,0.799554565701559], [0.30657142857142856,0.7193763919821826], [0.3191428571428571,0.8641425389755011], [0.3497142857142857,0.5879732739420935], [0.37314285714285717,0.8240534521158129], [0.404,0.5167037861915368], [0.4361428571428571,0.8084632516703786], [0.4382857142857143,0.6859688195991092], [0.46014285714285713,0.6859688195991092], [0.4604285714285714,0.7204677060133631], [0.5392857142857143,0.7209220489977728], [0.5564285714285714,0.8262806236080178], [0.5735714285714286,0.6726057906458798], [0.59,0.821826280623608], [0.619,0.534521158129176], [0.6564285714285715,0.8930957683741648], [0.6678571428571428,0.779510022271715], [0.6835714285714286,0.9242761692650334] ]; self._scaleMaps = function(){ self.collisionmap = self._scaleMap(self._prescaledmap,true); self.partialmap = self._scaleMap(self._prescaledpartialmap,true); } self._scaleMap = function(tmap,minVal){ //scale a collision map var smap = [],miny=1; for (var i=0;i<tmap.length;i++){ smap.push( [ tmap[i][0] * self._width, tmap[i][1] * self._height ] ); if (tmap[i][1] < miny) miny = tmap[i][1]; } if (minVal) smap.push(miny * self._height); return smap; } self.testCollision = function(){ if (self._mapCollision(self.controller.collisionmap)){ return CollideType.FULL; }else if (self._mapCollision(self.controller.partialmap)){ return CollideType.PART; }else return CollideType.NONE; } self._mapCollision = function(map){ if (self.y < map[map.length-1]) return false; var c = false; for (var i=0;i<map.length-2;i++){ //-2 so we skip the minY as the last element if (((map[i][1]>self.y) != (map[i+1][1] > self.y)) && (self.x < (map[i+1][0]-map[i][0]) * (self.y - map[i][1]) / (map[i+1][1]- map[i][1]) + map[i][0]) ){ c = !c; } } return c; }
Note: The comment regarding “skipping minY as the last element” is related to some other logic. When the vertices are scaled the minimum Y value is also stored in the outputted array so we can easily exclude flakes that haven’t reached that point yet.
Besides that the animation aspects are fairly boilerplate. Because we have to clear and refresh the falling snow canvas each frame there’s actually a second canvas there too which holds the fallen snow, flakes are drawn there when they collide so their Javascript objects can be removed.
I won’t go in to too much detail because you can go and disect the code yourself, but it’s also worth mentioning I built a non-HTML5 version without the settling or collision detection, just falling snow. The loader stub uses jQuery to load either the posh version or boring version based on browser <canvas> support.
Thanks all,
— Ben