Vue директивы

Базовая модель работы директив

Vue-директивы в контексте Motion One строятся как абстракция над функцией animate() и системой управления анимациями через motion-runtime. Директива становится связующим слоем между жизненным циклом DOM-элемента во Vue и imperative API анимации.

Ключевая идея заключается в том, что директива:

  • получает доступ к DOM-узлу через el
  • считывает входные параметры (binding.value, binding.modifiers)
  • инициирует анимацию через Motion One
  • синхронизирует обновления с реактивностью Vue

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


Базовая директива v-motion

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

Типовая структура обработки:

  • mounted — инициализация начального состояния
  • updated — реакция на изменение binding
  • unmounted — очистка анимации

Логика работы:

import { animate } from "motion";

export const vMotion = {
  mounted(el, binding) {
    const config = binding.value;

    if (config?.initial) {
      Object.assign(el.style, config.initial);
    }

    el._motion = animate(
      el,
      config.animate,
      config.options || {}
    );
  },

  updated(el, binding) {
    const config = binding.value;

    el._motion?.cancel();

    el._motion = animate(
      el,
      config.animate,
      config.options || {}
    );
  },

  unmounted(el) {
    el._motion?.cancel();
  }
};

Структура конфигурации директивы

v-motion="{
  initial: { opacity: 0, transform: 'translateY(20px)' },
  animate: { opacity: 1, transform: 'translateY(0px)' },
  options: { duration: 0.6, easing: 'ease-out' }
}"

Управление состояниями анимации

Директивный подход позволяет разделить анимацию на состояния, приближая его к state machine модели.

Типовые состояния:

  • initial — начальное состояние перед монтированием
  • enter — появление элемента
  • leave — исчезновение элемента
  • update — реакция на изменения данных

Реализация через watcher внутри директивы:

updated(el, binding) {
  const { state, variants } = binding.value;

  const target = variants[state];

  el._motion?.cancel();

  el._motion = animate(el, target, {
    duration: 0.4
  });
}

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

Variants позволяют задавать набор предопределённых анимационных сценариев.

v-motion="{
  state: 'active',
  variants: {
    inactive: { opacity: 0.3, scale: 0.98 },
    active: { opacity: 1, scale: 1 }
  }
}"

Особенности подхода:

  • упрощение управления сложными UI-состояниями
  • централизованная логика анимаций
  • отсутствие дублирования конфигураций

Интеграция с жизненным циклом Vue 3

Vue 3 Composition API влияет на архитектуру директив косвенно, через реактивные источники данных.

Типовой паттерн:

  • реактивное значение управляет состоянием
  • директива реагирует через updated
  • Motion One выполняет переход
watch(() => state.value, (newState) => {
  el._motion?.cancel();

  el._motion = animate(el, variants[newState]);
});

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


Transition-параметры и тонкая настройка

Motion One предоставляет богатую систему настройки переходов, которая в директивах передаётся через options.

Основные параметры:

  • duration — длительность
  • easing — функция сглаживания
  • delay — задержка
  • repeat — количество повторов

Пример:

v-motion="{
  animate: { x: 100 },
  options: {
    duration: 1,
    easing: 'ease-in-out',
    delay: 0.2
  }
}"

Динамическая настройка

Параметры могут зависеть от состояния приложения:

options: computed(() => ({
  duration: isFast.value ? 0.2 : 0.8
}))

Директивы входа и выхода (enter/leave)

В Vue часто требуется анимация появления и удаления элементов. Motion One позволяет реализовать это через директивные хуки.

v-motion="{
  enter: { opacity: 1, y: 0 },
  leave: { opacity: 0, y: -20 }
}"

Реализация:

mounted(el, binding) {
  animate(el, binding.value.enter);
},

unmounted(el, binding) {
  animate(el, binding.value.leave);
}

Особенность: анимация удаления требует задержки реального удаления DOM, что обычно решается через nextTick и управление v-if/v-show.


Scroll-анимации через директивы

Motion One поддерживает scroll-driven animation, что в Vue удобно оборачивается в директиву.

v-motion-scroll="{
  transform: {
    inputRange: [0, 1],
    outputRange: ['0px', '200px']
  }
}"

Логика:

  • отслеживание scroll event
  • вычисление прогресса (0–1)
  • применение interpolate через animate API
import { scroll } from "motion";

mounted(el, binding) {
  el._scroll = scroll(progress => {
    el.style.transform = `translateY(${progress * 200}px)`;
  });
}

Кастомные директивы на основе Motion One

Создание собственных директив позволяет расширять систему под конкретные UI-паттерны.

Пример: v-fade

export const vFade = {
  mounted(el) {
    animate(el, { opacity: [0, 1] }, { duration: 0.5 });
  }
};

Пример: v-slide-up

export const vSlideUp = {
  mounted(el) {
    animate(el, {
      opacity: [0, 1],
      transform: ["translateY(20px)", "translateY(0px)"]
    });
  }
};

Такие директивы формируют слой дизайн-системы, основанный на Motion One.


Комбинация директив и Composition API

Директивы могут взаимодействовать с composables, формируя гибридную архитектуру.

Пример composable:

export function useMotionState() {
  const state = ref("hidden");

  const toggle = () => {
    state.value = state.value === "hidden" ? "visible" : "hidden";
  };

  return { state, toggle };
}

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

watch(state, (s) => {
  animate(el, variants[s]);
});

Оптимизация анимаций

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

Основные стратегии:

1. Отмена предыдущих анимаций

el._motion?.cancel();

2. Батчинг обновлений

Объединение изменений состояния в один frame:

requestAnimationFrame(() => {
  animate(el, config);
});

3. Ограничение reflow

Минимизация изменения layout-свойств:

  • предпочтение transform и opacity
  • избегание top/left при анимации

4. Переиспользование конфигураций

const presets = {
  fastFade: { duration: 0.2 },
  slowFade: { duration: 0.8 }
};

Обработка ошибок и устойчивость

Директивы должны учитывать возможность отсутствия DOM или некорректных значений.

if (!el || !binding.value) return;

if (typeof binding.value !== "object") return;

Также важно защищать вызовы Motion One:

try {
  animate(el, config);
} catch (e) {
  console.warn("Motion error", e);
}