Привет, друзья!
Хочу поделиться с вами примерами использования GSAP
.
Что такое GSAP
?
Если в двух словах, то GSAP
(The GreenSock Animation Platform) — это набор инструментов для реализации анимации любого уровня сложности с помощью JavaScript
.
Использование
Установка
cd react-gsap
yarn add gsap
Импорт
import { gsap } from 'gsap'
Пример
gsap.to('#logo', { duration: 1, x: 100 })
// or
gsap.from("#logo", { duration: 1, x: 100 })
// or
gsap.fromTo("#logo", { width: 0, height: 0 }, { duration: 1.5, width: 100, height: 200 })
Методы gsap.to
и gsap.from
принимают 2 параметра:
-
targets
— анимируемый объект (или объекты). Это может быть один объект, массив объектов, ссылка наDOM-элемент
илиCSS-селектор
типа'.myClass'
(в последнем случаеGSAP
использует методdocument.querySelectorAll
для получения ссылок на анимируемые объекты) -
vars
— объект с настройками анимации (opacity: 0.5, rotation: 45, duration: 1, onComplete: (message) => console.log(message), onCompleteParams: ['потрачено']
и т.д.)
Метод gsap.fromTo
принимает 3 параметра: targets
, startVars
, endVars
.
Как организован проект?
Проект включает в себя 16
примеров, упакованных в одно приложение. Примеры условно можно можно разделить на простые и продвинутые. Переключение между примерами осуществляется с помощью селектора, реализованного с помощью хука useSelector
из Downshift
. По каждому примеру имеется краткое описание (см. ниже). Для того, чтобы эффект от прочтения статьи был максимальным рекомендую следующий алгоритм:
- хотя бы кратко знакомимся с материалами по ссылкам, приведенным выше
- читаем описание примера
- смотрим, как работает соответствующий компонент
- изучаем исходный код компонента
- воспроизводим компонент в своем приложении
Я старался комментировать ключевые моменты в коде компонентов, но вполне мог что-то упустить, поэтому приветствуется любая форма обратной связи.
Итак, поехали.
Простые примеры
Применение анимации к одному (целевому) элементу
gsap
требуется доступ к анимируемому DOM-элементу
. Для предоставления такого доступа используется хук useRef
. Разумеется, в момент анимирования элемент должен быть отрендерен, поэтому gsap
должен запускаться в хуке useEffect
:
export default function App() {
// ссылка на элемент
const el = useRef()
// запускаем `gsap` после рендеринга
useEffect(() => {
gsap.to(el.current, {
// полный поворот
rotation: '+=360'
})
})
return (
<div className='shape square green' ref={el}>
Square
</div>
)
}
См. компонент AnimatingTargetElement
.
Применение анимации ко всем потомкам
Для применения анимации к дочерним элементам (компонентам) используется вспомогательная функция utils.selector(el)
, где el
— предок анимируемых элементов:
const el = useRef()
const q = gsap.utils.selector(el)
useEffect(() => {
// применяем анимацию ко всем потомкам элемента с CSS-классом `shape`
gsap.to(q('.shape'), { x: 100 })
}, [])
См. компонент AnimatingAllDescendantElements
.
Применение анимации к некоторым потомкам
Для применения анимации к некоторым дочерним элементам используется техника под названием перенаправление или передача ссылки (Ref Forwarding):
const el1 = useRef()
const el2 = useRef()
useEffect(() => {
// ссылки на анимируемые элементы
const squares = [el1.current, el2.current]
gsap.to(squares, {
x: 120,
repeat: 3,
repeatDelay: 1,
yoyo: true
})
}, [])
См. компонент AnimatingSomeDescendantElements
.
Создание и управление состоянием анимации
Хук useRef
может использоваться не только для получения доступа к DOM-элементу
, но и для сохранения состояния между рендерингами компонента, такого как состояние анимации:
export default function App() {
const el = useRef()
const q = gsap.utils.selector(el)
// состояние анимации
const tl = useRef()
useEffect(() => {
tl.current = gsap.timeline()
// сначала анимируем квадрат
.to(q(".square"), {
rotate: 360
})
// затем анимируем круг
.to(q(".circle"), {
x: 100
})
}, [])
return (
<div ref={el}>
<Square>Square</Square>
<Circle>Circle</Circle>
</div>
)
}
См. компонент ControllingAnimationTimeline
.
Управление запуском анимации
По умолчанию хук useEffect
запускается после первого и каждого последующего рендеринга. Вызов useEffect
означает запуск анимации. Для управления запуском useEffect
(и анимации) используется массив зависимостей, когда отсутствие массива означает запуск после первого и каждого последующего рендеринга, пустой массив — запуск только после первого рендеринга, массив с зависимостями — запуск после первого рендеринга и при каждом изменении (любой) зависимости:
// запускается только после первого рендеринга
useEffect(() => {
gsap.to(q(".square.red"), { rotation: "+=360" })
}, [])
// запускается после первого рендеринга и после каждого изменения зависимости `someProp`
useEffect(() => {
gsap.to(q(".square.green"), { rotation: "+=360" })
}, [someProp])
// запускается после первого и каждого последующего рендеринга
useEffect(() => {
gsap.to(q(".square.blue"), { rotation: "+=360" })
})
См. компонент ControllingAnimationStart
.
Запуск анимации в ответ на изменение состояния
Пример запуска анимации в ответ на изменение пропа, передаваемого дочернему компоненту:
const Square = ({ children, randomX }) => {
const el = useRef()
// запускается при каждом изменении пропа `randomX`
useEffect(() => {
gsap.to(el.current, {
x: randomX
})
}, [randomX])
return (
<div className="shape square green" ref={el}>{children}</div>
)
}
См. компонент AnimationStartPropChange
.
Запуск анимации в ответ на действие пользователя
Пример запуска анимации в ответ на действие пользователя (наведение курсора на элемент):
// вызывается при наведении курсора
const onEnter = ({ currentTarget }) => {
gsap.to(currentTarget, { backgroundColor: "#e77614" })
}
// вызывается при "снятии" курсора
const onLeave = ({ currentTarget }) => {
gsap.to(currentTarget, { backgroundColor: "#28a92b" })
}
return (
<div
className="shape square green pointer"
onMouseEnter={onEnter}
onMouseLeave={onLeave}
>
Square
</div>
)
См. компонент AnimationStartUserAction
.
Предотвращение "вспышек"
Во избежание вспышек (мигания или мерцания) вместо хука useEffect
следует использовать хук useLayoutEffect
. Как вы знаете, последний запускается синхронно, т.е. перед отрисовкой DOM
:
useLayoutEffect(() => {
// код выполняется только в том случае,
// если `state` имеет значение `complete`
if (state !== 'complete') return
// анимируем дочерние компоненты
// перед отрисовкой `DOM`
gsap.fromTo(
q('.square'),
{
opacity: 0
},
{
opacity: 1,
duration: 1,
stagger: 0.33
}
)
}, [state])
См. компонент AnimationWithoutFlash
.
Не забывайте об отключении анимации и удалении обработчиков событий при размонтировании компонента, особенно, если речь идет о длительной анимации, использовании плагинов вроде ScrollTrigger
или изменении состояния компонента:
useEffect(() => {
const anim1 = gsap.to('.box1', { rotation: '+=360' })
const anim2 = gsap.to('.box2', {
scrollTrigger: {
// ...
}
})
const onMove = () => {
// ...
}
window.addEventListener('pointermove', onMove)
// очистка при размонтировании компонента
return () => {
anim1.kill()
anim2.scrollTrigger.kill()
window.removeEventListener('pointermove', onMove)
}
})
Продвинутые примеры
Взаимодействие компонентов
Порой требуется распределить жизненный цикл анимации (timeline
) между несколькими компонентами. Также иногда анимация зависит от элементов, находящихся в разных компонентах.
Для решения подобных задач существует 2 подхода:
- передача
timeline
от родительского компонента дочерним через пропы - передача колбека, вызываемого дочерними компонентами для добавления анимации в
timeline
Передача timeline
через пропы
function Square({ children, timeline, index }) {
const el = useRef()
// добавляем анимацию в `timeline`
useEffect(() => {
timeline.to(el.current, { x: -100 }, index * 0.1)
}, [timeline])
return <div className="shape square green" ref={el}>{children}</div>
}
function Circle({ children, timeline, index, rotation }) {
const el = useRef()
// добавляем анимацию в `timeline`
useEffect(() => {
timeline.to(el.current, { rotate: rotation, x: 100 }, index * 0.1)
}, [timeline, rotation])
return <div className="shape circle blue" ref={el}>{children}</div>
}
export default function App() {
// жизненный цикл анимации
const [tl, setTl] = useState(() => gsap.timeline())
return (
<>
{/* передаем `timeline` в качестве пропа */}
<Square timeline={tl} index={0}>Square</Square>
<Circle timeline={tl} rotation={360} index={1}>Circle</Circle>
</>
)
}
См. компонент PassingTimelineThroughProps
.
Передача колбека для добавления анимации в timeline
function Square({ children, addAnimation, index }) {
const el = useRef()
// создаем анимацию и добавляем ее в `timeline`
useEffect(() => {
const animation = gsap.to(el.current, { x: -100 })
addAnimation(animation, index)
return () => animation.progress(0).kill()
}, [addAnimation, index])
return <div className="shape square green" ref={el}>{children}</div>
}
function Circle({ children, addAnimation, index, rotation }) {
const el = useRef()
// создаем анимацию и добавляем ее в `timeline`
useEffect(() => {
const animation = gsap.to(el.current, { rotate: rotation, x: 100 })
addAnimation(animation, index)
return () => animation.progress(0).kill()
}, [addAnimation, index, rotation])
return <div className="shape circle blue" ref={el}>{children}</div>
}
export default function App() {
// жизненный цикл анимации
const [tl, setTl] = useState(() => gsap.timeline())
// передаем дочерним компонентам колбек
// для добавления анимации в `timeline`
const addAnimation = useCallback((animation, index) => {
tl.add(animation, index * 0.1)
}, [tl])
return (
<>
<Square addAnimation={addAnimation} index={0}>Square</Square>
<Circle addAnimation={addAnimation} index={1} rotation="360">Circle</Circle>
</>
)
}
См. компонент PassingCallbackThroughProps
.
Передача timeline
через контекст
Передавать timeline
или колбек для добавления анимации в timeline
не всегда удобно. Что если нам нужно передать timeline
компоненту, глубоко вложенному в другие компоненты? В этом случае не обойтись без контекста.
const SelectedContext = createContext()
function Square({ children, id }) {
const el = useRef()
const selected = useContext(SelectedContext)
useEffect(() => {
gsap.to(el.current, {
// сдвигаем элемент на `200px` влево, если его `id`
// совпадает со значением `selected` из контекста
x: selected === id ? 200 : 0
})
}, [selected, id])
return <div className="shape square green" ref={el}>{children}</div>
}
export default function App() {
// любой компонент может читать значения из контекста,
// независимо от уровня его вложенности
// в данном случае мы передаем `2` в качестве начального значения
return (
<SelectedContext.Provider value="2">
<Square id="1">Square 1</Square>
<Square id="2">Square 2</Square>
<Square id="3">Square 3</Square>
</SelectedContext.Provider>
)
}
См. компонент PassingTimelineThroughContext
.
Императивное взаимодействие
Передача пропов или использование контекста в большинстве случаев работает хорошо, но эти механизмы приводят к повторной отрисовке компонентов, что может оказать негативное влияние на производительность при постоянном изменении значения, например, при отслеживании позиции курсора.
Для пропуска рендеринга мы можем использовать хук useImperativeHandle
и создать API
для компонента. Любое значение, возвращаемое хуком, будет передаваться компоненту в виде ссылки:
const Circle = forwardRef((props, ref) => {
const el = useRef()
useImperativeHandle(ref, () => {
// возвращаем `API`
return {
moveTo(x, y) {
gsap.to(el.current, { x, y })
}
}
}, [])
return <div className="shape circle blue" ref={el}></div>
})
export default function App() {
const circleRef = useRef()
useEffect(() => {
// повторный рендеринг не запускается!
circleRef.current.moveTo(300, 100)
}, [])
return (
<>
<Circle ref={circleRef} />
</>
)
}
См. компонент ImperativeHandleMousePosition
.
Создание переиспользуемых анимаций
Создание переиспользуемых анимаций — отличный способ сохранения чистоты кода и уменьшения его количества. Простейшим способом это сделать является вызов функции для создания анимации:
const fadeIn = (target, args) => gsap.from(target, { opacity: 0, ...args })
function App() {
const el = useRef()
useLayoutEffect(() => {
fadeIn(el.current, { x: 100 })
}, [])
return <div className="shape square green" ref={el}>Square</div>
}
Более декларативный подход предполагает создание компонента-обертки для обработки анимации:
function FadeIn({ children, args }) {
const el = useRef()
useLayoutEffect(() => {
gsap.from(el.current.children, {
opacity: 0,
...args
})
}, [])
return <span ref={el}>{children}</span>
}
function App() {
return (
<FadeIn args={{ x: 100 }}>
<div className="shape square green">Square</div>
</FadeIn>
)
}
См. компонент ReusableAnimationWrapper
.
Использование gsap.effects
Рекомендуемым способом создания переиспользуемых анимаций является метод registerEffect()
:
function GsapEffect({ children, targetRef, effect, args }) {
useLayoutEffect(() => {
if (gsap.effects[effect]) {
gsap.effects[effect](targetRef.current, args)
}
}, [effect])
return <>{children}</>
}
function App() {
const el = useRef()
return (
<GsapEffect targetRef={el} effect="spin">
<Square ref={el}>Square</Square>
</GsapEffect>
)
}
См. компонент ReusableAnimationRegisterEffect
.
Выход из анимации
Как анимировать удаление элемента из DOM
? Одним из способов это сделать является изменение состояния компонента после завершения анимации:
function App() {
const el = useRef()
const [active, setActive] = useState(true)
const remove = () => {
gsap.to(el.current, {
opacity: 0,
onComplete: () => setActive(false)
})
}
return (
<div>
<button onClick={remove}>Remove</button>
{ active ? <div ref={el}>Square</div> : null }
</div>
)
}
См. компонент RemovingSingleElementFromDom
.
Точно такой же подход применим в отношении нескольких элементов:
function App() {
const [items, setItems] = useState([
{ id: 0 },
{ id: 1 },
{ id: 2 }
])
const removeItem = (value) => {
setItems(prev => prev.filter(item => item !== value))
}
const remove = (item, target) => {
gsap.to(target, {
opacity: 0,
onComplete: () => removeItem(item)
})
}
return (
<div>
{items.map((item) => (
<div key={item.id} onClick={(e) => remove(item, e.currentTarget)}>
Click Me
</div>
))}
</div>
)
}
См. компонент RemovingMultipleElementsFromDom
.
Кастомные хуки
Кастомные хуки позволяют извлекать логику анимации в переиспользуемые функции.
Вот как можно реализовать пример с registerEffect
с помощью кастомного хука:
function useGsapEffect(target, effect, vars) {
const [animation, setAnimation] = useState()
useLayoutEffect(() => {
setAnimation(gsap.effects[effect](target.current, vars))
}, [effect])
return animation
}
function App() {
const el = useRef()
const animation = useGsapEffect(el, "spin")
return <Square ref={el}>Square</Square>
}
useSelector
Хук для выборки дочерних компонентов.
Сигнатура
function useSelector() {
const ref = useRef()
const q = useMemo(() => gsap.utils.selector(ref), [])
return [q, ref]
}
Использование
function App() {
const [q, ref] = useSelector()
useEffect(() => {
gsap.to(q(".square"), { x: 200 })
}, [])
return (
<div ref={ref}>
<div className="shape square">Square</div>
</div>
)
}
useArrayRef
Хук для добавления ссылок в массив.
Сигнатура
function useArrayRef() {
const refs = useRef([])
refs.current = []
return [refs, (ref) => ref && refs.current.push(ref)]
}
Использование
function App() {
const [refs, setRef] = useArrayRef()
useEffect(() => {
gsap.to(refs.current, { x: 200 })
}, [])
return (
<div>
<div className="shape square green" ref={setRef}>Square 1</div>
<div className="shape square blue" ref={setRef}>Square 2</div>
<div className="shape square red" ref={setRef}>Square 3</div>
</div>
)
}
useStateRef
Данный хук решает проблему доступа к значениям состояния в колбеках. Он похож на useState
, но возвращает третье значение — ссылку на текущее состояние.
Сигнатура
function useStateRef(defaultValue) {
const [state, setState] = useState(defaultValue)
const ref = useRef(state)
const dispatch = useCallback((value) => {
ref.current = typeof value === "function" ? value(ref.current) : value
setState(ref.current)
}, [])
return [state, dispatch, ref]
}
Использование
const [count, setCount, countRef] = useStateRef(5)
const [gsapCount, setGsapCount] = useState(0)
useEffect(() => {
gsap.to(box.current, {
x: 200,
repeat: -1,
onRepeat: () => setGsapCount(countRef.current)
})
}, [])
Пожалуй, это все, чем я хотел поделиться с вами в данной статье.
Что касается моего личного мнения о GSAP
, то, пожалуй, это один из наиболее простых и вместе с тем продвинутых инструментов в своей категории. Теперь это мой первый (после CSS
и SASS
) кандидат на реализацию анимации в веб-приложениях.
Благодарю за внимание и хорошего дня!
yroman
Я так понимаю, там какая-то своя Standard "No Charge" GreenSock License? Чем эта библиотека лучше Framer Motion, у которой MIT license? Пока что из описания каких-то киллер фич я не увидел, всё то же самое есть во Framer.
aio350 Автор
Да, лицензия у GSAP своя, но большая часть функционала предоставляется бесплатно. С Framer Motion не знаком, поэтому ничего не могу сказать по этому поводу