Gsap-ScrollTrigger

寫視差的必背工具。

基本觀念

當我們希望動畫是藉由「滾動」來觸發時,就會用到 ScrollTrigger 這個 Plugin,來看一個簡單的範例:

example1-basic-1

這個範例做的事情是當我們把滾軸捲到距離 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(() => {
// 要先註冊 plugin,這樣我們才能用這個功能
gsap.registerPlugin(ScrollTrigger)
gsap.to('.square', {
rotationZ: 180,
duration: 1,
ease: 'none',
scrollTrigger: {
trigger: '.square', // 用來 trigger 這個動畫的元素
start: 100 // 距離 viewport 位置 100px 時觸發動畫
}
})
}, [])

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', // 用來 trigger 這個動畫的元素
start: 'bottom 250px', // 當 .square 的底端碰到 viewport 位置 250px 時觸發動畫
markers: true
}
})
}, [])

return (
<>
<div className='section'>
<div className='square'></div>
</div>
</>
)
}

export default App

example1-basic-2

總之一定要記得:

  • 第一個值是相對於 trigger 的那個元素,第二個值是相對於 viewport
  • 第一個值是相對於 trigger 的那個元素,第二個值是相對於 viewport
  • 第一個值是相對於 trigger 的那個元素,第二個值是相對於 viewport

所以如果我設定 start: 'bottom 100px' 的話,意思就是:

當 .square 的底端(bottom)碰到 viewport 100px 的位置時觸發動畫。

Toggle Actions

簡單來說這個功能是讓你可以決定當元素進入 or 離開滾軸範圍時妳想做什麼樣的動作(Action)

直接來看個例子:

example2-toggle-action

這個動畫其實很簡單,就是當滾軸進入範圍時把方塊用旋轉到中間的位置,不過特別的地方在於我們多加了一些 actions,讓他可以在進入、離開滾軸範圍時做不同的操作:

  1. 當 start 碰到 scroller-start 時重新播放動畫(進入)
  2. 當 end 碰到 scroller-end 時暫停動畫(離開)
  3. 當 end 又碰到 scroler-end 時繼續播放動畫(重新進入)
  4. 當 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,
// 用這個屬性來控制 action
// 可以設定的值:play resume reverse restart reset complete none
toggleActions: 'restart pause resume reverse'
// 每個值會對應到:onEnter onLeave onEnterBack onLeaveBack
}
})
}, [])

return (
<>
<div className='section'>
<div className='square'></div>
</div>
</>
)
}

export default App

Scrub

當你希望把動畫的時間軸完全交給「滾軸」來控制時就會用到這個功能。

example3-scrub-1

首先跟剛剛一樣要註冊一個時間軸,不同的是現在要多填入一些參數,讓 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 註冊額外的 module
gsap.registerPlugin(ScrollTrigger)
// 建立 timeline 並帶入參數
const timeline = gsap.timeline({
scrollTrigger: {
trigger: '.wrapper', // 觸發 scrollTrigger 的那個元素
markers: true, // 讓 gsap 自動幫你畫出 start / end 標記點
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

設定好以後應該會看到這樣的畫面:

example3-scrub-2

簡單來說這邊的意思是當 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', // 記得 .wrapper 要設置為 relative
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 時動畫會剛好結束播放。

example3-scrub-3

看到這邊以後,應該就不難理解這是怎麼運作的了,所以接著只要再把動畫的部分給完成就行了:

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

最後就達成一開始想要的結果了:

example3-scrub-4

Pin

當初在學這個屬性時我自己覺得實在沒有很好理解,所以先來看一個範例吧。

這是沒 pin 的時候:

example4-pin-1

這是有 pin 的時候:

example4-pin-2

附註:原始碼可以到這個 範例 參考。

簡單來說,只要設了 pin: true 以後,藍色方塊就會在 start 碰到 scroller-start 時,把自己「釘(pin)」在 scroller-start 的位置。接著等到 end 碰到 scroller-end 時,藍色方塊才會取消釘選。(有點饒口,建議多閱讀幾次)

pinSpacing

接著再來看另外一個例子,跟剛剛不同的是現在多了一個方塊:

example5-pin-spacing-1

這個範例是拿橘色方塊來當作 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

example5-pin-spacing-2

注意到藍色方塊跟橘色方塊一開始就產生了一段「距離」,這段距離就是透過 pinSpacing 來產生的。而且這個距離會算的剛剛好,當藍色方塊結束 pin 時會正好停在它原本跟橘色方塊應該相隔的距離,假設它們原本有 10px 的間隔,那藍色方塊結束 pin 時就會停在橘色方塊上方 10px 的距離。

如果有點難理解的話我直接示範給你看(現在把方塊加上 margin):

example5-pin-spacing-3

如果還是不懂的話,自己到 範例 去玩玩看吧。

實作簡易的視差效果

在知道 pinpinSpacing 的原理後,就可以做出這樣的效果:

example6-pin-parallax

我的思路是這樣:

  1. 建立一個 section(100vh 滿版高)
  2. section 中放置左右各 50% 的區塊,並透過 absolute 定位在 section 的左邊與右邊
  3. 步驟二的左右 50% 區塊要透過 z-index 來疊三層,調整圖層順序
  4. 透過 scrollTrigger 來設置動畫,trigger 為 section。觸發點為 section 最上方,結束點為 section 最下方,而 scroller-start / scroller-end 皆為 viewport 的最上方(0%)
  5. 建立 timeline,第一段要把左邊向上位移 100%,右邊向下位移 100%,第二段也以此類推。

這個範例我剛開始實作的時候有碰到一個不太懂的地方,就是為什麼section 明明只有設置 100vh 而已,整個網頁的高度卻是 200vh,並且產生滾軸?

其實是因為前面介紹的 pinSpacing,預設 true 時他會自動產生一段距離,讓你在這段 pin 結束時可以剛好跟下面的元素無縫接軌,也避免掉 pin 的期間會覆蓋到下面元素的問題。

而這裡 section 會 pin 住的範圍正好是 100vh,所以額外產生出來的範圍就會是 100vh,所以跟原本的高度加起來就會是 200vh

這段比較複雜一點,建議實際到 範例 看一下原始碼跟試試看,應該就會比較清楚了。

實作簡易的進場效果

example7-transition

這個範例主要是練習對 ScrollTrigger 的應用,也順便介紹幾個新知識:

  1. ScrollTrigger.create 可以透過這種方式來設定滾軸觸發,不用一定要綁在 tween 或 timeline 上
  2. gsap.utils.toArray('.selector') 其實就跟 querySelectorAll 差不多,差別在於它會自動幫你轉成 Array,而不是 NodeList。

至於思路的部分簡單說一下:

  1. 對每個方塊設置獨立的 ScrollTrigger,並且根據順序來決定從「左邊 / 右邊」來進場
  2. 對每個文字段落設置獨立的 ScrollTrigger,進入 scroller-start 時播放動畫(利用 fromTo
  3. 游標只要不停閃爍即可,所以只需要設定閃爍動畫後就行了

最後老話一句,想理解細節的話可以參考原始碼

Canvas-基本繪製與輸出圖片 Gsap-常見的基本方法
Your browser is out-of-date!

Update your browser to view this website correctly. Update my browser now

×