Статьи

Введение в Popmotion: пользовательский анимационный скруббер

В первой части вводной серии Popmotion мы узнали, как использовать анимации на основе времени, такие как tween движения и keyframes . Мы также узнали, как использовать эти анимации в DOM, используя styler .

Во второй части мы узнали, как использовать отслеживание pointer и записывать velocity . Затем мы использовали это для питания анимации, основанной на скорости, spring , decay и physics .

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

Попробуйте сами:

Во-первых, раскройте этот CodePen для шаблона HTML. Как и раньше, потому что это промежуточный урок, я не буду проходить через все.

Главное замечание заключается в том, что ручка скруббера состоит из двух элементов div : .handle и .handle-hit-area .

.handle — это круглый синий визуальный индикатор того, где находится ручка скруббера. Мы обернули его в невидимый элемент области попадания, чтобы облегчить захват элемента для пользователей с сенсорным экраном.

В верхней части панели JS импортируйте все, что мы собираемся использовать в этом руководстве:

1
2
const { easing, keyframes, pointer, decay, spring, styler, transform, listen, value } = popmotion;
const { pipe, clamp, conditional, linearSpring, interpolate } = transform;

Нам понадобятся три элемента в этом уроке. Мы будем анимировать .box , перетаскивать и анимировать .handle-hit-area и измерять .range .

Давайте также создадим styler для элементов, которые мы собираемся анимировать:

1
2
3
4
5
6
7
const box = document.querySelector(‘.box’);
const boxStyler = styler(box);
 
const handle = document.querySelector(‘.handle-hit-area’);
const handleStyler = styler(handle);
 
const range = document.querySelector(‘.range’);

Для нашей скраббируемой анимации мы собираемся перемещать .box слева направо с keyframes . Тем не менее, мы могли бы так же легко очистить tween или timeline используя тот же метод, который описан ниже в этом руководстве.

1
2
3
4
5
const boxAnimation = keyframes({
  values: [0, -150, 150, 0],
  easings: [easing.backOut, easing.backOut, easing.easeOut],
  duration: 2000
}).start(boxStyler.set(‘x’));

Ваша анимация теперь будет воспроизводиться. Но мы этого не хотим! Давайте приостановим это сейчас:

1
boxAnimation.pause();

Пришло время использовать pointer чтобы перетащить нашу ручку скруббера. В предыдущем уроке мы использовали свойства x и y , но для скруббера нам нужен только x .

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

Он будет работать точно так же, как pointer за исключением того, что он будет принимать только одно число в качестве аргумента и выводить только одно число ( x ):

1
const pointerX = (x) => pointer({ x }).pipe(xy => xy.x);

Здесь вы можете видеть, что мы используем метод pointer под названием pipe . pipe доступен для всех действий Popmotion, которые мы видели до сих пор, включая keyframes .

pipe принимает несколько функций. Когда действие start , все выходные данные будут проходить через каждую из этих функций по очереди, прежде чем будет update функция update предусмотренная для start .

В этом случае наша функция просто:

1
xy => xy.x

Все, что он делает, это берет объект { x, y } обычно выводимый pointer и возвращает только ось x .

Нам нужно знать, начал ли пользователь нажимать ручку, прежде чем мы начнем отслеживать с нашей новой функцией pointerX .

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

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

1
2
listen(handle, ‘mousedown touchstart’).start(startDrag);
listen(document, ‘mouseup touchend’).start(stopDrag);

Позже нам понадобится x- velocity дескриптора, поэтому давайте сделаем это value , которое, как мы узнали в предыдущем уроке, позволяет нам запрашивать скорость. В строке после того, как мы определим handleStyler , добавьте:

1
const handleX = value(0, handleStyler.set(‘x’));

Теперь мы можем добавить наши функции startDrag и stopDrag :

1
2
3
4
const startDrag = () => pointerX(handleX.get())
  .start(handleX);
   
const stopDrag = () => handleX.stop();

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

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

Каждое value имеет метод subscribe . Это позволяет нам подключать несколько подписчиков к срабатыванию при изменении value . Мы хотим искать анимацию keyframes при handleX обновлении handleX .

Сначала измерьте слайдер. В строке после определения range добавьте:

1
const rangeWidth = range.getBoundingClientRect().width;

keyframes.seek принимает значение прогресса, выраженное от 0 до 1 , тогда как наш handleX устанавливается со значениями пикселей от 0 до rangeWidth .

Мы можем преобразовать измерение в пикселях в диапазон от 0 до 1 , разделив текущее измерение в пикселях на rangeWidth . В строке после boxAnimation.pause() добавьте этот метод подписки:

1
handleX.subscribe(v => boxAnimation.seek(v / rangeWidth));

Теперь, если вы играете с помощью скруббера, анимация будет удаляться успешно!

Скруббер все еще можно вытащить за пределы полного диапазона. Чтобы решить эту проблему, мы могли бы просто использовать функцию clamp чтобы гарантировать, что мы не 0, rangeWidth значения за пределами 0, rangeWidth .

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

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

Сначала давайте применим пружину к крайнему левому пределу. Мы будем использовать два трансформатора : conditional и linearSpring .

1
2
3
4
const springRange = (min, max, strength) => conditional(
  v => v < min,
  linearSpring(strength, min)
);

conditional принимает две функции, утверждение и преобразователь. Утверждение получает предоставленное значение и возвращает либо true либо false . Если он возвращает true , второй функции будет предоставлено значение для преобразования и возврата.

В этом случае утверждение гласит: «Если предоставленное значение меньше, чем min , передайте это значение через преобразователь linearSpring ». linearSpring — это простая пружинная функция, которая, в отличие от physics или анимации spring , не имеет понятия времени. Предоставьте ему strength и target , и он создаст функцию, которая «притягивает» любое заданное значение к цели с определенной силой.

Замените нашу функцию startDrag на это:

1
2
3
const startDrag = () => pointerX(handleX.get())
 .pipe(springRange(0, rangeWidth, 0.1))
 .start(handleX);

Теперь мы springRange смещение x по указателю через нашу функцию springRange , поэтому, если вы перетащите ручку за springRange левую сторону, вы заметите, что она возвращается назад.

Чтобы применить то же самое к самой правой стороне, нужно составить второе conditional с первым, используя функцию автономного pipe :

01
02
03
04
05
06
07
08
09
10
const springRange = (min, max, strength) => pipe(
  conditional(
    v => v < min,
    linearSpring(strength, min)
  ),
  conditional(
    v => v > max,
    linearSpring(strength, max)
  )
);

Еще одно преимущество создания такой функции, как springRange заключается в том, что она становится очень тестируемой. Функция, которую она возвращает, как и все преобразователи, является чистой функцией, которая принимает одно значение. Вы можете проверить эту функцию, чтобы увидеть, проходит ли она значения, которые лежат в пределах min и max без изменений, и применяет ли пружины к значениям, которые лежат без.

Если вы отпустите ручку, когда она находится за пределами диапазона, она должна теперь вернуться в диапазон. Для этого нам нужно настроить функцию stopDrag для stopDrag анимации spring :

1
2
3
4
5
6
const stopDrag = () => {
  const x = handleX.get();
  (x < 0 || x > rangeWidth)
    ?
    : handleX.stop();
};

Наша функция snapHandleToEnd выглядит так:

1
2
3
4
5
6
7
const snapHandleToEnd = (x) => spring({
  from: x,
  velocity: handleX.getVelocity(),
  to: x < 0 ?
  damping: 30,
  stiffness: 5000
}).start(handleX);

Вы можете видеть, что to имеет значение 0 или rangeWidth зависимости от того, с какой стороны ползунка в данный момент находится ручка. Играя с damping и stiffness , вы можете играть с различными ощущениями весны.

Приятное прикосновение к скрубберу iOS, которое я всегда ценил, заключалось в том, что если вы бросите ручку, она будет постепенно замедляться, а не останавливаться. Мы можем легко воспроизвести это, используя анимацию decay .

В stopDrag замените handleX.stop() на momentumScroll(x) handleX.stop() momentumScroll(x) .

Затем в строке после функции snapHandleToEnd добавьте новую функцию под названием momentumScroll :

1
2
3
4
const momentumScroll = (x) => decay({
  from: x,
  velocity: handleX.getVelocity()
}).start(handleX);

Теперь, если вы бросите ручку, она постепенно остановится. Это также оживит вне диапазона ползунка. Мы можем остановить это, передав трансформатор decay.pipe методу decay.pipe :

1
2
3
4
5
const momentumScroll = (x) => decay({
  from: x,
  velocity: handleX.getVelocity()
}).pipe(clamp(0, rangeWidth))
  .start(handleX);

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

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

Как насчет решения этих проблем:

  • Сделайте отскок конца импульса, если рукоятка коснется любого конца скруббера.
  • Сделайте анимацию ручки в любой точке скруббера, когда пользователь нажимает на другую часть линейки.
  • Добавить полный контроль воспроизведения, как кнопка воспроизведения / паузы. Обновите положение ручки скруббера в процессе анимации.