В первой части вводной серии 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();
|
Перетаскивание по оси X
Пришло время использовать 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
, вы можете играть с различными ощущениями весны.
Momentum Scrolling
Приятное прикосновение к скрубберу 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
, мы объединяем простые чистые функции в более сложные поведения, оставляя составные части тестируемыми и пригодными для повторного использования.
Следующие шаги
Как насчет решения этих проблем:
- Сделайте отскок конца импульса, если рукоятка коснется любого конца скруббера.
- Сделайте анимацию ручки в любой точке скруббера, когда пользователь нажимает на другую часть линейки.
- Добавить полный контроль воспроизведения, как кнопка воспроизведения / паузы. Обновите положение ручки скруббера в процессе анимации.