ScrollTrigger是利用滑鼠滾動來控制動畫的套件,最大的賣點之一是他可以做出pin的效果,你可以自由控制什麼時候要被pin住。

$cover

ScrollTrigger 的基礎操作

ScrollTrigger是利用滑鼠滾動來控制動畫的套件,最大的賣點之一是他可以做出pin的效果,你可以自由控制什麼時候要被pin住。
他的原理是fixed住你想pin的區塊並且加入padding-bottom把下方的區塊推下去,讓使用者在感覺不出來畫面是有真實在滾動的。

step 1. 載入套件

第一步你要先告訴gsap你要加載這個套件跟你要控制的物件

gsap.registerPlugin(ScrollTrigger);

// trigger代表你想要作用的區塊(如果不寫的話會自動抓gsap.to的對象,所以也可以省略)
gsap.to(".b", {
scrollTrigger: {
trigger: ".b",
}
});

step 2. 設定開始跟結束的範圍&動畫效果

第二步你要設定開始點跟結束點,如果你覺得觸發的條件有點抽象可以開啟markers檢視觸發的位置在哪裡。觸發的點是當scroller-start進入start時開始,scroller-end進入到end時結束

gsap.to(".b", {
x: 150,
rotation: 360,
ease: "power1.out",
scrollTrigger: {
trigger: ".b", //觸發的物件(可省略)
start: "50% 70%", // (物件開始位置, 卷軸開始位置) 參數可以設定 top center bottom px %
end: "+=300", //(物件結束位置, 卷軸結束位置) , 也可以設卷軸捲動多少結束動畫(+=300是指start的位置再加上300)
pin: true, // 物件執行完動畫會跟著卷軸走,類似 position: fixed
scrub: 1, // 物件動畫延遲的秒數
toggleClass: "active", // 增加移除的class,class名稱須為字串
markers: true // 顯示標記
}
});

常見參數介紹

指令 型別 用途
start String | Number | Function
  • 可以輸入兩個參數分別代表 [物件開始位置, 卷軸開始位置]
  • 如果沒有輸入參數則預設為top
  • 單位可以設定 top center bottom px % +=100 也可以是自定義變數或是函式
  • max是一個特殊的單位,指最大的捲動長度
end String | Number | Function
  • 可以輸入兩個參數分別代表 [物件結束位置, 卷軸結束位置]
  • 如果沒有輸入參數則預設為bottom
  • 單位可以設定 top center bottom px % +=100 也可以是自定義變數或是函式
  • max是一個特殊的單位,指最大的捲動長度
markers Boolean 打開位置檢查器
horizontal Boolean 打開的話往下畫面會橫向移動(可以開啟markers發現位置由上下變成左右了)
pin Boolean | Element 觸發時會被釘選住(fixed),參數也可以設定你想pin的元素位置
pinSpacing Boolean 如果不想要釘選時自動生成下方的padding,可以使用false取消掉
toggleClass String 觸發時該物件會新增的class,class名稱須為字串(如果你想新增class的地方不是該物件就只能用onEnter的方法觸發)
toggleActions String 需要輸入四個參數,分別代表onEnter、onLeave、onEnterBack、onLeaveBack的行爲(EX:play none none none 代表只有正向進入時會執行動畫)
scrub Boolean | Number 如果設定true代表會按照滑鼠滾動的距離計算動畫的位置,如果設定數字則代表動畫延遲的秒數
onEnter Function 由上往下進入時觸發函式 onEnter: () => {...}
onEnterBack Function 由下往上進入時觸發函式 onEnterBack: () => {...}
onLeave Function 由上往下離開時觸發程式 onLeave: () => {...}
onLeaveBack Function 由下往上離開時觸發函式 onLeaveBack: () => {...}

註:這些參數都必須寫在scrollTrigger裡面才會作用

如果覺得onEnter很抽象的話,官方也有給解釋的範例

更詳細的參數介紹請見官方手冊


使用timeline優化流程

上述的範例都是在物件只有一個的情況下,當你的動畫是由很多物件組合在一起的,這時使用timeline去組合這些動畫會比較好。

step 1. 設定 timeline

首先你要先建立一個gsap.timeline,trigger的位置是這些物件的父層容器。

gsap.registerPlugin("ScrollTrigger");

gsap.timeline({
scrollTrigger: {
trigger: ".public-section", // 你想要作用的區塊
start: "top top", // 開始位置
end: "+=2000", // 結束位置(通常會給他設定這段動畫的總長為多少)
pin: ".public-section", // 你想要fixed的區塊
},
})

看下面的範例可以發現只要放在父層容器的所有物件都會跟著被fixed住,直到滾動超過2000px後才恢復正常的效果。

step 2. 設定動畫的時間軸

就像接龍一樣把接下來想做的動畫串連上去就可以了。

gsap.timeline({
scrollTrigger: {
trigger: ".public-section",
start: "top top",
end: "+=2000",
pin: ".public-section",
},
}).to(
".public-section .a",
{
x: 300,
scrollTrigger: {
start: 0,
end: 500,
scrub: 1,
},
}
).fromTo(
".public-section .b",
{
autoAlpha: 0
},
{
autoAlpha: 1,
y: -300,
scrollTrigger: {
start: 200,
end: 800,
scrub: 1,
},
}
)

如果你覺得接龍的方式會讓函式變得一大串,也可以把它拆分成多段,這樣程式碼看起來就不會像一坨義大利麵。

let part1_tl = gsap.timeline();
let part2_tl = gsap.timeline();

ScrollTrigger.create({
trigger: ".public-section",
start: "top top",
end: "+=2000",
pin: ".public-section",
});

part1_tl.to(
".public-section .a",
{
x: 300,
scrollTrigger: {
start: 0,
end: 500,
scrub: 1,
},
}
)

part2_tl.fromTo(
".public-section .b",
{
autoAlpha: 0
},
{
autoAlpha: 1,
y: -300,
scrollTrigger: {
start: 200,
end: 800,
scrub: 1,
},
}
)

案例研究

我這邊分析幾個官方給的example,可以從中學習他們是如何運用這些參數去做動畫。

案例一 stagger的運用

stagger是用來處理多個物件時每一個物件的間距秒數

首先他把大砲跟射出去的東西拆成兩個timeline,再用一個masterTl去組合在一起。

const masterTl = gsap.timeline({
paused: true,
});

const tl1 = gsap.timeline()
const tl2 = gsap.timeline()

masterTl
.add(tl1, 0)
.add(tl2, 0)
.play();

大砲的部分,他用3段動畫組合在一起,分別是往左、左右搖擺、往右,因為沒有設定scrollTrigger所以動畫會是一個執行完馬上接著做下一個。

const tl1 = gsap
.timeline()
.to(cannon, {
rotation: -angle,
duration: 0.65,
ease: "power1.inOut"
})
.to(
cannon,
{
rotation: angle,
ease: "power1.inOut",
duration: 1,
repeat: 3,
yoyo: true
}
)
.to(cannon, {
rotation: 0,
duration: 0.65,
ease: "power1.inOut"
});

子彈的部分因為是一個一個射出去,所以你要給gsap一個array讓他知道要抓取哪些元素。

const bullets = [];
const bulletsContainer = document.querySelector(".flair-container");
const tl1Time = tl1.duration();

for (i = 0; i < 40; i++) {
const className = "flair--" + gsap.utils.random(2, 35, 1);
flairBullet = document.createElement("div");
flairBullet.setAttribute("class", "flair flair-bullet " + className);
bulletsContainer.appendChild(flairBullet);
bullets.push(flairBullet);
gsap.set(flairBullet, { scale: "random(0.4, 0.6)" });
}

tip

gsap.utils()是gsap提供的一些實用的方法,你不一定要用原生的JS去處理,這邊有相關的方法介紹。 所以按照文件說明 gsap.utils.random(2, 35, 1) 這段的意思就是隨機產生2~35的數字寫最小單位是1(也就是不會有小數點)

tip

gsap.set()是設定元素的css設定需要有兩個參數,第一個是作用的元素、第二個是css樣式,等同於沒有ease效果的gsap.to()。

gsap.set(".class", { x: 100, y: 50, opacity: 0 }); // 這兩個的結果會一樣
gsap.to(".class", { duration: 0, x: 100, y: 50, opacity: 0 });

gsap.set([obj1, obj2, obj3], { x: 100, y: 50, opacity: 0 }); // 第一個參數也可以為array

最後做出 1.子彈出現 2.子彈射出 的連續動畫,這邊有用到physics2D 這個是需要付費的套件功能(Physics2DPlugin),所以就不多作介紹。

const tl2 = gsap
.timeline()
.to(bullets, {
opacity: 1,
duration: 0.25,
stagger: {
amount: tl1Time
}
})
.to(bullets,{
duration: tl1Time,
physics2D: {
velocity: "random(600, 850)",
angle: () => 270 + gsap.getProperty(cannon, "rotation"),
gravity: 600
},
stagger: {
amount: tl1Time
}
},0);

案例二 橫向移動

當你要介紹timeline相關的內容時,橫向移動是一個非常好的呈現方式

首先你要先抓取你要橫向移動的區塊,並且計算它有多長

let pinWrap = document.querySelector(".pin-wrap");
let pinWrapWidth = pinWrap.offsetWidth; // 計算長度
let horizontalScrollLength = pinWrapWidth - window.innerWidth; // 扣除螢幕的寬度(實際橫向移動的距離)

接著使用scrollTrigger把畫面固定住並且給他需要橫向移動的長度(這邊把ease關掉估計是怕locomotive-scroll會互相干擾)

gsap.to(".pin-wrap", {
scrollTrigger: {
scroller: pageContainer, //locomotive-scroll
scrub: true,
trigger: "#sectionPin",
pin: true,
start: "top top",
end: pinWrapWidth
},
x: -horizontalScrollLength,
ease: "none"
});

最後因為他有使用locomotive-scroll的關係,所以有使用refresh()跟update()的方式防止scrollTrigger計算錯誤。


進階設定

這邊舉一些我有用過的參數或是屬性,因為實在太多了無法一一列舉。

偵測裝置行為

有時候你會遇到手機裝置跟滑鼠裝置做出不一樣的行為時可以使用ScrollTrigger.isTouch這個屬性去做判斷。
0為滑鼠模式,1為觸碰模式,2為兩者皆是

if (ScrollTrigger.isTouch) {
// 可以觸碰的裝置
}

偵測移動方向

當你需要判斷現在使用者是往哪裡滑動的時候,可以用ScrollTrigger.observe()去做偵測不需要另外使用Observer這個套件。
至於參數設定跟Observer一樣,會在下一篇做更詳細的介紹。

ScrollTrigger.observe({
type: "wheel, touch", // 需要作用的行為
onUp: () => ..., // 往上滑想要做的事情
onLeft: () => ..., // 往右滑想要做的事情
wheelSpeed: -1, // 因為用手機滑動是往下滑畫面往上跑,所以行爲要剛好相反
tolerance: 50, // 數值越大會需要滑動較大力才會觸發
preventDefault: true, // 防止預設行為
});

同時控制多個相同的動畫

一般來說你可以用ScrollTrigger.array()同時控制多個物件,這邊有提供一個ScrollTrigger.batch()讓你更簡便處理回調函式。