Introduction:

Recently, I had the urge to do something cool with my engineering skills. For some reason, I always found those widgets - where digits roll upward - fascinating, which is why I had to build one myself. Now, whenever my website requires a number input, I can make people feel like they are Neo from the Matrix.

I started by designing a naive counter animation, which was fine, but it felt like something was missing, and I spent the next three days designing an animation that would feel much more refined. What I got is a counter animation that is great for the following things:

  1. Updating numbers reactively as the user types.
  2. Long running animations with many digits.
  3. Showing off to your friends.

If I am completely honest, the effect is only mildly better than the naive approach, but it is still unique enough to show, and I used some animation techniques which are fundamental but powerful - I learned plenty, and I hope you will also. Besides, I give you some buttons that you can click to watch numbers go up.

Without further ado, take a look at the following counter:

Naive Counter:
0000

Naive Counter Tween is defined as:

  • tween(t) := displayNumber(t * 9999)

Or more generally - a Naive Counter with from and to params:

  • tween(t) := displayNumber(lerp(from, to, t))
A Note About Tweens and Lerps

When creating an animation, the basic unit used is a Tween. For example - if you want to move a box:

  1. Box Starting Position: 0
  2. Box Ending Position: 100
  3. The tween will change values of box position - from 0 to 100.

An animation engine receives a duration and a tween, when it starts the animation it feeds the tween with values in range [0 -> 1] and the tween creates the desired animation.
For example:

  1. You Tell the Engine:
    • Animation Duration - 2 Seconds.
    • Animation tween - moveBoxTween(t)
  2. The Animation Engine Starts the 2 Seconds of Animation
    • at time = 0.0s -> moveBoxTween(0)
    • at time = 0.5s -> moveBoxTween(0.25)
    • at time = 1.0s -> moveBoxTween(0.5)
    • at time = 1.5s -> moveBoxTween(0.75)
    • at time = 2.0s -> moveBoxTween(1.0)

All the tween needs to do is to define a behavior for the number in the range [0 -> 1]. Ie:

  • define moveBoxTween(t) := box.setPosition(t * 100)
  • The above definition moves the box from position 0 to position 100 over the duration of the animation.
    • Time 0.0s -> box.setPosition(0.0 * 100) -> Box Position 0
    • Time 0.5s -> box.setPosition(0.25 * 100) -> Box Position 25
    • Time 1.0s -> box.setPosition(0.5 * 100) -> Box Position 50
    • Time 1.5s -> box.setPosition(0.75 * 100) -> Box Position 75
    • Time 2.0s -> box.setPosition(1.0 * 100) -> Box Position 100

A lerp is a simple function which maps a number t in range [0 -> 1] to number output in chosen range [from -> to].

  • lerp ranges :: [0 -> 1] => [from -> to]
  • define lerp(from, to, t) := return from + t * (to - from)
    • Ie: lerp(100, 200, 0.5) = 100 + 0.5 * (200 - 100) = 150

Now take a look at the following animation, containing a custom algorithm I spent more time to make than I would like to admit:

Overengineered Counter:
0000

The above animation lasts also 2 seconds, but the effect is a bit different. Did you notice the difference?
For convenience, I present you the two effects next to each other:

Naive Counter:
0000
Overengineered Counter:
0000

I will proceed to list the differences between the two approaches and hopefully will give a good enough explanation that would allow you to understand the implementation details of the Overengineered Approach.

Naive Approach - "Simple Tween"

Naive Approach's main issue: Most of our animation consists of the four digit numbers running, and this is what my overengineered approach was aiming to fix.
This can be illustrated via the breakdown below.

Naive Animation Breakdown:

  • numbers [0 -> 9] take 2ms - 0.1% of Total Animation
  • numbers [10 -> 99] take 18ms - 0.9% of Total Animation
  • numbers [100 -> 999] take 180ms - 9% of Total Animation
  • numbers [1000 -> 9999] take 1800ms - 90% of Total Animation

Further breakdown here:

Naive Approach Detailed Breakdown

Parameters:

  • Total Duration - 2 Seconds.
  • In [0 -> 9999] Total Numbers = 10000 Numbers
  • Per Number - 0.2ms screen time.
    • Calculation - 0.2 ms/number * 10000 numbers = 2 seconds total animation duration

Tweens Breakdown:

  • tween(0.00) = 0000
  • tween(0.01) ≈ 0100
  • tween(0.10) ≈ 1000
  • tween(0.20) ≈ 2000
  • tween(0.30) ≈ 3000
  • tween(0.40) ≈ 4000
  • tween(0.50) ≈ 5000
  • tween(0.60) ≈ 6000
  • tween(0.70) ≈ 7000
  • tween(0.80) ≈ 8000
  • tween(0.90) ≈ 9000
  • tween(1.00) = 9999

The breakdown above helps to understand why animating numbers naively feels lacking. From here on out I will try my best to give you a better understanding of the Overengineered Counter, what is it good for, technical details in cases you want to modify it, and some mental tools that are good for animations in general.

Overengineered Counter: Preface

I think the best way to start understanding the Overengineered Counter, is to actually list all its properties. Initially, some properties will be hard to justify, but hopefully, by the end of this blog post, you will understand why it works this way.

Property1: Animates All numbers in the range

All numbers in the range will be present in the animation - It is a real counter, and every frame it moves to a higher number.
(* with the caveat that there are enough frames in the timespan given. Ie: if there are 10000 numbers but only 5000 frames, then only half the numbers will display.)

Property2: Accelerates and Decelerates.

  • Has Two Steps, Acceleration and Deceleration.
  • Each Step has Stages (Stages are based on number of digits.)
  • NOTE: Each stage consists of all numbers in the range - Ie the triple digit stage will still count the single digits, just faster.
  1. Acceleration: Transitioning from lower Digit Number to Higher Digit Number Most Significant Digit.
    • Ie: if we animate [123 -> 9978] - so the acceleration phase would be [123 -> 9000]
    • Acceleration is done in stages:
      • [123 -> 130] - Single Digit Stage.
      • [130 -> 200] - Double digit Stage
      • [200 -> 1000] - Triple Digit Stage
      • [1000 -> 9000] - Quad Digit Stage
  2. Deceleration: After Most significant Digit is reached - starts "transitioning down".
    • Ie: if we animate [123 -> 9978] - so the deceleration phase would be [9000 -> 9978]
    • Down transition Stages:
      • [9000 -> 9900] - Triple Digit Stage
      • [9900 -> 9970] - Double Digit Stage
      • [9970 -> 9978] - Single Digit Stage

Property3: Each Digit Gets the same Counting Screen Time

This is hardest to explain in words - think of a guy named George who counts slowly 1, 2, 3 ..., until he reaches 10, then he starts counting 10, 20, 30, 40..., when he reaches 100, he counts 100, 200, 300... and so on. George stops counting when he gives up.

I mimic George in my overengineered animation, but I start counting from one number (Ie: 0) to another (Ie: 9999) and whenever the algorithm reaches to the next "break point" it starts counting faster. George was lazy and he counted 100, 200, 300... but I harness the power of modern microprocessors, I count very fast from 100 to 200, from 200 to 300, and so on. But counting from 100 to 200 will take the same time which it took to get from 10 to 20, and from 1 to 2.

Overengineered Counter - Technical Breakdown

This section is by far the most technical and it can be boring. It explains exactly how the animation is broken down, how is it achieved by the Multi Map Function.

Terminology:

  • Transition Unit - time taken for one count, a "tick".
    Ie: When George Counts -
    • 1 , 2 , 3 ... 9 , 10...
    • 10 , 20 , 30 ... 90 , 100...
    • 100 , 200 , 300 ... 900 , 1000...
    • Each step "," is a - Transition Unit - a set amount of time.
Transition Unit Details
  • Stage Unit - In Double Digit Stage the Stage Unit will be "10"; in Triple Digit stage - "100".
  • Transition Unit is a set amount of time calculated when the animation is defined.
  • Calculating Transition Unit is complex and the rest of the blog post aims to explain it.
  • For now, assume that we animate [123 -> 9978] over 1 second:
    • Transition Unit will be around ≈ 19ms
    • Example1: In Single Digit Stage - each number 123, 124, 125.. will be 19ms on the screen. Ie - number 123 takes 19ms screen time, number 124 takes another 19ms etc...
    • Example2: In Double Digit Stage - all the numbers in range [200 -> 210] will take 19ms and also numbers [210 -> 220] will take additional 19ms and so on...
    • Don't worry if you don't understand this section completely, I will give detailed breakdowns below.

MultiMap Introduction

Before explaining how the animation is taking place, I recommend reading the explanation about MultiMap in the note below.

A Note About MultiMap

To carry out complex lerping or mapping functionality I use a function called MultiMap.

A simple mapping function is of the form [a -> b] => [x -> y]
Meaning: "Given a value between a and b map it to a value between x and y". Ie:

  1. map 5 with [0 -> 10] => [100 -> 200] = 150
  2. map 2 with [0 -> 10] => [100 -> 200] = 120
  3. map 2 with [1 -> 3] => [100 -> 200] = 150
  4. map 10 with [10 -> 1000] => [-5 -> 10] = -5

A MultiMap function defines several exclusive ranges for input and for each of them an output range.
Ie: Define MultiMap Ranges as follows -

From=>To
[1 -> 3]=>[100 -> 200]
[10 -> 20]=>[100 -> 200]
[100 -> 200]=>[1000 -> 2000]
[-20 -> -10]=>[0 -> 10]

For the following inputs you will get the following results:

input=>Result
2=>150
3=>200
15=>150
19=>190
150=>1500
-15=>5
80=>Undefined

Explanation for input 2

  1. Sees that 2 is within range [1 -> 3]
  2. Calls for map(2, [1 -> 3], [100 -> 200])
  3. Map 2 from [1 -> 3] to [100 -> 200] = 150

For input -15 it selects the range map [-20 -> -10] => [0 -> 10] using the same process. Because 80 does not belong to any of the from ranges, it returns undefined.
Effectively - MultiMap allows us to define several mapping ranges for a single input

Putting it all together

In order to animate evenly across different ranges, I construct two MultiMap functions:

  1. Break down to transition ranges.
  2. Map t to transition number.
    • (I remind that t is a value in [0.0 -> 1.0] representing animation progress.)
  3. Map transition number to displayed value. (value = the animated number)

NOTE: We use TWO MultiMap functions, each MultiMap contains SEVERAL range to range mappings

  1. One MultiMap: t => Transition
  2. Second MultiMap: Transition => Number (the animated value)

Below a NOTE contains an example breakdown of [0 -> 9999] animation. The two tables within which map t to Transition and Transition to Number will be relevant for the next section.

[0 -> 9999] Transition Breakdown

Ascending Phase Breakdown:

[0 -> 10][10 -> 19][19 -> 28][28 -> 36]
00000001001001000
0001002002002000
0002003003003000
0003004004004000
0004005005005000
0005006006006000
0006007007007000
0007008008008000
000800900900(9000)
0009(0100)(01000)
(00010)

Descending Phase Breakdown:

[36 -> 45][45 -> 54][54 -> 63]
900099009990
910099109991
920099209992
930099309993
940099409994
950099509995
960099609996
970099709997
980099809998
(9900)(9990)9999
  • Each Arrow represents a transition, the columns represent the transition ranges for each phase.
    • Ie: In ranges [36 -> 45] I animate numbers [9000 -> 9900].
  • Note: Notice that numbers on the edges (ie 10, 100, 9000), are represented in both ranges (as well as the transitions 10, 28, 36...) this is to ensure the counting animation will apply to those ranges as well.

Parameters:

  1. Total Transition Ranges = 7
  2. Total Transitions = 63
  3. t Unit Per Transition = 1.0 / 63 ≈ 0.016
  4. Time per Transition = 2sec / 63 ≈ 32 ms per main transition number
    • ie: numbers starting with 9800 will be 32ms on the screen, as 9800 transitions to 9900.

Calculating T Range for Transitions Range

Assigning t range per Transition Range - In order to assign a range of t to a range of transitions, we simply count the number of transitions within a transitions range and multiply by t Unit Per Transition calculated above. ie:

  • Transition range [0 -> 10] consists of 10 transitions:
    • hence it will have a slice 10 * 0.016 = 0.16 total animation.
    • Results in mapping range t [0 -> 0.16] to Transitions [0 -> 10]
  • For range [10 -> 19] 9 transitions:
    • 9 * 0.016 = 0.144
    • Mapping range t [0.16 -> 0.304] to Transitions [10 -> 19].
  • And so forth...

Resulting MultiMap functions

MultiMap t => transition

t=>Transition
[0.0 -> 0.16]=>[0 -> 10]
[0.16 -> 0.304]=>[10 -> 19]
[0.304 -> 0.448]=>[19 -> 28]
[0.448 -> 0.592]=>[28 -> 36]
[0.592 -> 0.736]=>[36 -> 45]
[0.736 -> 0.88]=>[45 -> 54]
[0.88 -> 1.00]=>[54 -> 63]

Note: the above table contains rounding errors because I used 0.016 for calculation of transition time unit while it is a bit less. But I keep those numbers in the post for calculations and illustration, the real numbers are a bit different.

MultiMap transition => value:

Transition=>Value
[0 -> 10]=>[0 -> 10]
[10 -> 19]=>[10 -> 100]
[19 -> 28]=>[100 -> 1000]
[28 -> 36]=>[1000 -> 9000]
[36 -> 45]=>[9000 -> 9900]
[45 -> 54]=>[9900 -> 9990]
[54 -> 63]=>[9990 -> 9999]

Using the Calculation Above we get the two following MultiMap Functions:

MultiMap t => transition

t=>Transition
[0.0 -> 0.16]=>[0 -> 10]
[0.16 -> 0.304]=>[10 -> 19]
[0.304 -> 0.448]=>[19 -> 28]
[0.448 -> 0.592]=>[28 -> 36]
[0.592 -> 0.736]=>[36 -> 45]
[0.736 -> 0.88]=>[45 -> 54]
[0.88 -> 1.00]=>[54 -> 63]

MultiMap transition => Value:

Transition=>Value
Ascending
[0 -> 10]=>[0 -> 10]
[10 -> 19]=>[10 -> 100]
[19 -> 28]=>[100 -> 1000]
[28 -> 36]=>[1000 -> 9000]
Descending
[36 -> 45]=>[9000 -> 9900]
[45 -> 54]=>[9900 -> 9990]
[54 -> 63]=>[9990 -> 9999]

Now we have everything we need for animation. Examples below and the calculations for them follow that. For animation of 2s Example values:

  • Time = 0s ==> Value = 0
  • Time = 0.1s ==> Value = 3
  • Time = 0.4s ==> Value35
  • Time = 1.0s ==> Value3888
  • Time = 1.04s ==> Value4000
  • Time = 1.5s ==> Value ≈ 9908
  • Time = 1.75s ==> Value ≈ 9986
    • Note - this is at Descending Phase where the double digit numbers appear for 32ms while the single digits run fast.
  • Time = 1.9s ==> Value ≈ 9991
  • Time = 1.95s ==> Value ≈ 9995
  • Time = 1.99s ==> Value ≈ 9998
  • Time = 2.00s ==> Value ≈ 9999

Below I give some calculation examples of time -> value using the tables above. When a Range is chosen it is based on the Input, be it either t or transition.

Calculation Time=0.1
  • Time = 0.1s
  • t = 0.05 (= 0.1s / 2s)
  • transition = 12.5 (- map t = 0.05 from [0.0 -> 0.16] to [0 -> 10])
  • Value3 (- map 12.5 from [10 -> 19] to [10 -> 100])
Calculation Time=0.4
  • Time = 0.4s
  • t = 0.2 (= 0.4s / 2s)
  • transition = 12.5 (- map t = 0.2 from [0.16 -> 0.304] to [10 -> 19])
  • Value35 (- map 12.5 from [10 -> 19] to [10 -> 100])
Calculation Time=1.0
  • Time = 1.0s
  • t = 0.5 (= 1.0s / 2s)
  • transition = 30.88..9 (- map t = 0.5 from [0.448 -> 0.592] to [28 -> 36])
  • Value3888 (- map 12.5 from [28 -> 36] to [1000 -> 9000])
Calculation Time=1.04
  • Time = 1.04s
  • t = 0.502 (= 1.04s / 2s)
  • transition = 31.0 (- map t = 0.502 from [0.448 -> 0.592] to [28 -> 36])
  • Value4000 (- map 31.0 from [28 -> 36] to [1000 -> 9000])
Calculation Time=1.75
  • Time = 1.75s
  • t = 0.875 (= 1.75s / 2s)
  • transition = 53.6875 (- map t = 0.875 from [0.736 -> 0.88] to [45 -> 54])
  • Value ≈ 9986 (- map 53.6875 from [45 -> 54] to [9900 -> 9990])

Fun Section - Code and Playground

Custom Widget Allowing to Play With Parameters

NOTE - the Code for the below widget is not Included (find my email in the About section if you really need this code).
The reason I did not include the code is because it uses complex infrastructure for the animation.

Animated Counter Controls
Naive Counter
0000
Overengineered Counter
0000
Rerun:
Values:
Duration:
Defaults:

More Usage Examples

  1. Svelte Playground Code Sample - Here you can find a Minimal Code Sample with normal tween and Overengineered Tween. (The name of the tween in the example is animateNumberByDigits)
  2. Matrix Like UI - A Simple Interface Which Uses the Animation.
    NOTE This Example is best tinkered with on a Computer and not a Mobile Device.
    CODE: Located Here in this GitHub Repo