Работая над моим последним проектом, мне пришлось придумать серию из четырех анимированных счетчиков, и я просто не мог решиться импортировать еще один *.js в свой проект для такой простой функции.

В этой статье подробно рассказывается, как я создал компонент, и объясняются проблемы, которые я преодолел.

Проект GitHub

СтекБлиц

Использование запросаAnimationFrame()

Поскольку я уже создавал что-то подобное в прошлом, я уже знал, что синтаксис setTimeout() и setInterval() может быть запутанным и сложным в обслуживании. Результат также мерцает и пропускает некоторые кадры. Узнай почему.

Немного зная о фрейме метода requestAnimationFrame(), я решил узнать о нем больше. Должен сказать, что я очень доволен результатом, а также чистотой кода.

«Метод window.requestAnimationFrame() сообщает браузеру, что вы хотите выполнить анимацию, и запрашивает, чтобы браузер вызвал указанную функцию для обновления анимации перед следующей перерисовкой. […]

Этот метод следует вызывать всякий раз, когда вы готовы обновить анимацию на экране. Это потребует, чтобы ваша функция анимации была вызвана до того, как браузер выполнит следующую перерисовку. Количество обратных вызовов обычно составляет 60 раз в секунду, но обычно соответствует частоте обновления экрана в большинстве веб-браузеров в соответствии с рекомендацией W3C».

-МДН

Создание многократно используемой функции

Моя задача — создать четыре анимированных счетчика, поэтому моя первая задача — создать повторно используемую функцию. Мне нужны четыре параметра: элемент HTML, начальное значение, конечное значение и продолжительность.

Используя requestAnimationFrame, мне приходится вызывать метод каждый раз, когда мне нужен новый кадр: вот почему для этого я объявляю «шаг» как функцию. Внутри него я устанавливаю метку времени, сравниваю ее с прогрессом, достигнутым до сих пор, и устанавливаю значение в HTML. Наконец, если прогресс не достиг 100%, я использую параметр обратного вызова для повторного запуска анимации.

Как только эта функция создана, тяжелая работа сделана. Его довольно легко вызвать и использовать по мере необходимости. Все, что мне нужно сделать, это вызвать каждый счетчик с помощью @ViewChild. Итак, вот полный файл app.component.ts:

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);
   }
}

Создание события прокрутки

Поскольку анимация не отображается в верхней части страницы, мне пришлось создать событие прокрутки, которое позволяло бы запускать анимацию только один раз, когда элемент полностью находится в области просмотра, независимо от его размера.

Чтобы проверить, полностью ли элемент находится в области просмотра, я использовал метод getBoundingClientRect() и сравнил его со свойством innerHeight объекта окна. "Знать больше".

Чтобы убедиться, что анимация выполняется только один раз, я объявил логическое свойство elementAnimated и установил для него значение true после запуска анимации, чтобы она не запускалась каждый раз при запуске события.

// 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;
   }
}

Надеюсь, это помогло вам создать быстро анимированный счетчик с плавным освещением. Ваше здоровье!