Greetings one and all, today we’ll be taking a look at another bit of concepting/messing around from the start of 2015. This time we’ve got an animation of some flying reindeer!
This follows on from the settling snowfall effect (“Mimas”) I’ve covered previously. This project was started directly after Mimas, but never reached the same level of completion. Following the years’ theme of moons of Saturn, this project was called Iapetus.
The idea was simple: animate a sleigh flying through the sky and dropping presents on to houses. Another neat use of HTML5 canvas to create a little background effect.
But that wasn’t enough! I wanted to animate the reindeer’s limbs and head so they actually looked like they were moving through the sky. I initially considered recreating a spritesheet system like the one I used to use in Haxe games – I ported this to Javascript for a concept piece way back in 2013 and the performance wasn’t bad, and that was using DOM. But hey, the canvas gives us all the tools we need to make this work, so I went to work implementing something.
Bored of all this talking already? Go look at it running then.
Incidentally this is yet another project where I found a new Javascript OOP pattern I liked and had a play with it. I actually really like this OOP shim created by John Resig, it provides inheritance in all the ways you’d expect, function overriding with parent access, and consistent this values – it’s the closest I’ve felt to something like C# or Java while writing Javascript. Those 25 magical lines of code allow you to do brilliant things…
var AnimClip = Class.extend({ init:function(opts){ $.extend(true,this,{ easing:$.easing.linear, minval:0, maxval:0, prop:"", loop:false, reverse:false, duration:0, target:-1, _elap:0 },opts); } }); var BezierPosAnimClip = AnimClip.extend({ init:function(opts){ this._super($.extend({mirrorImage:false},opts)); } });
Above we create a “class” called AnimClip by using the extend method of the basic Class object. Constructors in John Resig’s system are provided by way of an init method, and in there we use jQuery’s $.extend method to merge default options with anything passed in arguments. Below that, we extend AnimClip to create BezierPosAnimClip – which provides the same animation system, but along a bezier curve – and in that constructor we again use $.extend to merge a new, additional option in to any arguments, and then send that object to _super, which is the AnimClip constructor.
Moving right along, how does this unholy mess of ragdolling reindeer work?
Simple really. The canvas – like many similar graphical constructs – is manipulated by way of a transformation matrix. Look these up if I’ve lost you already: Matrix math is a bit weird at first but it’s good to have an understanding of how 2D graphics applications achieve some of the things they do. Luckily in this case you don’t have to dive in to the maths (well, unless you want to…) because Javascript provides simple methods for transforming the canvas.
Suppose we have an image, and we want to draw it at (100,100), and rotate it 90 degrees? To achieve this we must move the entire canvas by (-100,-100), then rotate it -90degs, draw the image, rotate +90degs, and move (+100,+100) to get back to where we started. You can work out the state of the matrix after each transformation as a homework assignment.
So that’s great, but what if we have an image that has a “sub image” which needs to be affected by the parents position and rotation, but also has it’s own properties to add to that? We need to burrow down through the object chain, applying each parent object’s transformations on the way down and reversing them on the way up so everything is properly nested. In practise, this isn’t so hard.
My code has a class IGameObject which holds all the data for a given object: position, rotation, center of rotation, scale etc, as well as an array of children. Each frame we enter the update method and if we have children we use a pair of methods applyGlobalRotate and endGlobalRotate, which transforms the canvas for the current object so we can call in to the child objects to do the same. An extension of this, IAnimObject, adds an array of IAnimClips and code to utilize them so we can animate our different body parts. This little animation system reads an array of AnimClips each frame, which each manipulate a single property between a min and max value, optionally with one of jQuery’s easing functions.
To animate our reindeer we split him in to several parts: the body comprises our main object, and the two pairs of legs and head are children of the body. The initialization of a reindeer looks like this:
var Settings = { reindeerpoints: {head:[0.6024229074889868,0.4259887005649718],fleg:[0.72,0.4769230769230769],bleg:[0.3057709251101322,0.6085310734463277]} }; var reindeer = new IGameObj({ x:68, y:10, width:200, height:195, scale:0.15, src:"assets/deer-body.png", rot:15, children:[ new IAnimObj({ src:"assets/deer-head.png", width:200, height:195, scale:0.15, scaleRp:true, rpx:Settings.reindeerpoints.head[0], rpy:Settings.reindeerpoints.head[1], anims:[ new AnimClip({prop:"rot",loop:true,reverse:true,minval:0,maxval:20,duration:8000,easing: $.easing.easeInOutQuad }) ] }), new IAnimObj({ scaleRp:true, rpx:Settings.reindeerpoints.fleg[0], rpy:Settings.reindeerpoints.fleg[1], src:"assets/deer-flegs.png", width:200, height:195, scale:0.15, anims:[ new AnimClip({prop:"rot",loop:true,reverse:true,minval:0,maxval:45,duration:5000,easing: $.easing.linear )}) ] }), new IAnimObj({ scaleRp:true, rpx:Settings.reindeerpoints.bleg[0], rpy:Settings.reindeerpoints.bleg[1], src:"assets/deer-blegs.png", width:200, height:195, scale:0.15, anims:[ new AnimClip({prop:"rot",loop:true,reverse:true,minval:-30,maxval:0,duration:5000,easing: $.easing.linear )}) ] }) ] });
Pretty neat, I thought. In an ideal world of course the scale parameter from the parent object would also affect the children, but prototypes will be prototypes…
The only other part of the initial idea implemented was a very basic flyover animation using a linear Bézier curve as the route. No looping or reset occurs, the animation runs once. The last thing I remember working on in this code was making the individual reindeer and sleigh rotate to match the curve so they aren’t just a flat plane, but I clearly never got this done.
The Bézier code in question is actually ported again from some old Haxe code (in particular the Pinball project, which featured entire arenas designed with linked Béziers), and is quite neat and simple. Here’s everything you need:
function Bezier(start,mid,end){ var self = this; self.start = start; self.mid = mid; self.end = end; self._genVars = function(t){ var u = 1-t; return {u: u, tt: t*t, uu: u*u}; }; self.getPoint = function(t){ var v = self._genVars(t); return [ (v.uu * self.start[0]) + (2 * v.u * t * self.mid[0]) + (v.tt * self.end[0]), (v.uu * self.start[1]) + (2 * v.u * t * self.mid[1]) + (v.tt * self.end[1]) ]; }; }
The Bézier object takes three vertices as parameters: the start, middle and end point, which influence the path of your line. A call to getPoint() with 0 < t < 1 will return the position along the Bézier path denoted by t.
Integration with jQuery’s easing functions is super simple, too…
easeInOutQuad: function (x, t, b, c, d) { if ((t/=d/2) < 1) return c/2*t*t + b; return -c/2 * ((--t)*(t-2) - 1) + b; }
jQuery’s easings look like the above, with five parameters. In order they are: unused, current time, start value, end value and total duration. Sneakily, and very usefully, we can just send our current percentage completion and we’ll get the same animation effect as doing it any other way.
update:function(){ ... this._elap += Settings.frametime; var eased = this.easing(null, this._elap, 0, 1, this.duration); //returns 0 < x < 1 var pos = this.bezier.getPoint(eased); ... }
That’s probably about all there is to this code – as I say, a prototype that was cut short. I had a basic background scene ready to start dropping presents on but never got that far.
Go and take a look at Iapetus, the flying reindeer animation, and view source if you’d like. You have a little selection of buttons to view the sleigh “up close” or the flyover animation, set the easing for all animations to anything in the jQuery library, as well as a debug toggle to show the points of rotation for each object. FYI the “default” animation easings as designed are mostly linear, the only exception being $.easeInOutQuad on the reindeer heads.
– B