寫視差的必背工具。
基本觀念
當我們希望動畫是藉由「滾動」來觸發時,就會用到 ScrollTrigger 這個 Plugin,來看一個簡單的範例:
這個範例做的事情是當我們把滾軸捲到距離 viewport 30px 的位置時就會觸發動畫,原始碼如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29
| import { gsap } from 'gsap' import ScrollTrigger from 'gsap/ScrollTrigger' import { useEffect } from 'react'
function App() { useEffect(() => { gsap.registerPlugin(ScrollTrigger) gsap.to('.square', { rotationZ: 180, duration: 1, ease: 'none', scrollTrigger: { trigger: '.square', start: 100 } }) }, [])
return ( <> <div className='section'> <div className='square'></div> </div> </> ) }
export default App
|
這邊想特別講的是 start
值的設定方式,除了像上面那樣直接指定距離 viewport 多少 px 時觸發以外,也可以用「字串」的方式來指定,例如這樣子:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29
| import { gsap } from 'gsap' import ScrollTrigger from 'gsap/ScrollTrigger' import { useEffect } from 'react'
function App() { useEffect(() => { gsap.registerPlugin(ScrollTrigger) gsap.to('.square', { rotationZ: 180, duration: 1, ease: 'none', scrollTrigger: { trigger: '.square', start: 'bottom 250px', markers: true } }) }, [])
return ( <> <div className='section'> <div className='square'></div> </div> </> ) }
export default App
|
總之一定要記得:
- 第一個值是相對於 trigger 的那個元素,第二個值是相對於 viewport
- 第一個值是相對於 trigger 的那個元素,第二個值是相對於 viewport
- 第一個值是相對於 trigger 的那個元素,第二個值是相對於 viewport
所以如果我設定 start: 'bottom 100px'
的話,意思就是:
當 .square 的底端(bottom)碰到 viewport 100px 的位置時觸發動畫。
Toggle Actions
簡單來說這個功能是讓你可以決定當元素進入 or 離開滾軸範圍時妳想做什麼樣的動作(Action)
直接來看個例子:
這個動畫其實很簡單,就是當滾軸進入範圍時把方塊用旋轉到中間的位置,不過特別的地方在於我們多加了一些 actions,讓他可以在進入、離開滾軸範圍時做不同的操作:
- 當 start 碰到 scroller-start 時重新播放動畫(進入)
- 當 end 碰到 scroller-end 時暫停動畫(離開)
- 當 end 又碰到 scroler-end 時繼續播放動畫(重新進入)
- 當 start 又碰到 scroller-start 時倒放動畫(重新離開)
這邊的 原始碼 如下,我覺得看註解搭配圖片大概就能懂意思了:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36
| import { gsap } from 'gsap' import ScrollTrigger from 'gsap/ScrollTrigger' import { useEffect } from 'react'
function App() { useEffect(() => { gsap.registerPlugin(ScrollTrigger) gsap.to('.square', { rotationZ: 180, duration: 1, position: 'absolute', left: '50%', xPercent: '-50', scrollTrigger: { trigger: '.square', start: 'top 200px', end: 'center 100px', markers: true, toggleActions: 'restart pause resume reverse' } }) }, [])
return ( <> <div className='section'> <div className='square'></div> </div> </> ) }
export default App
|
Scrub
當你希望把動畫的時間軸完全交給「滾軸」來控制時就會用到這個功能。
首先跟剛剛一樣要註冊一個時間軸,不同的是現在要多填入一些參數,讓 gsap 知道這個時間軸是可以被滾軸控制的:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30
| import { gsap } from 'gsap' import ScrollTrigger from 'gsap/ScrollTrigger' import { useEffect } from 'react'
function App() { useEffect(() => { gsap.registerPlugin(ScrollTrigger) const timeline = gsap.timeline({ scrollTrigger: { trigger: '.wrapper', markers: true, start: 'top 200px', end: 'top 10px', scrub: true } }) }, [])
return ( <div className='App'> <div className='wrapper'> <div className='square square1'></div> </div> </div> ) }
export default App
|
設定好以後應該會看到這樣的畫面:
簡單來說這邊的意思是當 start
碰到 scroller-start
的時候就會播放動畫,而 end
碰到 scroller-end
時動畫就會結束。而且因為我們有設定 scrub: true
,所以動畫會隨著滾軸來控制播放的時間軸。
所以我們可以試著加上一點動畫,像這樣:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40
| import { gsap } from 'gsap' import ScrollTrigger from 'gsap/ScrollTrigger' import TextPlugin from 'gsap/TextPlugin' import { useEffect } from 'react'
function App() { useEffect(() => { gsap.registerPlugin(ScrollTrigger, TextPlugin) demo() }, [])
function demo() { const timeline = gsap.timeline({ scrollTrigger: { trigger: '.wrapper', markers: true, start: 'top 200px', end: 'top 10px', scrub: true } }) timeline.to('.square1', { position: 'absolute', top: '0', left: '50%', xPercent: '-50' }) }
return ( <div className='App'> <div className='wrapper'> <div className='square square1'></div> </div> </div> ) }
export default App
|
附註:注意 start
碰到 scroller-start
時動畫才開始播放,而 end
碰到 scroller-end
時動畫會剛好結束播放。
看到這邊以後,應該就不難理解這是怎麼運作的了,所以接著只要再把動畫的部分給完成就行了:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49
| import { gsap } from 'gsap' import ScrollTrigger from 'gsap/ScrollTrigger' import TextPlugin from 'gsap/TextPlugin' import { useEffect } from 'react'
function App() { useEffect(() => { gsap.registerPlugin(ScrollTrigger, TextPlugin) demo() }, [])
function demo() { const timeline = gsap.timeline({ scrollTrigger: { trigger: '.wrapper', markers: true, start: 'top 200px', end: 'top 10px', scrub: true } }) timeline .to('.square1', { position: 'absolute', top: '0', left: '50%', xPercent: '-50' }) .to('.square1', { position: 'absolute', top: '100%', left: '50%', xPercent: '-50', yPercent: '-100' }) }
return ( <div className='App'> <div className='wrapper'> <div className='square square1'></div> </div> </div> ) }
export default App
|
最後就達成一開始想要的結果了:
Pin
當初在學這個屬性時我自己覺得實在沒有很好理解,所以先來看一個範例吧。
這是沒 pin 的時候:
這是有 pin 的時候:
附註:原始碼可以到這個 範例 參考。
簡單來說,只要設了 pin: true
以後,藍色方塊就會在 start
碰到 scroller-start
時,把自己「釘(pin)」在 scroller-start
的位置。接著等到 end
碰到 scroller-end
時,藍色方塊才會取消釘選。(有點饒口,建議多閱讀幾次)
pinSpacing
接著再來看另外一個例子,跟剛剛不同的是現在多了一個方塊:
這個範例是拿橘色方塊來當作 scrollTrigger
,觸發時會把藍色方塊給 pin 住住,直到 end
碰到 scroller-end
為止。
這段主要的原始碼大概長這樣:
1 2 3 4 5 6 7 8 9 10 11 12
| useEffect(() => { const tl = gsap.timeline({ scrollTrigger: { trigger: '.square2', start: 'top 80%', end: 'top 50%', markers: true, pin: '.square', pinSpacing: false } }) }, [])
|
不過為什麼要特別講這個呢?首先看了上面的原始碼後應該會注意到 pinSpacing
這個屬性,他的用途是「自動把要被 pin 住的元素跟後面的元素加上一段距離」。我知道這樣子講很難理解,所以直接來看範例吧。
現在把 pinSpacing 設為 true
:
注意到藍色方塊跟橘色方塊一開始就產生了一段「距離」,這段距離就是透過 pinSpacing
來產生的。而且這個距離會算的剛剛好,當藍色方塊結束 pin 時會正好停在它原本跟橘色方塊應該相隔的距離,假設它們原本有 10px 的間隔,那藍色方塊結束 pin 時就會停在橘色方塊上方 10px 的距離。
如果有點難理解的話我直接示範給你看(現在把方塊加上 margin
):
如果還是不懂的話,自己到 範例 去玩玩看吧。
實作簡易的視差效果
在知道 pin
跟 pinSpacing
的原理後,就可以做出這樣的效果:
我的思路是這樣:
- 建立一個
section
(100vh 滿版高)
- 在
section
中放置左右各 50% 的區塊,並透過 absolute
定位在 section
的左邊與右邊
- 步驟二的左右 50% 區塊要透過
z-index
來疊三層,調整圖層順序
- 透過
scrollTrigger
來設置動畫,trigger 為 section
。觸發點為 section
最上方,結束點為 section
最下方,而 scroller-start
/ scroller-end
皆為 viewport 的最上方(0%)
- 建立
timeline
,第一段要把左邊向上位移 100%,右邊向下位移 100%,第二段也以此類推。
這個範例我剛開始實作的時候有碰到一個不太懂的地方,就是為什麼section
明明只有設置 100vh
而已,整個網頁的高度卻是 200vh
,並且產生滾軸?
其實是因為前面介紹的 pinSpacing
,預設 true
時他會自動產生一段距離,讓你在這段 pin 結束時可以剛好跟下面的元素無縫接軌,也避免掉 pin 的期間會覆蓋到下面元素的問題。
而這裡 section
會 pin 住的範圍正好是 100vh
,所以額外產生出來的範圍就會是 100vh
,所以跟原本的高度加起來就會是 200vh
。
這段比較複雜一點,建議實際到 範例 看一下原始碼跟試試看,應該就會比較清楚了。
實作簡易的進場效果
這個範例主要是練習對 ScrollTrigger 的應用,也順便介紹幾個新知識:
ScrollTrigger.create
可以透過這種方式來設定滾軸觸發,不用一定要綁在 tween 或 timeline 上
gsap.utils.toArray('.selector')
其實就跟 querySelectorAll
差不多,差別在於它會自動幫你轉成 Array,而不是 NodeList。
至於思路的部分簡單說一下:
- 對每個方塊設置獨立的
ScrollTrigger
,並且根據順序來決定從「左邊 / 右邊」來進場
- 對每個文字段落設置獨立的
ScrollTrigger
,進入 scroller-start
時播放動畫(利用 fromTo
)
- 游標只要不停閃爍即可,所以只需要設定閃爍動畫後就行了
最後老話一句,想理解細節的話可以參考原始碼。