50
39

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

アワード系のWebサイトで見かける、ゆったりした慣性スクロールの実装

Last updated at Posted at 2020-03-30

Lenis という慣性スクロールのライブラリが素敵です。

AwwwardsFWAに掲載されているサイトでよく見かける、ゆったりした慣性スクロール。

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

他に良い実装があったら知りたい

できる限りの最適化はしているつもりですが、良い実装があったら教えて下さい!

50
39
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
50
39

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?