最近、Next.jsをつかってページスクロールを管理する実験を行ってます。
Next.jsを使ってページスクロールに連動してクロールするサイトを作りました。https://t.co/Mdth7el9XS pic.twitter.com/LZwbleg1yv
— 君塚史高 (@ki_230) 2021年1月15日
Next.js + Three.jsでページスクロールに連動してダンスを踊るサイトを作りました。
— 君塚史高 (@ki_230) 2021年1月19日
🕺 https://t.co/0kkwD8lqkm pic.twitter.com/61c5XzkPFh
ものすごくよく見ないとわからない成果ですが、Next.jsでページスクロールにイージングをつけました。
— 君塚史高 (@ki_230) 2021年1月28日
🖱 https://t.co/TOUoUkj7sC pic.twitter.com/nGhJUiKifZ
Next.jsを使って、利き手に合わせて斜めにスクロールするウェブサイトをつくりました。
— 君塚史高 (@ki_230) 2021年2月9日
🖱 https://t.co/iPfFW73TpN pic.twitter.com/GU8HKTM5D8
で、色々試していく中で、この仕組みをカスタムフックにしておくと、使いまわしやすいのではないかと思いまして、挑戦してみました。
useScroll.tsx
import { useEffect, useState } from 'react'; import Animation from './Animation'; const useScroll = (id: string = '__next'): [ number, (targetProgress: number) => void ] => { const [ direction, setDirection ] = useState(''); const [ progress, setProgress ] = useState(0); const [ lastProgress, setLastProgress ] = useState(0); const [ scrollProgress, setScrollProgress ] = useState(null); const [ contentsHeight, setContentsHeight ] = useState(0); const [ windowHeight, setWindowHeight ] = useState(0); const [ scrollY, setScrollY ] = useState(0); const [ animation, setAnimation ] = useState(new Animation({ startValue: 0, targetValue: 0, duration: 1 })); useEffect(() => { if (!direction) { init(); return; } }, [direction]); useEffect(() => { const diff = lastProgress - progress; if (diff < 0) { setDirection('down'); } else { setDirection('up'); } setLastProgress(progress); }, [progress]); useEffect(() => { if (typeof scrollProgress !== 'number') { return; } window.scrollTo(window.scrollX, (contentsHeight - windowHeight) * scrollProgress); setScrollProgress(null); }, [scrollProgress]); useEffect(() => { if (contentsHeight - windowHeight) { setProgress(scrollY / (contentsHeight - windowHeight)); } }, [scrollY]); useEffect(() => { window.addEventListener('mousedown', cancelScroll, { passive: false }); window.addEventListener('wheel', cancelScroll, { passive: false }); window.addEventListener('touchstart', cancelScroll, { passive: false }); return () => { animation.stop(); window.removeEventListener('mousedown', cancelScroll); window.removeEventListener('wheel', cancelScroll); window.removeEventListener('touchstart', cancelScroll); } function cancelScroll() { animation.stop(); } }, [animation]); function init() { window.addEventListener('resize', handleResize, { passive: true }); document.addEventListener('scroll', handleScroll, { passive: true }); handleResize(); setScrollProgress(normalize(0)); } function handleResize() { setContentsHeight(document.getElementById(id).clientHeight); setWindowHeight(window.innerHeight); } function handleScroll() { setScrollY(window.scrollY); } function normalize(val: number): number { return Math.max(0, Math.min(val, 1)); } function setProgressWithAnimation(targetProgress: number) { const startProgress = progress; const diff = targetProgress - startProgress; setAnimation( new Animation({ startValue: 0, targetValue: targetProgress, duration: 800, easing: 'easeInOut', step: (val: number) => { val = startProgress + diff * val; setScrollProgress(normalize(val)); } }) ); } return [ progress, setProgressWithAnimation ]; }; export default useScroll;
ざっくり書くと、こんな感じです。
別途読み込んでいる Animation.ts はこちらです。
DEMO
こんな感じでつかいます。
https://kimizuka.github.io/next-scroll/scroll-hook/
const [ progress, setProgressWithAnimation ] = useScroll();
探り探り実装してみたものの、引数に初期値を渡さない設計(ドキュメントルートのIDを渡す)になっているのが気がかりです。良いのかな。
でも、少なくとも自分で使う分には便利に使えているのでオーケーとしましょう。