Viewport-зависимые параметры

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

В Velocity.js управление анимацией строится вокруг явных вызовов Velocity(element, properties, options), но поведение этих анимаций часто дополняется внешней логикой, завязанной на viewport. Именно эта связка превращает библиотеку из инструмента “проигрывания эффектов” в систему реактивных визуальных состояний.

Геометрия viewport и базовые измерения

Для любых viewport-зависимых эффектов используется несколько фундаментальных величин:

  • высота и ширина видимой области
  • текущая прокрутка страницы
  • координаты элемента относительно документа и viewport

Ключевые API браузера:

const viewportHeight = window.innerHeight;
const viewportWidth = window.innerWidth;

const scrollTop = window.scrollY || document.documentElement.scrollTop;

Положение элемента в viewport определяется через:

const rect = element.getBoundingClientRect();

const elementTop = rect.top;
const elementBottom = rect.bottom;

getBoundingClientRect() возвращает координаты относительно текущего viewport, что делает его базовой точкой для любых вычислений видимости.

Преобразование положения в прогресс анимации

Основной приём viewport-зависимых эффектов — перевод позиции элемента в нормализованное значение от 0 до 1.

function getVisibilityProgress(el) {
  const rect = el.getBoundingClientRect();
  const viewportHeight = window.innerHeight;

  const start = viewportHeight;
  const end = -rect.height;

  return (start - rect.top) / (start - end);
}

Такой прогресс используется для:

  • управления прозрачностью
  • смещения
  • масштабирования
  • синхронизации нескольких анимаций

Velocity.js в этом случае выполняет роль исполнителя, а вычисление прогресса — внешней управляющей системы.

Привязка Velocity.js к скроллу

Типичный паттерн — запуск анимации при достижении элементом видимой области.

function animateIn(el) {
  Velocity(el, {
    opacity: 1,
    translateY: 0
  }, {
    duration: 600,
    easing: "ease-out"
  });
}

function onScroll() {
  const elements = document.querySelectorAll(".block");

  elements.forEach(el => {
    const rect = el.getBoundingClientRect();

    if (rect.top < window.innerHeight && rect.bottom > 0) {
      if (!el.dataset.animated) {
        el.dataset.animated = "true";
        animateIn(el);
      }
    }
  });
}

window.addEventListener("scroll", onScroll);

Здесь viewport становится триггером, а Velocity.js отвечает только за воспроизведение состояния.

Однократные и повторяемые триггеры

Viewport-параметры позволяют различать два типа поведения:

Однократный запуск

Элемент анимируется один раз при появлении:

if (rect.top < window.innerHeight * 0.9) {
  Velocity(el, "fadeIn");
}

Состояние фиксируется через dataset или класс.

Повторяемая анимация

Элемент реагирует на каждое пересечение viewport:

function isInViewport(el) {
  const rect = el.getBoundingClientRect();
  return rect.bottom > 0 && rect.top < window.innerHeight;
}

Velocity.js в таком случае может проигрывать анимацию в обе стороны: появление и исчезновение.

Пороговые значения и зоны активации

Viewport редко используется как бинарная система “виден / не виден”. Чаще вводятся зоны:

  • нижняя активация (вход в экран)
  • центральная зона (основная анимация)
  • верхняя зона (выход)
const viewportHeight = window.innerHeight;

const enterZone = viewportHeight * 0.8;
const exitZone = viewportHeight * 0.2;

if (rect.top < enterZone && rect.bottom > 0) {
  Velocity(el, { opacity: 1 });
}

Такая модель позволяет строить плавные переходы между состояниями без резких скачков.

Синхронизация анимации с прокруткой

Более сложный подход — не запуск анимации, а её управление в реальном времени через scroll progress.

function update() {
  const rect = el.getBoundingClientRect();
  const progress = 1 - rect.top / window.innerHeight;

  const clamped = Math.max(0, Math.min(1, progress));

  Velocity(el, {
    translateY: (1 - clamped) * 100,
    opacity: clamped
  }, {
    duration: 0
  });
}

window.addEventListener("scroll", update);

Здесь Velocity.js используется как слой применения стилей, но без временной интерполяции — движение определяется viewport напрямую.

Parallax-эффекты через viewport

Viewport является естественной основой параллакса. Разные элементы получают разные коэффициенты реакции на скролл.

function parallax(el, speed) {
  const rect = el.getBoundingClientRect();
  const offset = rect.top * speed;

  Velocity(el, {
    translateY: offset
  }, { duration: 0 });
}

window.addEventListener("scroll", () => {
  document.querySelectorAll("[data-parallax]").forEach(el => {
    const speed = parseFloat(el.dataset.speed || "0.5");
    parallax(el, speed);
  });
});

Чем меньше speed, тем медленнее элемент реагирует на движение viewport.

Динамическая длительность и задержки

Viewport может определять не только старт анимации, но и её параметры.

function getDuration(el) {
  const rect = el.getBoundingClientRect();
  const distance = Math.abs(rect.top - window.innerHeight / 2);

  return Math.max(200, 1200 - distance);
}

Velocity(el, {
  opacity: 1,
  translateY: 0
}, {
  duration: getDuration(el)
});

Чем ближе элемент к центру viewport, тем быстрее происходит анимация.

Intersection-подобная логика без IntersectionObserver

Хотя современный стандарт предлагает IntersectionObserver, многие системы на Velocity.js используют ручные вычисления viewport для большего контроля.

function isVisible(el, threshold = 0.1) {
  const rect = el.getBoundingClientRect();
  const visibleHeight = Math.min(rect.bottom, window.innerHeight) -
                        Math.max(rect.top, 0);

  const ratio = visibleHeight / rect.height;

  return ratio > threshold;
}

Это позволяет строить более сложные модели частичной видимости.

Адаптация под изменение viewport

Изменение размеров окна полностью меняет систему координат. Любые viewport-зависимые анимации требуют пересчёта состояния.

function recalc() {
  document.querySelectorAll(".block").forEach(el => {
    Velocity(el, "stop");
  });
}

window.addEventListener("resize", recalc);
window.addEventListener("orientationchange", recalc);

Дополнительно часто пересчитываются кэшированные значения высоты и порогов.

Оптимизация вычислений

Viewport-зависимые эффекты легко становятся узким местом при частом scroll-событии. Основные техники оптимизации:

  • использование requestAnimationFrame
  • throttle обработчиков
  • минимизация DOM-запросов
let ticking = false;

window.addEventListener("scroll", () => {
  if (!ticking) {
    requestAnimationFrame(() => {
      updateAnimations();
      ticking = false;
    });

    ticking = true;
  }
});

Velocity.js в таких системах работает только в момент применения результата, а не на каждом событии scroll.

Разделение ответственности: viewport vs animation engine

Чёткая архитектура viewport-зависимых систем строится на разделении:

  • viewport слой: вычисления, геометрия, прогресс
  • logic слой: правила активации
  • Velocity.js: исполнение анимации

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