Prefers-reduced-motion

В интерфейсах, построенных на Popmotion, анимация часто является частью вычислительной модели: значения изменяются во времени через tween, spring, physics, animate. При включённой системной настройке prefers-reduced-motion вся эта модель требует адаптации — не просто отключения эффектов, а перераспределения динамики поведения.


Определение предпочтений системы

Базовый механизм опирается на media query:

const prefersReducedMotion = window.matchMedia(
  '(prefers-reduced-motion: reduce)'
).matches;

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


Абстракция конфигурации анимаций

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

const motionConfig = {
  reduced: {
    duration: 0,
    spring: {
      stiffness: 2000,
      damping: 100
    },
    distanceFactor: 0.2
  },
  full: {
    duration: 300,
    spring: {
      stiffness: 500,
      damping: 30
    },
    distanceFactor: 1
  }
};

Выбор режима:

const config = prefersReducedMotion
  ? motionConfig.reduced
  : motionConfig.full;

Адаптация tween-анимаций

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

import { tween } from 'popmotion';

function safeTween(props) {
  if (prefersReducedMotion) {
    return tween({
      from: props.from,
      to: props.to,
      duration: 0
    });
  }

  return tween({
    ...props,
    duration: config.duration
  });
}

Такой подход сохраняет API неизменным, изменяя только поведение времени.


Spring-анимации и подавление избыточной инерции

Физическая модель spring наиболее чувствительна к восприятию движения. В режиме reduced motion корректируется не только длительность, но и сама физика системы.

import { spring } from 'popmotion';

function safeSpring(props) {
  if (prefersReducedMotion) {
    return spring({
      from: props.from,
      to: props.to,
      stiffness: 3000,
      damping: 100
    });
  }

  return spring({
    ...props,
    stiffness: config.spring.stiffness,
    damping: config.spring.damping
  });
}

Высокая жёсткость и демпфирование фактически гасят колебания, превращая spring-переход в почти линейное приближение без инерционного хвоста.


Масштабирование амплитуды движения

Даже при сохранении анимации часто требуется уменьшение расстояния перемещения. Это особенно важно для x, y, scale, rotate.

Вводится универсальный коэффициент:

function reduceMotionValue(value) {
  return prefersReducedMotion
    ? value * config.distanceFactor
    : value;
}

Использование в анимации:

safeTween({
  from: { x: 0 },
  to: { x: reduceMotionValue(300) },
  duration: config.duration
});

Такое масштабирование позволяет сохранить структуру интерфейса без резких перемещений.


Полное отключение анимации через императивные значения

Popmotion допускает управление значениями напрямую через styler или value. В reduced-режиме часто применяется моментальная установка состояния.

import { styler } from 'popmotion';

const box = styler(document.querySelector('.box'));

function setPosition(x) {
  if (prefersReducedMotion) {
    box.set({ x });
    return;
  }

  safeSpring({ from: box.get('x'), to: x })
    .start(box.set);
}

Здесь анимационный поток полностью исключается, но API вызова остаётся единым.


Унификация через фабрику анимационных эффектов

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

function animateX(from, to) {
  return prefersReducedMotion
    ? tween({ from: { x: to }, to: { x: to }, duration: 0 })
    : spring({
        from: { x: from },
        to: { x: to },
        stiffness: config.spring.stiffness,
        damping: config.spring.damping
      });
}

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


Обработка цепочек анимаций

Popmotion позволяет строить последовательности через композицию chain, parallel и ручное управление .start().

В режиме reduced motion последовательность сворачивается в синхронное применение конечных значений.

function runSequence(steps, apply) {
  if (prefersReducedMotion) {
    const last = steps[steps.length - 1];
    apply(last.to);
    return;
  }

  steps.reduce((prev, step) => {
    return prev.start(() => {
      step.start(apply);
    });
  });
}

Интеграция с событиями DOM и реактивными потоками

При использовании Popmotion с событиями (listen, pointer tracking) поведение также должно адаптироваться. Часто движение курсора используется для параллакса или динамических смещений.

import { listen } from 'popmotion';

if (!prefersReducedMotion) {
  listen(document, 'mousemove')
    .start(({ clientX, clientY }) => {
      box.set({
        x: clientX * 0.05,
        y: clientY * 0.05
      });
    });
}

В reduced режиме поток событий может полностью игнорироваться или заменяться статическим состоянием.


Декларативная нормализация параметров

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

function normalizeAnimation(params) {
  if (prefersReducedMotion) {
    return {
      ...params,
      duration: 0,
      stiffness: 3000,
      damping: 100,
      velocity: 0
    };
  }

  return params;
}

Применение:

spring(normalizeAnimation({
  from: 0,
  to: 1,
  stiffness: 500,
  damping: 30
}));

Приоритет состояния системы над локальными настройками

В архитектуре Popmotion-ориентированных интерфейсов состояние prefers-reduced-motion рассматривается как внешний источник истины, перекрывающий локальные UI-флаги.

Даже если компонент запрашивает анимацию, финальное решение принимается на уровне адаптера:

function shouldAnimate(localFlag) {
  return !prefersReducedMotion && localFlag;
}

Разделение уровней движения

В сложных интерфейсах движение делится на категории:

  • декоративное (fade, slide, scale)
  • функциональное (перемещение модальных окон, изменения layout)
  • критическое (индикация состояния)

Для reduced motion сохраняется только критический уровень, остальные переводятся в мгновенные изменения или минимальные переходы без траектории.

function applyMotion(type, animation) {
  if (prefersReducedMotion && type === 'decorative') {
    animation.stop?.();
    return;
  }

  animation.start();
}

Контроль временных зависимостей

В Popmotion временная ось часто используется как ключевой параметр (elapsed, time, duration). В reduced режиме она либо обнуляется, либо фиксируется:

const adjustedDuration = prefersReducedMotion ? 0 : 400;

Для physics-анимаций:

const adjustedPhysics = prefersReducedMotion
  ? { velocity: 0, friction: 1 }
  : { velocity: 10, friction: 0.8 };

Итоговая модель адаптации Popmotion к reduced motion

Архитектурно система строится как слой трансформации над примитивами Popmotion. Вместо модификации каждой анимации по отдельности вводится единый контур:

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

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