Media queries в анимациях

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

Браузерные медиазапросы через window.matchMedia представляют собой событийный API, который позволяет отслеживать изменения условий среды: ширину экрана, плотность пикселей, предпочтения пользователя.

В контексте анимаций медиазапрос перестаёт быть только инструментом CSS и превращается в триггер конфигурации поведения анимации.

const mq = window.matchMedia('(min-width: 900px)')

mq.addEventListener('change', (e) => {
  if (e.matches) {
    // десктопная конфигурация анимации
  } else {
    // мобильная конфигурация
  }
})

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

Интеграция matchMedia с Popmotion animate

Popmotion предоставляет функцию animate, которая управляет временем, физикой и интерполяцией значений. В связке с медиазапросами она используется как конечный исполнитель конфигурации, выбранной на основе состояния среды.

import { animate } from 'popmotion'

const mq = window.matchMedia('(min-width: 900px)')

function startAnimation(isDesktop) {
  animate({
    from: 0,
    to: isDesktop ? 300 : 100,
    duration: isDesktop ? 800 : 400,
    onUpdate: (latest) => {
      document.querySelector('.box').style.transform =
        `translateX(${latest}px)`
    }
  })
}

startAnimation(mq.matches)

mq.addEventListener('change', (e) => {
  startAnimation(e.matches)
})

Здесь медиазапрос управляет не самим движением, а параметрами движения: расстоянием, длительностью и характером анимации.

Переключение физических моделей в зависимости от устройства

Popmotion поддерживает физические модели (spring, inertia), которые особенно чувствительны к устройству и контексту использования. На мобильных устройствах часто требуется более мягкая динамика, на десктопе — более выраженная.

import { animate, spring } from 'popmotion'

const mq = window.matchMedia('(min-width: 900px)')

function createSpring() {
  return mq.matches
    ? { stiffness: 200, damping: 20 }
    : { stiffness: 120, damping: 18 }
}

animate({
  from: 0,
  to: 1,
  ease: spring(createSpring()),
  onUpdate: (v) => {
    document.querySelector('.card').style.transform =
      `scale(${1 + v * 0.1})`
  }
})

Физическая модель становится адаптивной, а не фиксированной. Это позволяет учитывать не только размеры экрана, но и характер взаимодействия.

Адаптация анимации под prefers-reduced-motion

Одним из наиболее значимых медиазапросов для анимаций является prefers-reduced-motion. Он отражает системное предпочтение пользователя минимизировать движение интерфейса.

Popmotion не требует специального режима для этого запроса — достаточно изменить параметры анимации или заменить её мгновенным состоянием.

const reducedMotion = window.matchMedia('(prefers-reduced-motion: reduce)')

function runAnimation() {
  if (reducedMotion.matches) {
    document.querySelector('.panel').style.opacity = 1
    document.querySelector('.panel').style.transform = 'none'
    return
  }

  animate({
    from: 0,
    to: 1,
    duration: 600,
    onUpdate: (v) => {
      const el = document.querySelector('.panel')
      el.style.opacity = v
      el.style.transform = `translateY(${20 * (1 - v)}px)`
    }
  })
}

runAnimation()

В этом сценарии медиазапрос полностью изменяет стратегию исполнения: вместо анимации применяется статическое состояние.

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

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

const mqDesktop = window.matchMedia('(min-width: 900px)')
const mqLandscape = window.matchMedia('(orientation: landscape)')
const mqReduced = window.matchMedia('(prefers-reduced-motion: reduce)')

function getConfig() {
  if (mqReduced.matches) {
    return { duration: 0 }
  }

  if (mqDesktop.matches && mqLandscape.matches) {
    return { duration: 900, distance: 300 }
  }

  return { duration: 500, distance: 150 }
}

function run() {
  const config = getConfig()

  animate({
    from: 0,
    to: config.distance,
    duration: config.duration,
    onUpdate: (v) => {
      document.querySelector('.item').style.transform =
        `translateX(${v}px)`
    }
  })
}

Так формируется декларативная модель конфигурации, где медиазапросы определяют параметры анимации, а Popmotion выполняет вычислительную часть.

Динамическое пересоздание анимаций при изменении среды

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

import { animate } from 'popmotion'

let currentAnimation = null

function start() {
  if (currentAnimation) currentAnimation.stop()

  const isWide = window.matchMedia('(min-width: 900px)').matches

  currentAnimation = animate({
    from: 0,
    to: isWide ? 400 : 200,
    duration: 700,
    onUpdate: (v) => {
      document.querySelector('.block').style.left = `${v}px`
    }
  })
}

start()

window.matchMedia('(min-width: 900px)')
  .addEventListener('change', start)

Здесь важен контроль предыдущего процесса: без остановки анимации возникает конфликт обновлений состояния.

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

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

const presets = {
  mobile: {
    duration: 300,
    distance: 120,
    easing: 'ease-out'
  },
  desktop: {
    duration: 700,
    distance: 320,
    easing: 'ease-in-out'
  }
}

function getPreset() {
  return window.matchMedia('(min-width: 900px)').matches
    ? presets.desktop
    : presets.mobile
}

animate({
  from: 0,
  to: getPreset().distance,
  duration: getPreset().duration,
  onUpdate: (v) => {
    document.querySelector('.el').style.transform =
      `translateX(${v}px)`
  }
})

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

Синхронизация нескольких анимаций через медиазапросы

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

function runGroup() {
  const isDesktop = window.matchMedia('(min-width: 900px)').matches

  animate({
    from: 0,
    to: isDesktop ? 200 : 80,
    onUpdate: (v) => {
      document.querySelector('.a').style.transform = `translateX(${v}px)`
      document.querySelector('.b').style.transform = `translateY(${v / 2}px)`
      document.querySelector('.c').style.opacity = v / 200
    }
  })
}

Единый источник конфигурации предотвращает рассинхронизацию анимационных компонентов.

Медиазапросы и производительность анимаций

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

const isLowPower = window.matchMedia('(max-width: 480px)').matches

animate({
  from: 0,
  to: 1,
  duration: isLowPower ? 200 : 800,
  onUpdate: (v) => {
    document.querySelector('.gpu').style.opacity = v
  }
})

В более сложных системах этот механизм расширяется до выбора между requestAnimationFrame-ориентированными и таймерными стратегиями исполнения.

Реактивная архитектура анимаций на основе медиазапросов

При масштабировании интерфейсов медиазапросы становятся частью архитектурного слоя, отвечающего за конфигурацию motion-системы. Popmotion в таком случае выступает исполнительным движком, а медиазапросы — слоем принятия решений.

const queries = {
  desktop: window.matchMedia('(min-width: 1200px)'),
  tablet: window.matchMedia('(min-width: 768px)')
}

function resolveMode() {
  if (queries.desktop.matches) return 'desktop'
  if (queries.tablet.matches) return 'tablet'
  return 'mobile'
}

function animateLayout() {
  const mode = resolveMode()

  const config = {
    desktop: { x: 400, duration: 900 },
    tablet: { x: 250, duration: 600 },
    mobile: { x: 120, duration: 350 }
  }[mode]

  animate({
    from: 0,
    to: config.x,
    duration: config.duration,
    onUpdate: (v) => {
      document.querySelector('.layout').style.transform =
        `translateX(${v}px)`
    }
  })
}

animateLayout()