Animating a counter with Angular — Using requestAnimationFrame()
Working on my last project, I had to come up with a series of four animated counters and I just couldn’t resolve to import another *.js to my project for such a simple feature.
This article will detail how I built the component and explain the challenges I overcame.
Using requestAnimationFrame()
Since I had created something similar in the past, I already knew setTimeout() and setInterval() syntax can be messy and hard to maintain. The result is also flickering and missing some frames. Find out why.
Knowing a little about the requestAnimationFrame() method frame, I set out to discover more about it. I have to say I am highly satisfied with the result, but also with the code cleanliness.
“The window.requestAnimationFrame() method tells the browser that you wish to perform an animation and requests that the browser calls a specified function to update an animation before the next repaint. […]
You should call this method whenever you’re ready to update your animation onscreen. This will request that your animation function be called before the browser performs the next repaint. The number of callbacks is usually 60 times per second, but will generally match the display refresh rate in most web browsers as per W3C recommendation.”
Creating a reusable function
My task is to create four animated counters, so my first concern is to create a reusable function. I need four parameters: the HTML element, a starting value, an ending value, and a duration.
Using requestAnimationFrame, I have to call the method every time I need a new frame: this is why I declare a “step” as a function to do so. Inside it, I set a timestamp, compare it to the progress made so far, and set the value in the HTML. Finally, if the progress has not reached 100%, I use the callback parameter to run the animation again.
Once this function is created, the hard work is done. It is pretty easy to call it and use it as needed. All I have to do is call each counter using @ViewChild. So here is the full app.component.ts file:
export class AppComponent implements AfterViewInit { title = 'Counter animation with angular'; cAnimated: boolean = false; dAnimated: boolean = false; // Accessing DOM elements with ViewChild @ViewChild('a') a: any; @ViewChild('b') b: any; @ViewChild('c') c: any; @ViewChild('d') d: any; constructor(private render: Renderer2) {} // Counter animation function animateValue(obj, start, end, duration) { let startTimestamp = null; const step = timestamp => { // Set the actual time if (!startTimestamp) startTimestamp = timestamp; // Calculate progress (the time versus the set duration) const progress = Math.min((timestamp - startTimestamp) / duration, 1); // Calculate the value compared to the progress and set the value in the HTML obj.nativeElement.innerHTML = Math.floor(progress * (end - start) + start); // If progress is not 100%, an call a new animation of step if (progress < 1) window.requestAnimationFrame(step) }; // Call a last animation of step window.requestAnimationFrame(step); }}
Creating a scrolling event
Since the animations are not showing at the top of the page, I had to create a scrolling event that would allow the animation to start, only once, when the element is fully in the viewport, no matter which size it is.
To check if the element is fully in the viewport I used the getBoundingClientRect() method and compared it to the innerHeight property of the window object. To know more.
To make sure the animation runs only once, I declared an elementAnimated Boolean property and set it to true, once the animation is started so it would not start every time the event is triggered.
// ngAfterViewInit is called after the view is initially rendered. @ViewChild() depends on it. You can't access view members before they are rendered. See the paragraph below.ngAfterViewInit() { // Calling the first two animations this.animateValue(this.a, 0, 2021, 1500); this.animateValue(this.b, 0, 16, 1500); // Create a scrolling event using Renderer2 this.render.listen('window', 'scroll', () => { // Get element c position let cPosition = this.c.nativeElement.getBoundingClientRect(); // Compare it with the height of the window if (cPosition.top >= 0 && cPosition.bottom <= window.innerHeight) { // if it has not been animated yet, animate c if (this.cAnimated == false) { this.animateValue(this.c, 0, 2300, 1500); // prevent animation from running again this.cAnimated = true; } } // Get element d position let dPosition = this.d.nativeElement.getBoundingClientRect(); // Compare it with the height of the window if (dPosition.top >= 0 && dPosition.bottom <= window.innerHeight) { // if it has not been animated yet, animate d if (this.dAnimated == false) { this.animateValue(this.d, 0, 3, 1500); // prevent animation from running again this.dAnimated = true; }}
Hopefully, this has helped you in your path to create a smooth lighting fast animated counter. Cheers!