ben tedder : code things

Using Snap.svg and javascript to build an animated car

Let's walk through the process of using Snap.svg to animate an svg. I'm going to use a recent project as an example. If you check out the Green Isle Mile quiz, you'll see a little car with turning wheels. If you take the quiz, you'll see the car perform a variety of actions (spinning wheels, headlights, brake lights, etc) as the progress bar grows. Here's how I did it.

First, I created a Car constructor function in javascript.

var Car = function(args) {
  
};

This is important, because the car was instantiated a couple of times. Once on the homepage where the wheels spin forever, and once inside the quiz, where the car is much more animated.

Now, when the Car is instantiated, it's important that we pass it the svg element (or at least the DOM reference to it). The SVG element was split up in illustrator, and when I exported it I did a little work on the code in a text editor. I was able to add class names (chassis, wheel-front, headlight, etc) and group things logically. It was great to see the element in illustrator, export, and be able to work with it in code to manipulate it like I needed. Fun stuff.

So, by passing in something like the following, our Car constructor receives an element to begin working on.

var car = new Car({
            carEl: ".bar .car"
          });

Let's get into Snap.svg. It's quite an undertaking, but I ended up only using quite basic elements of it. You'll get a feel when you see how this Car object initializes:

Car = function(args) {   
  this.buildCar(args.carEl);
  this.listen();
};
Car.prototype.buildCar(el) {
  this.car      = Snap(el);
  this.chassis  = Snap.select(el + " .chassis");
  this.wheels   = {
    front: Snap.select(el + " .wheel-front"),
    rear: Snap.select(el + " .wheel-rear")
  };
  this.centers  = {
    front: this.getCenterPoint(this.wheels.front),
    rear: this.getCenterPoint(this.wheels.rear)
  };
  this.headlight  = Snap.select(el + " .headlight");
  this.beam       = Snap.select(el + " .beam");
  this.brakes     = Snap.select(el + " .brakes");
}

If you notice, I'm creating a new Snap element with each of the child nodes of the svg element I passed in. This allows me to to animate and do all kinds of fun things with the independent pieces and groups in the svg. I'm also attaching those elements to the Car object itself.

You'll also notice a function called getCenterPoint. This was by far the trickiest part...making a wheel turn on its center point...while moving. In the end, it was simple, just took a long time to figure out what was going on, as the center point was changing as the wheels were rotating. Long, boring story. Here's the code for that:

getCenterPoint: function(snapObj) {
  var svg = snapObj.getBBox();
  return (svg.cx + "," + svg.cy);
}

Next, I gave the car a few important public methods. These methods were public in the sense that the instantiator of the Car could call them as they see fit. As in, car.drive(), which would make the wheels turn.

drive: function() {},

park: function() {},

flash: function() {}

As you can see, these methods on the Car object are very obvious as to what they accomplish. However, they are supported by a few private methods. These private methods are the building blocks of what allows the car to drive, park, or flash. Check out the above methods with our new private methods called.

drive: function() {
  this._headlightsOn();
  this._wheelsTurn();
  this._brakeLightsOff();
},

park: function() {
  this._brakeLightsOn();
  this._headlightsOff();
  this._wheelsStop();
},

flash: function() {
  var _this = this;
  this.beam.animate({
    "fill-opacity": 1
  }, 200, function() {
    _this.beam.attr({ "fill-opacity": 0 });

  });
},

Looking back at this project, I would argue that flash should be abstracted more. I'm bringing in elements of Snap.svg in this method, and my goal was to keep the animations separated out from the main public methods of the Car. Lessons learned for next time. Moving on.

Let's examine the _wheelsTurn() function first. That one is interesting.

_wheelsTurn: function() {
  this._turn("front");
  this._turn("rear");
},

Wait, no it's not. Turns out that I needed to have a method to turn both wheels, but each wheel needed to know how to turn. Here's that:

_turn: function(wheel) {
  var _this = this;
  this.wheels[wheel].transform("r0," + this.centers[wheel]);
  this.wheels[wheel].animate({
    transform: "r90," + this.centers[wheel]
  }, 300, mina.linear, function() {
    _this._turn(wheel);
  });
},

So, look closely to what's happening here. When _turn is called, the wheel rotates back 0 degrees. Then we know where we're starting. Then we animate the wheel. The animation is a rotation of 90 degrees with the center being the center point of the specific wheel that is turning. It happens over 300 milliseconds with a linear curve. The callback function just calls _turn again. This is where the initial setting of the 0 rotation is important. Because my svg wheel is nicely split up into quadrants, I actually only need to turn the wheel 90 degrees, send it back, and it looks like a full rotation to the naked eye. Kind of cool, huh? Saw that method out there on the internet, and applied it to my situation.

The lights? Yep, when the car is in drive mode, the headlights slowly come on, and when the car stops, the headlights fade out. This one is done interestingly. I actually ended up applying a transparent gradient to the triangle shape that is the headlight beam. Then applying an opacity animation allows it to appear that the lights are actually turning on, and not just appearing.

_headlightsOn: function() {
  this.beam.animate({
    "fill-opacity": 1
  }, 1000, function() {
  });
},

So, I could go on and on (and would have done if time allowed) with all the cool things you could make the car do. But the biggest, most important factor in animating my Car with Snap was not necessarily Snap itself. It was in the organization of the Car. It really allowed me to keep Snap over in its corner and use it only when needed. I got to interact with the Car like a person would normally interact with a car:

car.drive() when I wanted it to move, car.park() when I wanted it to stop. The Car then knew "when I park, turn off my headlights, stop my wheels, and turn on my brake lights." I love the concept behind this. Treat the car like a car, and teach it what to do when you give it commands. Use Snap.svg when you want to make it look like a car.