Momentum scrolling

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

Ключевые параметры модели:

  • скорость (velocity) — начальный импульс после отпускания pointer
  • трение (friction) — коэффициент замедления
  • масса (mass) — влияет на инерцию и длительность движения
  • затухание (decay) — функция экспоненциального уменьшения скорости

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


Получение скорости жеста

Основой momentum scrolling является корректное измерение скорости pointer-события. Popmotion предоставляет инструменты отслеживания координат и вычисления производной по времени.

import { pointer } from 'popmotion';

const tracker = pointer({
  x: 0,
  y: 0
});

tracker.start((v) => {
  console.log(v.x, v.y);
});

Однако для инерции важно не само положение, а изменение позиции во времени. Popmotion внутри pointer уже рассчитывает velocity, что позволяет использовать его как источник импульса.


Перенос скорости в инерционное движение

После завершения взаимодействия (pointer up) полученная скорость передаётся в функцию затухания. В Popmotion для этого используется decay.

import { pointer, decay, animate } from 'popmotion';

let startX = 0;
let currentX = 0;

const track = pointer({
  x: startX
}).start((v) => {
  currentX = v.x;
});

Когда пользователь отпускает элемент, pointer останавливается, а накопленная скорость используется для продолжения движения.


Модель затухания через decay

decay реализует экспоненциальное замедление движения. Это ключевой механизм momentum scrolling.

const momentum = decay({
  from: currentX,
  velocity: track.x.velocity,
  power: 0.8,
  timeConstant: 350
}).start({
  update: (v) => {
    currentX = v;
  }
});

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

  • from — стартовая позиция
  • velocity — начальная скорость движения
  • timeConstant — скорость затухания (больше значение → дольше движение)
  • power — усиление начального импульса

Физически это приближение к экспоненциальному распаду скорости:

v(t) = v₀ * e^(-t / τ)


Связка pointer и decay в единую систему

Momentum scrolling требует переключения между двумя режимами: активным перетаскиванием и инерционным движением.

import { pointer, decay } from 'popmotion';

let animation;
let position = 0;

const drag = pointer({ x: position }).start((v) => {
  position = v.x;

  if (animation) animation.stop();
});

При завершении жеста запускается decay:

const startMomentum = (velocity) => {
  animation = decay({
    from: position,
    velocity,
    timeConstant: 300
  }).start((v) => {
    position = v;
  });
};

Захват момента отпускания pointer

Критический момент — фиксирование скорости в момент pointer up. Popmotion позволяет получить velocity из трекинга, либо вычислить его вручную.

let lastVelocity = 0;

const drag = pointer({ x: 0 }).start({
  update: (v) => {
    position = v.x;
  },
  velocity: (v) => {
    lastVelocity = v.x;
  },
  complete: () => {
    startMomentum(lastVelocity);
  }
});

Ограничение области движения

Momentum scrolling почти всегда требует ограничения границ. Popmotion предоставляет clamp через модификаторы или ручную обработку значения.

const clamp = (min, max) => (v) =>
  Math.max(min, Math.min(max, v));

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

decay({
  from: position,
  velocity: lastVelocity
}).start((v) => {
  position = clamp(0, 1000)(v);
});

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


Эффект “перелёта” и возврат в границы

Более естественное поведение достигается добавлением пружинного возврата после выхода за пределы области.

import { spring } from 'popmotion';

const snapBack = (value, min, max) => {
  if (value < min || value > max) {
    return spring({
      from: value,
      to: Math.min(max, Math.max(min, value)),
      stiffness: 200,
      damping: 25
    });
  }
};

Так создаётся эффект мягкого возврата вместо резкого обрезания движения.


Комбинирование decay и spring для естественной инерции

Реальные интерфейсы используют комбинацию:

  1. decay — основное инерционное движение
  2. spring — коррекция границ
  3. pointer — управление пользователем

Сценарий:

  • пользователь двигает объект
  • pointer фиксирует скорость
  • decay продолжает движение
  • при выходе за пределы активируется spring

Пример полноценного momentum scrolling

import { pointer, decay, spring } from 'popmotion';

let position = 0;
let animation;

const min = 0;
const max = 800;

const update = (v) => {
  position = v;
  console.log(position);
};

const startDecay = (velocity) => {
  animation = decay({
    from: position,
    velocity,
    timeConstant: 400
  }).start({
    update: (v) => update(v),
    complete: () => {
      if (position < min || position > max) {
        spring({
          from: position,
          to: position < min ? min : max,
          stiffness: 180,
          damping: 20
        }).start(update);
      }
    }
  });
};

pointer({ x: 0 }).start({
  update: (v) => {
    position = v.x;
    if (animation) animation.stop();
    update(position);
  },
  complete: (v) => {
    startDecay(v.x.velocity);
  }
});

Сглаживание и контроль ощущения “тяжести”

Ощущение инерции регулируется несколькими параметрами:

  • увеличение timeConstant делает движение более “тяжёлым”
  • уменьшение velocity снижает резкость старта
  • настройка stiffness и damping влияет на возврат
  • фильтрация входной скорости убирает дрожание

Дополнительное сглаживание:

const smoothVelocity = (v, prev) => prev * 0.8 + v * 0.2;

Накопление энергии жеста

Momentum в интерфейсах зависит от длины и скорости жеста. Более длинный drag должен давать более сильную инерцию.

let history = [];

const record = (x) => {
  const now = Date.now();
  history.push({ x, now });

  history = history.filter(h => now - h.now < 50);
};

Скорость рассчитывается по последним точкам, что повышает точность инерции.


Прерывание и перезапуск инерции

Каждое новое взаимодействие должно мгновенно останавливать текущую анимацию:

if (animation) {
  animation.stop();
}

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


Имитация нативного скролла

Momentum scrolling в Popmotion часто используется для:

  • кастомных scroll-view
  • каруселей
  • drag-to-scroll интерфейсов
  • мобильных UI компонентов

Поведение становится визуально близким к нативному скроллу благодаря сочетанию:

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