Уже послезавтра, 14 мая, стартует новый поток курса Python для веб-разработки, поэтому мы решили поделиться переводом о не совсем очевидной, но интересной области разработки сайтов — анимации анимации. Автор не просто даёт готовый рецепт, но шаг за шагом показывает, как сделать анимацию прокрутки плавной и приятной. Эта статья больше о концепции, которая поможет вам по-другому взглянуть на вашу анимацию. Так случилось, что этот конкретный пример демонстрирует бесконечную прокрутку, в частности, «идеальную» бесконечную прокрутку колоды карт без дублирования какой-то из них.
Зачем я здесь? Ну, всё началось с твита, заставившего меня задуматься о разметке и боковой прокрутке контента.
Я взял эту концепцию и использовал её на своем сайте. На момент написания статьи она всё ещё актуальна.
Затем я задумался над тем, какие бывают галереи и какие концепции боковой прокрутки существуют. Мы включили прямую трансляцию и решили попробовать сделать что-то вроде старого шаблона Apple «Cover Flow». Помните его?
Первое, о чём я подумал, — мне нужно сделать так, чтобы не был задействован JavaScript, как это делается в демоверсии выше, и чтобы я смог использовать “прогрессивное улучшение". Я схватил Greensock и ScrollTrigger, и дело пошло.
Эта работа привела меня к разочарованию. Я что-то сделал, но я не смог заставить бесконечную прокрутку работать так, как я хотел. Кнопки «Далее» и «Назад» не хотели работать вместе. Вы можете увидеть результат ниже, и в моей реализации используется горизонтальная прокрутка.
Итак, я открыл новую тему на форуме Гринсока. Я и не подозревал, что вот-вот открою себя для серьёзного обучения! Мы решили проблему с кнопками. Но, оставаясь верным себе, я должен был спросить, можно ли как-то ещё улучшить моё решение. Был ли “чистый” способ сделать бесконечную прокрутку? Я пробовал что-то на стриме, но безуспешно. Мне было любопытно. Я пробовал технику, которую написал для релиза ScrollTrigger, в песочнице ниже.
Первый ответ на форкме был о том, что сделать это довольно сложно:
Сложность бесконечной прокрутки в том, что полоса прокрутки ограничена, а желаемый эффект — нет. Таким образом, вам нужно либо зациклить позицию прокрутки, как в разделе демонстраций ScrollTrigger, либо напрямую подключиться к связанным с прокруткой событиям навигации (например к событию колёсика мыши) вместо использования фактической позиции прокрутки.
Я подумал, что это действительно так, и был рад оставить всё как есть. Прошло два дня, и Джек написал ответ, который поразил меня, когда я начал копаться в нём. И теперь, после того, как я нашёл решение, я здесь, чтобы поделиться с вами методом из ответа.
Анимируйте что угодно
Одна вещь, которую часто упускают из виду с GSAP, — это то, что с её помощью можно анимировать практически всё. Часто этот факт упускается потому, что при размышлении об анимации на ум приходят визуальные эффекты — реальное физическое движение чего-либо. Наша первая мысль не о том, чтобы вывести этот процесс на метауровень и анимировать с шага назад.
Подумайте об анимации в более крупном масштабе, а затем разбейте её на слои. Например, вы проигрываете мультик. Мультик представляет собой сборник композиций. Каждая композиция — это сцена. У вас есть возможность просматривать эту коллекцию композиций с помощью пульта дистанционного управления, будь он на YouTube или будь он вашим пультом от телевизора и т. д. В происходящем есть почти три уровня.
И в этом состоит трюк, который нам нужен для создания различных типов бесконечных циклов. Это основной принцип. Мы анимируем положение заголовка воспроизведения на временной шкале при помощи временной шкалы. И затем мы можем очистить эту временную шкалу, устанавливая позицию прокрутки; ничего страшного, если это звучит запутанно. Мы разберёмся.
Переход к "мета"
Начнём с примера. Мы собираемся создать анимацию движения, которая перемещает некоторые поля слева направо. Вот она:
Десять контейнеров, которые перемещаются слева направо. С Greensock это довольно просто. Здесь мы используем fromTo и repeat для анимации движения. Но у нас есть пробел в начале каждой итерации. Мы также используем stagger, чтобы выделить движение, и это сыграет важную роль в дальнейшем.
gsap.fromTo('.box', {
xPercent: 100
}, {
xPercent: -200,
stagger: 0.5,
duration: 1,
repeat: -1,
ease: 'none',
})
Теперь самое интересное. Давайте приостановим анимацию движения и назначим её переменной. Затем создадим анимацию движения, которая будет воспроизводить это. Это делается увеличением totalTime, что позволяет нам получить или установить промежуточную анимацию воспроизведения, учитывая при этом повторы и задержки повтора.
const SHIFT = gsap.fromTo('.box', {
xPercent: 100
}, {
paused: true,
xPercent: -200,
stagger: 0.5,
duration: 1,
repeat: -1,
ease: 'none',
})
const DURATION = SHIFT.duration()
gsap.to(SHIFT, {
totalTime: DURATION,
repeat: -1,
duration: DURATION,
ease: 'none',
})
Это наша первая «мета»-анимации движения. Выглядит точно так же, но мы добавили новый уровень контроля. Мы можем изменить что-либо на этом слое, не затрагивая исходный слой. Например, мы могли бы изменить анимации ease на power4.in. Это полностью меняет анимацию, но не влияет на её основу. Мы как будто защищаем себя с помощью резервного варианта.
Мало того, мы можем выбрать повторение только определённой части временной шкалы. Мы могли бы сделать это с другим fromTo, например:
Код для этого 'эффекта будет примерно таким:
gsap.fromTo(SHIFT, {
totalTime: 2,
}, {
totalTime: DURATION - 1,
repeat: -1,
duration: DURATION,
ease: 'none'
})
Вы видите, к чему всё идёт? Посмотрите на эту анимацию движения. Хотя цикл повторяется, числа меняются при каждом повторении. Но контейнеры находятся в правильном положении.
Достижение «идеального» цикла
Если мы вернёмся к нашему исходному примеру, между каждыми повторениями будет заметный промежуток.
Вот и вся хитрость. Здесь ключ к проблеме. Нам нужно сделать идеальный цикл.
Начнём с того, что повторим шаг трижды. Это равносильно использованию repeat: 3. Обратите внимание, что мы удалили repeat: -1 из анимации движения.
const getShift = () => gsap.fromTo('.box', {
xPercent: 100
}, {
xPercent: -200,
stagger: 0.5,
duration: 1,
ease: 'none',
})
const LOOP = gsap.timeline()
.add(getShift())
.add(getShift())
.add(getShift())
Мы превратили начальную анимацию движения в функцию, которая возвращает анимацию движения, и трижды добавляли её на новую временную шкалу. И это дало нам следующее.
Хорошо. Но пробел всё же есть. Теперь мы можем ввести параметр позиции для добавления и позиционирования этих анимаций. Хочется, чтобы всё было гладко. Это означает вставку каждого набора промежуточных кадров до окончания предыдущего. Это значение зависит от stagger и количества элементов.
const stagger = 0.5 // Used in our shifting tween
const BOXES = gsap.utils.toArray('.box')
const LOOP = gsap.timeline({
repeat: -1
})
.add(getShift(), 0)
.add(getShift(), BOXES.length * stagger)
.add(getShift(), BOXES.length * stagger * 2)
Если мы обновим нашу временную шкалу, чтобы она повторялась, и понаблюдаем за ней (при регулировке stagger, чтобы увидеть, как это влияет на ситуацию)…
Вы заметите, что посередине есть окно, которое создаёт «бесшовный» цикл. Вспомните те навыки, которые мы применили ранее, когда мы манипулировали временем? Вот что нам нужно сделать здесь — зациклить окно времени, в котором цикл является «бесшовным».
Мы могли бы попробовать изменить totalTime через это окно цикла.
const LOOP = gsap.timeline({
paused: true,
repeat: -1,
})
.add(getShift(), 0)
.add(getShift(), BOXES.length * stagger)
.add(getShift(), BOXES.length * stagger * 2)
gsap.fromTo(LOOP, {
totalTime: 4.75,
},
{
totalTime: '+=5',
duration: 10,
ease: 'none',
repeat: -1,
})
Здесь мы ставим totalTime анимации движения от 4,75 и добавляем к нему длину цикла. Длина цикла равна 5. И это среднее окно временной шкалы. Для этого мы можем использовать изящный оператор из GSAP +=, который даёт нам следующее:
Найдите минутку, чтобы понять, как это работает. Это, может быть, самая сложная часть, которую нужно осмыслить. Мы вычисляем временные окна на нашей шкале времени. Такое сложно визуализировать, но я попробовал.
Вот демонстрация часов, в которых стрелки повернутся один раз за 12 секунд. Они зацикливаются бесконечно с помощью repeat: -1, а затем мы используем fromTo для анимации определённого временного окна с заданной продолжительностью. Если вы уменьшите временное окно, скажем, на 2 и 6, а затем измените продолжительность на 1, стрелки переместятся с 2 часов на 6 часов при повторении. Но мы никак не тронули базовую анимацию.
Попробуйте поиграть со значениями, чтобы увидеть, как она влияет на объекты.
На этом этапе было бы неплохо составить формулу позиции нашего окна. Мы также могли бы использовать переменную для продолжительности перехода каждого окна.
const DURATION = 1
const CYCLE_DURATION = BOXES.length * STAGGER
const START_TIME = CYCLE_DURATION + (DURATION * 0.5)
const END_TIME = START_TIME + CYCLE_DURATION
Вместо использования трех составных таймлайнов мы могли бы трижды перебирать наши элементы, при этом нам не нужно вычислять позиции. Визуализация этого в виде трёх составных временных шкал — отличный способ разобраться в концепции и хороший способ, чтобы помочь понять основную идею.
Давайте изменим нашу реализацию, чтобы с самого начала создать одну большую временную шкалу.
const STAGGER = 0.5
const BOXES = gsap.utils.toArray('.box')
const LOOP = gsap.timeline({
paused: true,
repeat: -1,
})
const SHIFTS = [...BOXES, ...BOXES, ...BOXES]
SHIFTS.forEach((BOX, index) => {
LOOP.fromTo(BOX, {
xPercent: 100
}, {
xPercent: -200,
duration: 1,
ease: 'none',
}, index * STAGGER)
})
Такое проще собрать, а результат — одно и то же окно. Но нам не нужно думать о математике. Теперь мы перебираем три набора контейнеров и располагаем каждую анимацию в соответствии с шагом.
Как такое может выглядеть, если мы отрегулируем смещение? Это сожмёт контейнеры ближе друг к другу.
Но это ломает окно, потому что нет totalTime. Окно нужно перерассчитать. Сейчас хороший момент, чтобы применить вычисленную ранее формулу.
const DURATION = 1
const CYCLE_DURATION = STAGGER * BOXES.length
const START_TIME = CYCLE_DURATION + (DURATION * 0.5)
const END_TIME = START_TIME + CYCLE_DURATION
gsap.fromTo(LOOP, {
totalTime: START_TIME,
},
{
totalTime: END_TIME,
duration: 10,
ease: 'none',
repeat: -1,
})
Починили!
Мы могли бы даже ввести «смещение», если бы захотели изменить начальную позицию.
const STAGGER = 0.5
const OFFSET = 5 * STAGGER
const START_TIME = (CYCLE_DURATION + (STAGGER * 0.5)) + OFFSET
Теперь наше окно начинается с другой позиции.
Но всё же это не очень хорошо, так как даёт нам эти неудобные стопки на каждом конце. Чтобы избавиться от этого эффекта, нужно подумать о «физическом» окне для наших контейнеров. Или о том, как они появляются и исчезают с экрана.
Мы воспользуемся document.body в качестве окна для нашего примера. Давайте обновим анимацию движения контейнеров, чтобы они были отдельными шкалами времени, где блоки увеличиваются при появлении и уменьшаются при исчезновении. Мы можем использовать yoyo и repeat: 1, чтобы добиться появления и исчезновения.
SHIFTS.forEach((BOX, index) => {
const BOX_TL = gsap
.timeline()
.fromTo(
BOX,
{
xPercent: 100,
},
{
xPercent: -200,
duration: 1,
ease: 'none',
}, 0
)
.fromTo(
BOX,
{
scale: 0,
},
{
scale: 1,
repeat: 1,
yoyo: true,
ease: 'none',
duration: 0.5,
},
0
)
LOOP.add(BOX_TL, index * STAGGER)
})
Почему мы используем временную шкалу длительностью в 1? Так проще отслеживать её. Мы знаем, что время равняется 0,5, когда контейнер находится посередине.
Стоит отметить, что ease не будет иметь того эффекта, о котором мы обычно думаем. Фактически ease будет играть роль в том, как располагаются поля. Например, при перемещении контейнеры справа сбиваются в кучу, прежде чем они переместятся. Код выше даёт этот эффект.
Почти готово, но наши контейнеры на время исчезают посередине. Чтобы исправить это, давайте введём свойство immediateRender. Оно действует как animation-fill-mode: none в CSS. Мы сообщаем GSAP, что не хотим сохранять или предварительно записывать какие-либо установленные для контейнера стили.
SHIFTS.forEach((BOX, index) => {
const BOX_TL = gsap
.timeline()
.fromTo(
BOX,
{
xPercent: 100,
},
{
xPercent: -200,
duration: 1,
ease: 'none',
immediateRender: false,
}, 0
)
.fromTo(
BOX,
{
scale: 0,
},
{
scale: 1,
repeat: 1,
zIndex: BOXES.length + 1,
yoyo: true,
ease: 'none',
duration: 0.5,
immediateRender: false,
},
0
)
LOOP.add(BOX_TL, index * STAGGER)
})
Это небольшое изменение решает наши проблемы! Обратите внимание, что мы начали прописывать z-index: BOXES.length. Эта строка должна защитить нас от любых проблем с z-index.
Вот оно! Наш первый бесконечный бесшовный цикл. Никаких повторяющихся элементов. Мы изменяем время!
Если хочется увидеть больше контейнеров одновременно, мы можем повозиться со временем, stagger и ease. Здесь у нас stagger 0,2, а также мы добавили opacity.
Ключевой момент: мы можем использовать repeatDelay, чтобы переход opacity выполнялся быстрее, чем масштаб. Затухание — более 0,25 секунды. Ожидание — 0,5 секунды. Исчезание — через 0,25 секунды.
.fromTo(
BOX, {
opacity: 0,
}, {
opacity: 1,
duration: 0.25,
repeat: 1,
repeatDelay: 0.5,
immediateRender: false,
ease: 'none',
yoyo: true,
}, 0)
Круто! Мы можем делать всё, что захотим, с появлением и исчезновением контейнеров. Главное здесь — у нас есть окно времени, которое даёт нам бесконечный цикл.
Подключение для скроллинга
Теперь, когда у нас есть бесшовный цикл, давайте используем его для скроллинга. Для этого мы можем задействовать ScrollTrigger из GSAP. Это потребует дополнительной анимации движения, чтобы очистить наше окно цикла. Обратите внимание на то, как теперь мы реализовали приостановку цикла.
const LOOP_HEAD = gsap.fromTo(LOOP, {
totalTime: START_TIME,
},
{
totalTime: END_TIME,
duration: 10,
ease: 'none',
repeat: -1,
paused: true,
})
const SCRUB = gsap.to(LOOP_HEAD, {
totalTime: 0,
paused: true,
duration: 1,
ease: 'none',
})
Уловка здесь состоит в том, чтобы использовать ScrollTrigger для очистки головной части воспроизведения цикла, обновляя totalTime из SCRUB. Есть несколько способов настроить такой скролл.
Мы могли расположить его горизонтально или привязать к контейнеру. Но мы собираемся обернуть наши поля в элемент .boxes и закрепить его в области просмотра. (Это фиксирует его положение во вьюпорте.) Мы также будем придерживаться вертикальной прокрутки. Посмотрите пример, чтобы увидеть стили для .boxes, которые устанавливают размер окна просмотра.
import ScrollTrigger from 'https://cdn.skypack.dev/gsap/ScrollTrigger'
gsap.registerPlugin(ScrollTrigger)
ScrollTrigger.create({
start: 0,
end: '+=2000',
horizontal: false,
pin: '.boxes',
onUpdate: self => {
SCRUB.vars.totalTime = LOOP_HEAD.duration() * self.progress
SCRUB.invalidate().restart()
}
})
Важная часть находится внутри onUpdate. Здесь мы устанавливаем totalTime анимации движения в зависимости от прогресса прокрутки. Вызов invalidate сбрасывает все внутренние записанные позиции для SCRUB. Затем перезапуск устанавливает позицию на новое значение totalTime, которое мы установили.
Мы можем перемещаться вперёд и назад по временной шкале и обновлять позицию.
Разве это не впечатляет? Мы можем покрутить, чтобы прокрутить временную шкалу, которая очищает временную шкалу — окно временной шкалы. Уделите секунду, чтобы переварить: именно это здесь и происходит.
Путешествие во времени для бесконечной прокрутки
До сих пор мы манипулировали временем. А теперь отправимся в путешествие во времени! Для этого мы собираемся использовать некоторые другие утилиты GSAP, и мы больше не будем очищать totalTime LOOP_HEAD, вместо этого обновив его через прокси. Это ещё один отличный пример перехода на «мета»-GSAP. Начнём с прокси-объекта, отмечающего положение точки воспроизведения.
const PLAYHEAD = { position: 0 }
Теперь мы можем обновить SCRUB, чтобы обновить position. В то же время мы можем использовать утилиту GSAP wrap, которая заворачивает значение position в продолжительность LOOP_HEAD. Например, если длительность равна 10 и мы предоставим значение 11, то вернёмся к 1.
const POSITION_WRAP = gsap.utils.wrap(0, LOOP_HEAD.duration())
const SCRUB = gsap.to(PLAYHEAD, {
position: 0,
onUpdate: () => {
LOOP_HEAD.totalTime(POSITION_WRAP(PLAYHEAD.position))
},
paused: true,
duration: 1,
ease: 'none',
})
И последнее, но не менее важное: нам нужно пересмотреть ScrollTrigger, чтобы он обновлял правильную переменную в SCRUB — position, а не totalTime.
ScrollTrigger.create({
start: 0,
end: '+=2000',
horizontal: false,
pin: '.boxes',
onUpdate: self => {
SCRUB.vars.position = LOOP_HEAD.duration() * self.progress
SCRUB.invalidate().restart()
}
})
На данный момент мы перешли на прокси и не увидим никаких изменений.
Нам нужен бесконечный цикл при прокрутке. Наша первая мысль может заключаться в том, чтобы прокрутить до начала, когда мы завершим прокрутку. И он сделает именно это — прокрутит назад. Хотя это именно то, чего мы хотим, мы не хотим, чтобы ползунок перемещался в обратном направлении. Вот тут-то и приходит на помощь totalTime. Помните? Он получает или устанавливает положение точки воспроизведения в соответствии с totalDuration, которая включает любые повторы и задержки повтора.
Например, предположим, что продолжительность заголовка цикла была 5 и мы дошли до этого значения, мы не будем очищать его до 0. Вместо этого продолжим очистку головной части цикла до 10. Если мы продолжим, он перейдёт к 15 и так далее. Между тем мы будем отслеживать переменную iteration, потому что она сообщает нам, где мы находимся в scrub. Мы также позаботимся о том, чтобы обновлять итерацию только при достижении пороговых значений выполнения. Начнём с переменной iteration:
let iteration = 0
Теперь давайте обновим нашу реализацию ScrollTrigger:
const TRIGGER = ScrollTrigger.create({
start: 0,
end: '+=2000',
horizontal: false,
pin: '.boxes',
onUpdate: self => {
const SCROLL = self.scroll()
if (SCROLL > self.end - 1) {
// Go forwards in time
WRAP(1, 1)
} else if (SCROLL < 1 && self.direction <; 0) {
// Go backwards in time
WRAP(-1, self.end - 1)
} else {
SCRUB.vars.position = (iteration + self.progress) * LOOP_HEAD.duration()
SCRUB.invalidate().restart()
}
}
})
Обратите внимание, как мы теперь учитываем итерацию при расчёте position. Помните, что это оборачивается скраббером. Мы также определяем, когда достигаем пределов прокрутки, и там точка для WRAP.
Функция ниже устанавливает соответствующее значение iteration и устанавливает новую позицию прокрутки.
const WRAP = (iterationDelta, scrollTo) => {
iteration += iterationDelta
TRIGGER.scroll(scrollTo)
TRIGGER.update()
}
У нас бесконечная прокрутка! Если у вас есть одна из этих причудливых мышек с колёсиком прокрутки, которую вы можете отпустить, чтобы оно крутилось само, попробуйте! Это весело!
Вот демонстрация, которая отображает текущую итерацию и прогресс:
Привязка прокрутки
При работе с такой функцией всегда есть что-то хорошее. Начнём с привязки к прокрутке. GSAP упрощает это, поскольку мы можем использовать gsap.utils.snap без каких-либо других зависимостей. Это даёт привязку к моментам, где мы указали точки. Объявляем шаг от от 0 до 1 ч десятью полями, то есть подойдёт Snap 0,1.
const SNAP = gsap.utils.snap(1 / BOXES.length)
SNAP возвращает функцию, которую мы можем использовать для привязки нашего значения position.
Мы хотим сделать привязку только после того, как прокрутка закончилась. Для этого мы можем использовать прослушиватель событий на ScrollTrigger. Когда прокрутка закончится, мы перейдём к определённому значению position.
ScrollTrigger.addEventListener('scrollEnd', () => {
scrollToPosition(SCRUB.vars.position)
})
А вот scrollToPosition:
const scrollToPosition = position => {
const SNAP_POS = SNAP(position)
const PROGRESS =
(SNAP_POS - LOOP_HEAD.duration() * iteration) / LOOP_HEAD.duration()
const SCROLL = progressToScroll(PROGRESS)
TRIGGER.scroll(SCROLL)
}
Что мы тут делаем?
Расчёт момента времени для привязки
Расчёт текущего прогресса. Допустим, LOOP_HEAD.duration() равно 1 и мы установили 2,5. Это даёт нам прогресс 0,5, в результате чего iteration равна 2, где
2,5 - 1 * 2/1 === 0,5
. Мы рассчитываем прогресс так, чтобы он всегда был между 1 и 0.Расчёт конечной точки прокрутки. Эту часть дистанции может преодолеть ScrollTrigger. В примере мы установили расстояние 2000, и нам нужна его часть. Напишем новую функцию progressToScroll для её расчета.
const progressToScroll = progress =>
gsap.utils.clamp(1, TRIGGER.end - 1, gsap.utils.wrap(0, 1, progress) * TRIGGER.end)
Эта функция принимает значение прогресса и сопоставляет его с наибольшим расстоянием прокрутки. Но мы используем ограничение, чтобы гарантировать, что значение никогда не может быть 0 или 2000. Это важно. Мы защищаемся от привязки к этим значениям, так как это может поставить нас в бесконечный цикл.
Есть кое-что, что нужно сделать. Посмотрите эту демонстрацию, которая показывает обновлённые значения для каждой привязки.
Почему всё намного шустрее? Изменены продолжительность и сглаживание очистки. Меньшая продолжительность и более резкое сглаживание дают нам привязку.
const SCRUB = gsap.to(PLAYHEAD, {
position: 0,
onUpdate: () => {
LOOP_HEAD.totalTime(POSITION_WRAP(PLAYHEAD.position))
},
paused: true,
duration: 0.25,
ease: 'power3',
})
Но если вы поиграли с этой демоверсией, вы заметите проблему. Иногда, когда мы оборачиваемся внутри оснастки, ползунок прыгает. Мы должны учитывать это, проверяя, что мы оборачиваем, когда щёлкаем, но только тогда, когда это необходимо.
const scrollToPosition = position => {
const SNAP_POS = SNAP(position)
const PROGRESS =
(SNAP_POS - LOOP_HEAD.duration() * iteration) / LOOP_HEAD.duration()
const SCROLL = progressToScroll(PROGRESS)
if (PROGRESS >= 1 || PROGRESS < 0) return WRAP(Math.floor(PROGRESS), SCROLL)
TRIGGER.scroll(SCROLL)
}
И теперь у нас есть бесконечный скролл с привязкой!
Что дальше?
Мы создали основу для создания надёжного бесконечного скролла. Можно воспользоваться ею, чтобы добавить элементы управления или функциональность клавиатуры. Например, таким образом можно связать кнопки «Далее» и «Назад» и элементы управления с клавиатуры. Всё, что нам нужно делать, — это управлять временем, верно?
const NEXT = () => scrollToPosition(SCRUB.vars.position - (1 / BOXES.length))
const PREV = () => scrollToPosition(SCRUB.vars.position + (1 / BOXES.length))
// Left and Right arrow plus A and D
document.addEventListener('keydown', event => {
if (event.keyCode === 37 || event.keyCode === 65) NEXT()
if (event.keyCode === 39 || event.keyCode === 68) PREV()
})
document.querySelector('.next').addEventListener('click', NEXT)
document.querySelector('.prev').addEventListener('click', PREV)
Код может дать нам что-то вроде этого.
Мы можем использовать нашу функцию scrollToPosition и увеличивать значение по мере необходимости.
Вот оно!
Видите это? GSAP может анимировать не только элементы! Здесь мы изгибали время и управляли им, чтобы создать почти идеальный бесконечный слайдер. Никаких повторяющихся элементов, никакого беспорядка и хорошая гибкость. Подведём итоги. В этой статье мы рассмотрели, как:
Анимировать анимацию.
Манипулируя временем, рассматривать его как на инструмент позиционирования.
Использовать ScrollTrigger для прокрутки анимации через прокси.
Использовать некоторые замечательные утилиты GSAP для обработки логики за нас.
Теперь вы можете управлять временем! Эта концепция перехода на «мета» GSAP открывает множество возможностей. Что еще можно было оживить? Аудио? Видео? Что касается демо «Cover Flow», то вот к чему мы пришли!
Если вы хотите научиться работать с JavaScript и стеком веб-технологий в целом, а после превзойти автора этой статьи, обратите внимание на особенный, авторский курс Frontend-разработчик, созданный опытными разработчиками. На нем вы сделаете 5 проектов на JavaScript и научитесь писать сложные компоненты на React и интерфейсы с авторизацией и с подключением к бекенду. Смотрите программу курса, сравнивайте и выбирайте подходящее для себя.
Узнайте, как прокачаться и в других специальностях или освоить их с нуля:
Другие профессии и курсы
ПРОФЕССИИ
КУРСЫ
nin-jin
Почему бы просто не сделать горизонтальный скроллинг и не мучать пользователя этими прыгающими анимациями?
DmitryKazakov8
Вау-эффект, продажи, маркетинг.
nin-jin
Этим эффектом уже никого не удивить. А вот скачущие анимации создают неприятное ощущение.