Lenis という慣性スクロールのライブラリが素敵です。
AwwwardsやFWAに掲載されているサイトでよく見かける、ゆったりした慣性スクロール。
http://madies.mx
http://www.amandabraga.com
http://56k.studiovoila.com
見覚えがある人もいると思いますが、スクロールの反応が気持ち遅れて来る、自前の慣性スクロールっぽいやつです。これのやり方の参考が少なく実装に手間取ったので、実装例をまとめました。他にも色々なアプローチがありそうですが、参考のひとつとして。
最初に断っておきますが、かなり邪悪な実装です。ブラウザの本来の機能を上書きする実装の多くは、ユーザーの利益にはならないので、ご利用は計画的に。。
デモ
https://codepen.io/nishinoshake/full/dyowbyr
採用する利点と欠点
利点があまり浮かばない・・・
利点
- このスクロールに心地よさを感じる人がいるらしい
欠点
- 慣れていない人にとっては違和感がある
- スクロール系のライブラリと相性が悪い
- 低スペックなマシンだと見るに耐えない
- 邪悪な実装がサイトの寿命を縮める
原理
スクロールと同期して、transformで要素を滑らせる。以下、スクロールしたいエリアをコンテナと呼びます。
1. コンテナにposition:fixedを設定
2. bodyにコンテナの高さを設定
3. スクロール量をコンテナのtransformに少しづつ設定
1.のfixedだけだと、bodyの高さが無くなってスクロールができなくなり、スクロールバーも表示されません。スクロールバーが無いままのサイトや、独自でカスタムしている邪悪なサイトもありますが、これ以上は罪を重ねない方が良いと思います。
bodyにコンテナの高さを設定すると、通常のスクロールイベントを拾えるので、transformとの同期も楽になります。mousewheelやtouchmoveイベントの実装は辛いので、こちらの方がまだマシです。
前にJavaScriptのイベントをたくさん見られるサイトを作ったので、イベントに不慣れな方はぜひ。
実装例
<div id="container">
ここにコンテンツ
</div>
class MomentumScroll {
constructor(selector) {
this.container = document.querySelector(selector)
this.scrollY = 0
this.translateY = 0
this.speed = 0.1
this.rafId = null
this.isActive = false
this.scrollHandler = this.scroll.bind(this)
this.resizeHandler = this.resize.bind(this)
this.run()
}
run() {
if (this.isActive) {
return
}
this.isActive = true
this.on()
this.setStyles()
}
destroy() {
if (!this.isActive) {
return
}
this.isActive = false
this.off()
this.clearStyles()
if (this.rafId) {
cancelAnimationFrame(this.rafId)
}
this.rafId = null
}
resize() {
document.body.style.height = `${this.container.clientHeight}px`
}
scroll() {
this.scrollY = window.scrollY || window.pageYOffset
if (!this.rafId) {
this.container.style.willChange = 'transform'
this.rafId = requestAnimationFrame(() => this.render())
}
}
on() {
this.resize()
this.scroll()
window.addEventListener('scroll', this.scrollHandler, { passive: true })
window.addEventListener('resize', this.resizeHandler)
window.addEventListener('load', this.resizeHandler)
}
off() {
window.removeEventListener('scroll', this.scrollHandler)
window.removeEventListener('resize', this.resizeHandler)
window.removeEventListener('load', this.resizeHandler)
}
setStyles() {
this.container.style.position = 'fixed'
this.container.style.width = '100%'
this.container.style.top = 0
this.container.style.left = 0
}
clearStyles() {
document.body.style.height = ''
this.container.style.position = ''
this.container.style.width = ''
this.container.style.top = ''
this.container.style.left = ''
this.container.style.transform = ''
this.container.style.willChange = ''
}
render() {
const nextY = this.translateY + (this.scrollY - this.translateY) * this.speed
const isNear = Math.abs(this.scrollY - nextY) <= 0.1
this.translateY = isNear ? this.scrollY : nextY
const roundedY = Math.round(this.translateY * 100) / 100
this.container.style.transform = `translate3d(0, -${roundedY}px, 0)`
if (isNear) {
this.rafId = null
this.container.style.willChange = ''
} else {
this.rafId = requestAnimationFrame(() => this.render())
}
}
}
document.addEventListener('DOMContentLoaded', () => {
const momentumScroll = new MomentumScroll('#container')
// document.body.addEventListener('click', () => {
// if (momentumScroll.isActive) {
// momentumScroll.destroy()
// } else {
// momentumScroll.run()
// }
// })
})
Codepen
See the Pen Minimal Momentum Scroll by nishinoshake (@nishinoshake) on CodePen.
スマホを除外したい
iPhoneやAndroidで実行したくない場合は、ユーザーエージェントを確認して、PCのブラウザの時だけ実行してください。
ゆったりさせるところの抜粋
スピードをパラメータで設定して、段階的に適用していくイメージ。
// 必要なところだけ
this.scrollY = 0
this.translateY = 0
this.speed = 0.1
// スクロール時に値を設定
this.scrollY = window.scrollY || window.pageYOffset
// レンダリング時に少しづつ実際のスクロール値に近づける
const nextY = this.translateY + (this.scrollY - this.translateY) * this.speed
他に良い実装があったら知りたい
できる限りの最適化はしているつもりですが、良い実装があったら教えて下さい!