State machines для анимаций

Анимации в интерфейсах редко существуют как одиночные переходы от точки A к точке B. Чаще они представляют собой систему режимов: «закрыто», «открыто», «наведение», «активно», «перетаскивание», «возврат». Именно это делает конечные автоматы (state machines) естественным инструментом для управления движением.

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

Формализация состояния анимации

Любая анимационная система на основе конечного автомата опирается на три элемента:

  • набор состояний
  • набор событий
  • переходы между состояниями

Минимальная модель:

  • idle — покой
  • entering — появление
  • active — активное состояние
  • leaving — исчезновение

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

Пример структуры:

const machine = {
  initial: 'idle',
  states: {
    idle: {
      on: {
        MOUNT: 'entering'
      }
    },
    entering: {
      on: {
        DONE: 'active'
      }
    },
    active: {
      on: {
        UNMOUNT: 'leaving'
      }
    },
    leaving: {
      on: {
        DONE: 'idle'
      }
    }
  }
};

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

Popmotion и событийная модель движения

Popmotion предоставляет низкоуровневые примитивы движения:

  • tween — интерполяция значений
  • spring — физически корректное движение
  • physics — движение с силами
  • styler — привязка к DOM

State machine не заменяет их, а управляет их запуском.

Суть интеграции: состояние определяет, какой motion-процесс активен.

Связка состояний и tween-анимации

Рассмотрим переход «появление элемента».

import { tween, styler } from 'popmotion';

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

function enter() {
  return tween({
    from: { opacity: 0, y: 50 },
    to: { opacity: 1, y: 0 },
    duration: 300
  }).start(v => boxStyler.set(v));
}

Теперь добавляется состояние:

let state = 'idle';

function send(event) {
  switch (state) {
    case 'idle':
      if (event === 'MOUNT') {
        state = 'entering';
        enter().then(() => {
          state = 'active';
        });
      }
      break;
  }
}

Проблема такого подхода — рост сложности при увеличении числа переходов.

Декларативная модель переходов

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

Добавляется универсальный диспетчер:

function createMachine(config) {
  let current = config.initial;

  function send(event) {
    const nextState = config.states[current].on?.[event];

    if (nextState) {
      current = nextState;
      config.actions?.[current]?.(send);
    }
  }

  return { send };
}

Теперь анимации становятся реакцией на вход в состояние.

Привязка Popmotion к состояниям

Каждое состояние может запускать свою анимацию.

const machine = createMachine({
  initial: 'idle',
  states: {
    idle: {
      on: {
        MOUNT: 'entering'
      }
    },

    entering: {
      on: {
        DONE: 'active'
      }
    },

    active: {
      on: {
        UNMOUNT: 'leaving'
      }
    }
  },

  actions: {
    entering(send) {
      tween({
        from: 0,
        to: 1,
        duration: 400
      }).start({
        update: v => boxStyler.set({ opacity: v }),
        complete: () => send('DONE')
      });
    },

    leaving(send) {
      tween({
        from: 1,
        to: 0,
        duration: 250
      }).start({
        update: v => boxStyler.set({ opacity: v }),
        complete: () => send('DONE')
      });
    }
  }
});

Важное свойство этой модели — отсутствие внешнего контроля над анимацией. Состояние само определяет жизненный цикл движения.

Прерывание и конкурентные переходы

В UI часто возникает ситуация: новое событие приходит до завершения текущей анимации.

Например:

  • элемент начал появляться
  • пользователь мгновенно инициировал скрытие

Без state machine возникает конфликт tween-процессов.

Решение — хранение ссылки на активную анимацию:

let currentAnimation = null;

function safeStart(animation, send) {
  if (currentAnimation) currentAnimation.stop();

  currentAnimation = animation.start({
    complete: () => send('DONE')
  });
}

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

Иерархические состояния

В сложных интерфейсах одного уровня недостаточно.

Пример:

  • modal

    • closed
    • opening
    • open
    • closing
  • внутри modal:

    • content-loading
    • content-ready

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

const machine = {
  initial: 'closed',
  states: {
    closed: {
      on: { OPEN: 'opening' }
    },
    opening: {
      on: { DONE: 'open' }
    },
    open: {
      on: {
        CLOSE: 'closing',
        LOAD: 'loading'
      },
      states: {
        loading: {
          on: { READY: 'ready' }
        },
        ready: {}
      }
    }
  }
};

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

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

State machine особенно полезна при координации нескольких Popmotion-потоков.

Пример: карточка одновременно изменяет:

  • opacity
  • scale
  • position
  • shadow
function enter() {
  tween({ from: 0, to: 1, duration: 300 }).start(v =>
    boxStyler.set({
      opacity: v,
      scale: 0.8 + v * 0.2,
      y: 30 * (1 - v)
    })
  );
}

При добавлении состояния появляется гарантия согласованного старта всех эффектов:

actions: {
  entering(send) {
    const anim = tween({
      from: 0,
      to: 1,
      duration: 400
    });

    anim.start({
      update: v => boxStyler.set({
        opacity: v,
        scale: 0.9 + v * 0.1,
        y: 20 * (1 - v)
      }),
      complete: () => send('DONE')
    });
  }
}

Все параметры движения теперь зависят от одного источника времени.

Детеминизм переходов

Ключевое свойство state machines — детерминированность.

Для одного состояния и события всегда существует один результат.

Это особенно важно в анимациях:

  • исключаются гонки tween-процессов
  • устраняются случайные пересечения эффектов
  • упрощается отладка поведения

Popmotion здесь выступает как «движок исполнения», а state machine — как «контроллер логики».

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

Состояния часто привязываются к UI-событиям:

  • pointerdown → dragging
  • pointerup → settling
  • scroll → parallax

Пример:

element.addEventListener('pointerdown', () => send('DRAG_START'));
element.addEventListener('pointerup', () => send('DRAG_END'));

Внутри состояний запускается физическая анимация:

dragging: {
  on: {
    DRAG_END: 'settling'
  },
  actions: () => {
    physics({
      from: currentPosition,
      velocity: currentVelocity,
      friction: 0.8
    }).start(v => boxStyler.set({ x: v }));
  }
}

Разделение ответственности

State machine разделяет систему на уровни:

  • события (input layer)
  • состояния (logic layer)
  • Popmotion (motion layer)
  • DOM (render layer)

Такой разрез устраняет смешение обязанностей, которое обычно возникает в ручных анимациях.

Масштабирование сложных интерфейсов

При увеличении числа анимационных сценариев без state machine возникает экспоненциальный рост сложности:

  • больше условий
  • больше зависимостей между tween’ами
  • больше точек рассинхронизации

С конечным автоматом рост линейный: добавляется только новое состояние и его переходы.

Композиция машин

Несколько state machines могут работать совместно.

Пример:

  • машина модального окна
  • машина кнопок внутри
  • машина загрузки контента

Каждая управляет своей областью, не вмешиваясь в остальные.

const modalMachine = createMachine(...);
const contentMachine = createMachine(...);

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

Принцип однонаправленного потока управления

State machine формирует строгую модель:

событие → состояние → эффект → новое состояние

Popmotion выполняет только «эффект», не принимая решений о переходах.

Такой подход делает анимационную систему предсказуемой даже при высокой плотности взаимодействий и сложной композиции интерфейса