Progress коллбэк

progress-callback в Velocity.js вызывается на каждом шаге анимации и предназначен для отслеживания текущего состояния выполнения tween-процесса в реальном времени. Он работает как низкоуровневый механизм наблюдения за прогрессом, позволяя синхронизировать внешние эффекты, интерфейсные изменения и дополнительные вычисления с ходом анимации.

Функция прогресса передаётся в виде опции объекта параметров:

Velocity(element, {
  opacity: 1,
  width: "100%"
}, {
  duration: 2000,
  progress: function(elements, percentComplete, timeRemaining, timeStart, tweenValue) {
    // логика отслеживания прогресса
  }
});

Параметры callback-функции:

  • elements — массив DOM-элементов, участвующих в текущей анимации
  • percentComplete — число от 0 до 1, отражающее долю завершения анимации
  • timeRemaining — оставшееся время до завершения (в миллисекундах)
  • timeStart — временная метка начала анимации
  • tweenValue — интерполированное значение текущего tween-шага (зависит от анимируемых свойств)

Механика вызова progress

progress вызывается на каждом кадре анимационного цикла, синхронно с обновлением значений свойств. Интервал зависит от requestAnimationFrame или fallback-таймеров в старых окружениях.

Каждый вызов отражает текущее состояние интерполяции между стартовым и конечным значением. Важно учитывать, что:

  • вызов происходит многократно до завершения анимации
  • нет гарантированной частоты (зависит от FPS)
  • значения могут немного варьироваться из-за easing-функций

percentComplete как основная метрика

Наиболее часто используемое значение — percentComplete. Оно нормализовано в диапазоне:

0.0 → начало анимации
1.0 → завершение анимации

Пример использования для синхронизации интерфейса:

Velocity(box, { width: "500px" }, {
  duration: 3000,
  progress: function(elements, percentComplete) {
    const percent = Math.round(percentComplete * 100);
    document.querySelector("#status").textContent = percent + "%";
  }
});

Использование timeRemaining

timeRemaining позволяет оценить оставшееся время анимации и использовать его для предиктивных эффектов:

Velocity(circle, { translateX: "300px" }, {
  duration: 4000,
  progress: function(elements, percentComplete, timeRemaining) {
    if (timeRemaining < 1000) {
      elements[0].style.opacity = percentComplete;
    }
  }
});

Практически применяется для:

  • плавного завершения визуальных эффектов
  • подготовки состояния UI перед завершением
  • адаптивного управления следующими анимациями

tweenValue и его роль

tweenValue отражает текущее интерполированное значение анимируемого свойства. Оно наиболее полезно при анимации числовых параметров или кастомных свойств.

Пример:

Velocity(box, { left: 600 }, {
  duration: 2000,
  progress: function(elements, percentComplete, timeRemaining, timeStart, tweenValue) {
    console.log(tweenValue);
  }
});

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

Применение progress для синхронизации UI

progress часто используется для построения связей между анимацией и интерфейсными элементами.

Прогресс-бар

Velocity(loader, { width: "100%" }, {
  duration: 5000,
  progress: function(elements, percentComplete) {
    document.querySelector(".bar").style.width = (percentComplete * 100) + "%";
  }
});

Анимация чисел

const counter = document.querySelector(".counter");

Velocity(counter, { dummy: 1 }, {
  duration: 3000,
  progress: function(elements, percentComplete) {
    const value = Math.floor(percentComplete * 1000);
    counter.textContent = value;
  }
});

Сравнение с complete callback

progress отличается от complete по характеру вызова:

  • progress — вызывается многократно во время анимации
  • complete — вызывается один раз по завершении

Типичный сценарий комбинирования:

Velocity(box, { opacity: 0 }, {
  duration: 1500,
  progress: function(elements, percentComplete) {
    elements[0].style.transform = `scale(${1 - percentComplete * 0.5})`;
  },
  complete: function(elements) {
    elements[0].style.display = "none";
  }
});

Влияние easing на progress

Хотя percentComplete формально линейно возрастает от 0 до 1, фактическое изменение визуальных значений зависит от easing-функции.

Это приводит к тому, что:

  • визуальное движение может быть нелинейным
  • progress остаётся линейной метрикой времени, а не визуального расстояния
  • tweenValue отражает easing-эффект

Производительность progress

progress может вызываться десятки раз в секунду, поэтому внутри callback недопустимы тяжёлые операции:

  • перерасчёт больших DOM-структур
  • частые reflow/repaint операции
  • сложные вычисления без мемоизации

Оптимальный подход:

let lastUpdate = 0;

Velocity(box, { left: 300 }, {
  duration: 3000,
  progress: function(elements, percentComplete, timeRemaining, timeStart) {
    const now = performance.now();
    if (now - lastUpdate < 50) return;

    lastUpdate = now;
    requestAnimationFrame(() => {
      elements[0].textContent = Math.round(percentComplete * 100);
    });
  }
});

Множественные элементы и progress

Если анимация применяется к группе элементов, progress вызывается для всей группы, а не для каждого элемента отдельно.

Velocity(document.querySelectorAll(".item"), { opacity: 0 }, {
  duration: 2000,
  progress: function(elements, percentComplete) {
    elements.forEach(el => {
      el.style.transform = `translateY(${percentComplete * 20}px)`;
    });
  }
});

В таких случаях важно учитывать, что синхронизация будет общей для всех элементов.

Использование progress в цепочках анимаций

progress может управлять переходом между этапами анимационной последовательности:

Velocity(box, { width: 500 }, {
  duration: 2000,
  progress: function(elements, percentComplete) {
    if (percentComplete > 0.5) {
      Velocity(elements[0], { backgroundColor: "#ff0000" }, { duration: 300 });
    }
  }
});

Подобные конструкции требуют осторожности, так как могут приводить к множественным триггерам анимаций.

Ограничения progress

  • отсутствие гарантированной частоты вызова
  • возможные пропуски кадров при нагрузке
  • зависимость от requestAnimationFrame
  • невозможность точного контроля момента вызова
  • потенциальная перегрузка при сложной логике внутри callback