Адаптация под размеры экрана

Базовый принцип адаптивной анимации

Анимации, завязанные на фиксированные пиксельные значения, теряют предсказуемость при изменении размеров экрана. Один и тот же переход может выглядеть корректно на десктопе и ломаться на мобильных устройствах из-за иной плотности контента, другого viewport и изменённой геометрии элементов.

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

Motion One предоставляет API, который позволяет комбинировать декларативные параметры анимации с динамическим пересчётом значений на основе состояния DOM.


Использование viewport-единиц вместо фиксированных значений

Первый уровень адаптации — переход от пикселей к viewport-единицам:

  • vw — ширина окна
  • vh — высота окна
  • % — относительные размеры контейнера
import { animate } from "motion";

animate(".box", {
  x: "50vw",
  opacity: [0, 1]
}, {
  duration: 0.8
});

Использование vw/vh позволяет избежать необходимости вручную пересчитывать значения при каждом изменении экрана. Однако viewport-единицы ограничены: они не учитывают локальную геометрию контейнера и особенности flex/grid раскладки.


Динамический расчёт значений через DOM-метрики

Для более точной адаптации используется измерение элементов через getBoundingClientRect():

import { animate } from "motion";

const element = document.querySelector(".card");
const rect = element.getBoundingClientRect();

animate(element, {
  x: window.innerWidth - rect.right - 20
});

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

Типичные сценарии:

  • выезд элементов за границу контейнера
  • адаптивные slide-in эффекты
  • синхронизация с соседними блоками

ResizeObserver для реактивного пересчёта анимаций

Изменение размеров окна не единственный фактор: layout может меняться при:

  • изменении контента
  • переключении шрифтов
  • загрузке изображений
  • изменении flex/grid поведения

Для отслеживания таких изменений используется ResizeObserver:

import { animate } from "motion";

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

const updateAnimation = () => {
  const rect = box.getBoundingClientRect();

  animate(box, {
    x: rect.width * 0.5,
    y: rect.height * 0.2
  }, {
    duration: 0.5
  });
};

const observer = new ResizeObserver(updateAnimation);
observer.observe(box);

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


Адаптивные значения через функции

Motion One поддерживает передачу функций вместо фиксированных значений. Это позволяет вычислять параметры на лету:

import { animate } from "motion";

animate(".item", {
  x: (latest, element) => {
    return element.offsetWidth * 0.3;
  }
});

Функциональный подход даёт доступ к:

  • текущему DOM-элементу
  • промежуточному состоянию анимации
  • вычисляемым layout-параметрам

Это особенно полезно при построении интерфейсов с динамическими сетками.


Медиа-условия и условная логика анимаций

Адаптация часто требует изменения не только значений, но и самой логики анимации.

import { animate } from "motion";

const isMobile = window.matchMedia("(max-width: 768px)").matches;

animate(".panel", {
  x: isMobile ? 0 : 200,
  opacity: [0, 1]
}, {
  duration: isMobile ? 0.3 : 0.8
});

Более гибкий вариант — подписка на изменения media query:

const mq = window.matchMedia("(max-width: 768px)");

mq.addEventListener("change", (e) => {
  animate(".panel", {
    x: e.matches ? 0 : 200
  });
});

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


Адаптация timeline под разные разрешения

Timeline в Motion One чувствителен к длительности и задержкам, которые должны масштабироваться вместе с интерфейсом.

import { timeline } from "motion";

const isCompact = window.innerWidth < 600;

timeline([
  [".header", { opacity: [0, 1] }, { duration: isCompact ? 0.2 : 0.6 }],
  [".content", { y: [50, 0] }, { at: "-0.3" }]
]);

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


Привязка анимаций к scroll и адаптивные оффсеты

При использовании scroll-based эффектов важно учитывать разную высоту экранов.

import { animate, scroll } from "motion";

scroll(
  animate(".parallax", {
    y: ["0%", "-30%"]
  }),
  {
    offset: ["start end", "end start"]
  }
);

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

const isSmall = window.innerHeight < 700;

scroll(
  animate(".parallax", {
    y: ["0%", isSmall ? "-15%" : "-40%"]
  })
);

Масштабирование значений через коэффициенты

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

const scale = Math.min(window.innerWidth / 1200, 1);

animate(".modal", {
  scale: 0.8 + scale * 0.2,
  y: (1 - scale) * 40
});

Такой метод позволяет:

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

Синхронизация анимации с layout shift

Изменения layout (например, загрузка изображений) могут ломать уже запущенные анимации. Решение — пересчёт параметров при каждом значимом shift:

const images = document.querySelectorAll("img");

images.forEach(img => {
  img.addEventListener("load", () => {
    const rect = img.getBoundingClientRect();

    animate(img, {
      y: rect.height * 0.1
    });
  });
});

В интерфейсах с динамическим контентом это предотвращает накопление ошибок позиционирования.


Комбинация нескольких стратегий адаптации

На практике используется не один, а несколько слоёв адаптации:

  • viewport-единицы для базовых смещений
  • ResizeObserver для реактивности
  • media queries для смены логики
  • коэффициенты масштабирования для плавности
  • DOM-метрики для точности

Motion One в таком подходе выступает как исполнитель анимаций, тогда как вся адаптивная логика выносится на уровень вычисления параметров.


Переиспользуемые адаптивные функции

Структурирование адаптации в отдельные функции повышает предсказуемость поведения:

const getResponsiveX = (el) => {
  const width = el.offsetWidth;
  const scale = window.innerWidth / 1000;

  return width * 0.2 * scale;
};

animate(".card", {
  x: (latest, el) => getResponsiveX(el)
});

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