Адаптивные анимации

В адаптивных интерфейсах анимации в mo.js должны подстраиваться под размер экрана, плотность пикселей и особенности устройства так же гибко, как и сам UI. Жёстко заданные координаты, фиксированные радиусы и неизменяемые тайминги приводят к визуальным ошибкам: элементы выходят за пределы экрана, траектории становятся непропорциональными, эффект теряет выразительность на мобильных устройствах.

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

const vw = window.innerWidth;
const vh = window.innerHeight;

const radius = Math.min(vw, vh) * 0.1;

const burst = new mojs.Burst({
  radius: { 0: radius * 2 },
  count: 10,
  children: {
    radius: radius / 4,
    fill: 'white'
  }
});

document.addEventListener('click', (e) => {
  burst
    .tune({ x: e.pageX, y: e.pageY })
    .replay();
});

Здесь ключевой принцип заключается в том, что геометрия эффекта зависит от viewport, а не от фиксированных чисел. Это позволяет сохранять пропорции на экранах разных размеров.

Пересчёт параметров при изменении размеров

Адаптивные анимации требуют постоянного пересчёта конфигурации при изменении размеров окна.

let vw = window.innerWidth;
let vh = window.innerHeight;

const updateMetrics = () => {
  vw = window.innerWidth;
  vh = window.innerHeight;
};

window.addEventListener('resize', updateMetrics);

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

Пропорциональные траектории движения

Линейные и криволинейные движения в mo.js часто задаются через абсолютные значения. Для адаптивности они должны быть выражены через относительные коэффициенты.

const shiftX = () => window.innerWidth * 0.2;
const shiftY = () => window.innerHeight * 0.15;

const tween = new mojs.Tween({
  duration: 800,
  onUpdate: (progress) => {
    const x = shiftX() * progress;
    const y = shiftY() * progress;

    element.style.transform = `translate(${x}px, ${y}px)`;
  }
});

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

Учет плотности пикселей (DPR)

На устройствах с высокой плотностью пикселей (Retina и аналоги) одинаковые значения радиусов и толщин выглядят визуально меньше. Корректировка через devicePixelRatio позволяет компенсировать этот эффект.

const dpr = window.devicePixelRatio || 1;

const circle = new mojs.Shape({
  radius: 50 * dpr,
  strokeWidth: 2 * dpr,
  fill: 'transparent',
  stroke: '#4dd0e1'
});

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

Адаптивные burst-эффекты

Burst-анимации особенно чувствительны к размерам экрана, так как количество частиц и их радиус должны соответствовать доступному пространству.

const computeCount = () => Math.floor(window.innerWidth / 50);

const burst = new mojs.Burst({
  count: computeCount(),
  radius: { 0: 120 },
  children: {
    shape: 'circle',
    radius: { 6: 0 },
    duration: 1000
  }
});

Число элементов увеличивается на больших экранах и уменьшается на малых, сохраняя визуальный баланс.

Динамическая пересборка анимаций

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

let burst;

const createBurst = () => {
  const radius = Math.min(window.innerWidth, window.innerHeight) * 0.15;

  burst = new mojs.Burst({
    radius: { 0: radius },
    count: 12,
    children: {
      radius: radius / 5,
      fill: '#ff4081'
    }
  });
};

createBurst();

window.addEventListener('resize', () => {
  createBurst();
});

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

Адаптация временных параметров

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

const baseDuration = 600;

const getDuration = () => {
  return window.innerWidth < 600 ? baseDuration * 0.7 : baseDuration;
};

const tween = new mojs.Tween({
  duration: getDuration(),
  easing: 'cubic.out'
});

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

Использование относительных координат

Абсолютные координаты быстро теряют актуальность в адаптивной среде. Более устойчивый подход — использование относительных позиций от центра или краёв экрана.

const centerX = () => window.innerWidth / 2;
const centerY = () => window.innerHeight / 2;

const shape = new mojs.Shape({
  x: centerX,
  y: centerY,
  radius: 40,
  fill: '#29b6f6'
});

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

Интеграция с layout-системами

В современных интерфейсах анимации часто зависят от DOM-структуры. Использование getBoundingClientRect позволяет синхронизировать mo.js с реальными элементами.

const button = document.querySelector('.button');

button.addEventListener('click', () => {
  const rect = button.getBoundingClientRect();

  const burst = new mojs.Burst({
    x: rect.left + rect.width / 2,
    y: rect.top + rect.height / 2,
    radius: { 0: rect.width },
    count: 8
  });

  burst.replay();
});

Такой подход обеспечивает точное позиционирование эффектов относительно интерфейсных компонентов.

Оптимизация адаптивных анимаций

Частые пересчёты параметров при resize могут стать узким местом производительности. Оптимизация достигается через throttling событий и кеширование вычислений.

let timeout;

window.addEventListener('resize', () => {
  clearTimeout(timeout);

  timeout = setTimeout(() => {
    updateMetrics();
  }, 150);
});

Дополнительно полезно избегать пересоздания анимаций без необходимости, ограничиваясь обновлением параметров через tune().

Контекстно-зависимые эффекты

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

const isMobile = () => window.innerWidth < 768;

const burstConfig = {
  count: isMobile() ? 6 : 14,
  radius: isMobile() ? 80 : 160,
  children: {
    radius: isMobile() ? 4 : 8
  }
};

const burst = new mojs.Burst(burstConfig);