跳至主要内容

[JS30] Day13: Slide In on Scroll

keywords: scroll, detection, 捲軸, 滾動, debounce, 投影片效果

CSS 部分

圖片左右排版

這裡圖片排版的方式就是使用 float 來達到讓圖片左右排列的效果。

.align-left {
float: left;
margin-right: 20px;
}

.align-right {
float: right;
margin-left: 20px;
}

圖片滑入

之所以能製作出圖片滑入的效果,主要是使用 opacity (0 -> 1), transform (translateX) 和 transition

一開始的圖片樣式opacity 是 0 ,translateX 則向右(左)往外偏出一些:

.slide-in {
opacity: 0;
transition: all 0.5s;
}

.align-left.slide-in {
transform: translateX(-30%) scale(0.95);
}
.align-right.slide-in {
transform: translateX(30%) scale(0.95);
}

當使用者捲軸滾到那張圖片的時候,我們會在該圖片上添加 .active 這個 class,讓圖片回到原來文章中的位置,並且顯示出來(opacity):

slide-in.active {
opacity: 1;
transform: translateX(0%) scale(1);
}

JS 部分

原理其實不難,就是當視窗的捲軸捲動到「特定的高度」時,就要為該張圖片增添 active 的 class

我們會用到幾個屬性和方法:

window.scrollY; // 取的瀏覽器視窗捲軸 Y 的高度(捲軸在最上方時是 0)
window.innerHeight; // 瀏覽器內視窗的高度
element.offsetTop; // 元素距離外層容器上方的距離
element.height; // 元素的高度

window @ WebAPIs in MDN HTMLElement @ WebAPIs in MDN

偵測瀏覽器底部捲軸的距離

由於 window.scrollY 給的是視窗上方到捲軸最上面的距離,但是我們想要的是視窗下方到捲軸最上面的距離,因此我們要再加上 window.innerHeight

const scrollAt = window.scrollY + window.innerHeight;

找出要為圖片添加 class 的特定高度

我們希望視窗捲軸高度到該張圖片的一半高時添加 .active class(高度可以自訂),因此:

const imageMiddleOffset = image.offsetTop + image.height / 2;

這裡有一個要留意的地方是, offset 指的就是相對於 offsetParent 的距離。一般來說offsetParent都會是 body,但是如果這個元素的外層有另外設定 position 的話,那麼 offsetParent 就可能改變。

因此,再使用 offset 屬性前,盡量先用 offsetParent 來查看該元素的父層對應到的是哪個元素。

達到特定高度時添加 class

再來就是當我們捲軸的高度超過特定高度(圖片一半)時,就添加 class:

if (scrollAt > imageMiddleOffset) {
sliderImage.classList.add('active');
}

debounce 函式

另外,因為我們監聽的是 window 上的 scroll 事件,所以只要瀏覽器一有 scroll 的情況都會觸發這個事件,這可能導致瀏覽器的效能下降,因此在這堂範例中額外提供 debounce 函式,這個函式的功能是在「特定的時間內,只會觸發某事件一次」。

這種 debounce 函式常常應用在可能會多次觸發事件的時間點,例如 scroll, keydown 等等。在 lodash.js 中亦可找到此函式。

/**
* debounce function
* 讓某函式在一定時間內只能觸發一次,目的是提升效能
**/
function debounce(func, wait = 20, immediate = true) {
var timeout;
return function () {
var context = this,
args = arguments;
var later = function () {
timeout = null;
if (!immediate) func.apply(context, args);
};
var callNow = immediate && !timeout;
clearTimeout(timeout);
timeout = setTimeout(later, wait);
if (callNow) func.apply(context, args);
};
}

用法就是把要執行的函式包在 debounce function 中:

window.addEventListener('scroll', debounce(scrollHandler));

完整程式碼

// STEP1 選擇所有圖片
const sliderImages = document.querySelectorAll('.slide-in');

// STEP2 監聽 window 捲軸滾動事件
window.addEventListener('scroll', debounce(scrollHandler));

// STEP3 處理捲軸滾動
function scrollHandler() {
sliderImages.forEach((sliderImage, index) => {
const imageMiddleOffset = sliderImage.offsetTop + sliderImage.height / 2; // 取得每一張圖片中間的 offsetY 位置
const scrollAt = window.scrollY + window.innerHeight;
if (scrollAt > imageMiddleOffset) {
sliderImage.classList.add('active');
}
});
}
/**
* debounce function
* 讓某函式在一定時間內只能觸發一次,目的是提升效能
**/
function debounce(func, wait = 20, immediate = true) {
var timeout;
return function () {
var context = this,
args = arguments;
var later = function () {
timeout = null;
if (!immediate) func.apply(context, args);
};
var callNow = immediate && !timeout;
clearTimeout(timeout);
timeout = setTimeout(later, wait);
if (callNow) func.apply(context, args);
};
}

完成作品

Day13: Slide In on Scroll