Apple-Style Counter Revisited

November 15th, 2010 / Code

My original post detailing how I recreated Apple's flip-style counter is a popular one, and I get many questions about how to properly implement it. It didn't take long before I saw some shortcomings to the original technique I used, so I thought it was time to revisit my Apple-style counter and make some improvements.

Linear sucks

The biggest problem I had with the original counter was that it was restricted to counting in a linear fashion. The background graphic was a massively long image in which we would drop from frame to frame until it hit the bottom and then we'd start back at the top. You had to animate in numerical order (1 → 2 → 3 → 4) or else the animation would jump around and look bad. All you could do was say what number you want to start at, and how fast you want to count. That's it. I found that very limiting—it needed to be able to animate to and from any digit.

Non-linear bliss

In order to animate freely between digits, I needed to break each digit into separate top and bottom pieces, and animate each independently:

Top zeros

Top zero states.

Bottom zeros

Bottom zero states.

So now instead of one huge 35KB graphic, we have two smaller graphics totaling 23KB at the expense of an added HTTP request. I'm OK with that trade-off.

More complicated animation

Top animation Top animationNaturally, having two separate pieces for each digit makes the background image animations more interesting. For example, lets see how the script animates from 0 to 3 (red) and then from 3 to 7 (blue).

The left graphic is the top half of the digit, and the animation sequence is fairly simple. The starting position for each digit is the first slot in each row. Once the top of the digit finishes we animate the bottom half, which you can see in the graphic on the right.

The bottom animation jumps around a bit more, because I wanted the starting position for each bottom to also be in the first slot of each row. In slot 2 there is a shadow to indicate the top half of the digit is halfway through its fall, perpendicular to the screen and invisible. We then jump to the appropriate slot 3, which is the first you can see of the new digit falling into place, followed by the last two jumps to finish the animation.

So the three red jumps would happen first for the top, followed by the four red jumps for the bottom of the digit. Then it would start over for the blue jumps. It is only more complicated conceptually, but in practice only creates one extra background animation action.

The same effect could also be accomplished if all digits were in one long column like they were in my original script, and just shift it up and down rapidly into the correct position. Or you could just join the top and bottom graphics into one large sprite and handle the offsets accordingly in the animation. However, the logic controlling the background-position animations would be more complicated that way, so I broke it into two separate graphics for simplicity's sake.

Added functionality

With my original script, there wasn't much you could actually do with the counter. Its functionality was limited to a "set it and forget it" setup. There was no easy way to manipulate the counter once created. Let's start with that... creating a new counter:

var myCounter = new flipCounter(div, options);
div
ID of the div that will hold the counter, entered without the '#'.
options
Object with the following properties:

  1. value [int]: Initial value of the counter. Default: 0
  2. inc [int]: Increment of the counter. Default: 1
  3. pace [int]: Pace of the counting in milliseconds. Default: 1000 (1 second)
  4. auto [bool]: Should the counter auto-increment? Default: TRUE
  5. tFH [int]: Top frame height. Default: 39
  6. bFH [int]: Bottom frame height. Default: 64
  7. fW [int]: Frame width. Default: 53
  8. bOffset [int]: Bottom sprite offset. As of v0.5.2 I combined the top and bottom digit sprites into one. The sprites didn't change at all, I just stacked them to create one long sprite. The script expects the top half sprite to be right on top of the bottom one, and bOffset tells it how many pixels down before the bottom sprite starts. If you want to continue using the two separate sprites, just set this to zero. Default: 390
If you change any frame dimensions, be sure to update the CSS with correct width and heights.
Frame dimensions guide

Frame dimensions guide.

Sample HTML:

<div id="counter" class="flip-counter"></div>

That's all you need for a counter. The ID can be whatever you'd like, but every counter needs the class "flip-counter", because that is what the CSS uses to style the counters.

Sample JavaScript:

var myCounter = new flipCounter("counter", {inc: 23, pace: 500});

The above code creates a counter inside the div with the ID "counter", starting from 0 (default), incrementing by 23, every 500ms, automatically (default).

Methods

Now that we have an easier way to set up the counter, there needed to be easy ways to manipulate it, so I created the following methods:

setValue(val)
Sets the value of the counter and animates the digits to new value. You can change the value of the counter we just created like so: myCounter.setValue(12345);
val [int]: New value for counter.
setIncrement(inc)
Sets the increment of the counter without animating any digits. Example: myCounter.setIncrement(123);
inc [int]: New increment value.
setPace(num)
Sets the pace of the counter. Only visibly affects counters with auto-increment set to true. Example: myCounter.setPace(800);
pace [int]: New pace value in milliseconds.
setAuto(auto)
Sets counter to auto-incrememnt (true) or not (false). Example: myCounter.setAuto(false);
auto [bool]: True or false.
step()
Manually step through increments when auto-increment is off. Example: myCounter.step();
add(num)
Adds a number to the counter value, not affecting the 'inc' or 'pace' of the counter. Example: myCounter.add(456);
num [int]: Number to add to counter.
subtract(num)
Subtracts a number from the counter value, not affecting the 'inc' or 'pace' of the counter. Note: The counter does not handle negative numbers. Any subtraction operation that results in a negative number will return zero. Example: myCounter.subtract(456);
num [int]: Number to subtract from counter.
getValue() *new*
Returns the current value of the counter. Example: var value = myCounter.getValue();
This method is not chainable.
stop() *new*
Stops the counter if it is in the process of incrementing to a value using either incrementTo or smartIncrementTo, which are explained below. Example: myCounter.stop();
incrementTo(num, time, pace) *new*
Increments to a given number either by using the currently set pace and inc values, or by using a "smart" increment technique that determines optimal values for you.
To use the simple technique, all you need to supply is the target number: myCounter.incrementTo(12345); This will increment to 12,345 using the current pace and inc values.
To use the smart method, supply the time in seconds you want the increment process to last, and a desired pace: myCounter.incrementTo(12345, 10, 400); This will increment to 12,345 as well, but the increment process will take 10 seconds to complete. The counter will determine the optimal values to use. In the example I've set a desired pace of 400, which the counter will try and stay as close to as possible when finding the optimal values. This method is not chainable.
num [int]: Number to increment to.
time [int] (optional): Duration, in seconds, the increment process should take. When set, the counter will use a smart increment technique to optimize the animation.
pace [int] (optional): Desired pace for animation when using the smart increment option. This number may increase as needed for optimal timing.

Chaining

Most methods are chainable, unless specifically mentioned above, so the following example works fine:

Demo

I've created a demo page with the help of jQueryUI that lets you manipulate the counter using all of the built-in methods. Go play with it:

Get the code

You can get everything you need here at Bitbucket, where you will always be able to find the latest version of the script. If you use the script for something cool, I'd love to hear about it.

Here is a transparent PSD provided by a kind soul from the comments. It is smaller than the sprite used for the demo, so you'll have to adjust the dimensions as I describe above.

Update: 11/27/10

  • Added 4 new methods: getValue, stop, incrementTo (thanksnerdynick), smartIncrementTo.
  • Other various bugfixes.

Update: 12/5/10

Update: 2/7/11

Updated to work with jQuery 1.5, older versions will not work with v1.5. Animation technique changed.

Update: 4/13/11 – v0.5

I finally added a version number to keep track of changes. Major changes:

  • Removed jQuery dependency, the counter runs on plain JavaScript now. I'm still using jQuery and jQueryUI for the demo controls, but it is no longer required for the counter itself.
  • Added frame dimensions to configuration options, which will make it easier to use custom graphics (not sure why I didn't have it this way before).

Update: 4/19/11 – v0.5.2

  • Combined top and bottom sprites into one long sprite.
  • Added bOffset parameter, explained in the configuration options.

Update: 5/7/11 – v0.5.3

  • Changed default counter ID to "flip-counter".
  • All counters now need a class of "flip-counter". This is what the CSS uses for styling, so having multiple counters doesn't require adding each new ID to the CSS.
  • idPre parameter removed because was pointless. Each counter needs a unique ID to begin with, so that can be used as the prefix for the digits.
  • Comma now added to digits sprite, so counter only uses one image.

CSS Experiment

Here's a peek at my progress with removing images from the counter entirely.