Работа с анимациями в браузере почти всегда упирается в положение элемента относительно видимой области экрана. Viewport выступает не статичной рамкой, а изменяемым состоянием: прокрутка, изменение размера окна, ориентация устройства постоянно сдвигают координатную систему, в которой интерпретируются все визуальные эффекты.
В Velocity.js управление анимацией строится вокруг явных вызовов
Velocity(element, properties, options), но поведение этих
анимаций часто дополняется внешней логикой, завязанной на 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 в этом случае выполняет роль исполнителя, а вычисление прогресса — внешней управляющей системы.
Типичный паттерн — запуск анимации при достижении элементом видимой области.
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 напрямую.
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, тем быстрее происходит анимация.
Хотя современный стандарт предлагает
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-зависимые анимации требуют пересчёта состояния.
function recalc() {
document.querySelectorAll(".block").forEach(el => {
Velocity(el, "stop");
});
}
window.addEventListener("resize", recalc);
window.addEventListener("orientationchange", recalc);
Дополнительно часто пересчитываются кэшированные значения высоты и порогов.
Viewport-зависимые эффекты легко становятся узким местом при частом scroll-событии. Основные техники оптимизации:
requestAnimationFramelet ticking = false;
window.addEventListener("scroll", () => {
if (!ticking) {
requestAnimationFrame(() => {
updateAnimations();
ticking = false;
});
ticking = true;
}
});
Velocity.js в таких системах работает только в момент применения результата, а не на каждом событии scroll.
Чёткая архитектура viewport-зависимых систем строится на разделении:
Такое разделение позволяет избегать смешивания состояния DOM и временной логики, сохраняя предсказуемость поведения анимаций даже при высокой частоте изменений viewport.