JavaScript 縦スクロールを横スクロールに変換してとどめたい
スクロールしても一定量、スクロールしないと動かない、そんな仕組みを作りたいと思いまして。
何言ってるか伝わりづらいと思うのですが、ページを閲覧するときにスクロールしますよね。
で、縦にスクロールするのが普通だと思うんですが、それの移動量を横スクロールに変換したいんです。
正確に言うと、横スクロールに変換することで、その領域に強制的にとどめる仕組みを作りたかったと。
なお、この手のどこに需要があるのかわからない解説記事は大体自分のために書いております。
言葉で伝えてもわかりづらいと思うので以下のサンプルをご覧ください。
横スクロールエリア
一定量、スクロールしないと先に進みません。
スクロール量に応じて、画像が変化します。
サンプル画像のカミソリは「kazakiri」というブランドの1枚刃の替刃が安いオススメのカミソリです。
よかったらこちらの記事もどうぞ。
画像をじっくり見てほしいわけです。
なんでこんなものを作ったかと言いますと、画像をじっくりみて欲しいためです。
正確には、変化する画像をちゃんと見てほしい、というか、強制的に見させたかったと。
動作の仕様と仕組みの解説
どういう仕組みで動いているかというと、スクロールをとどめるCSSの「postion:sticky」を使って表示エリアを固定します。
裏側で、横スクロールエリアが設定されていまして、縦スクロールを横スクロールに変換しています。
そのスクロール量を%(パーセンテージ)に変換して、画像のアルファ値(透明度)を変換して、画像を徐々に変化させております。
画像変化について
同じサイズの画像を用意しまして、同じ位置に画像を重ねます。
ベースの画像を最初において、徐々に出てくる画像を後ろに置いて、2枚目の後ろに置いてある画像に対してアルファ値の変更をかけるわけです。
それにより、スクロールに応じて画像が徐々に変化するモーションが作れます。
コード解説と謝辞
思いついたきっかけとして、スクロールを無視するアクションの方法を考えたときに、確か横スクロールさせるスクリプトあったよな、と思い検索しました。
そこで参考にさせていただいたのが UNICO LABO様の以下のページです。
横スクロールさせるスクリプトはいくつか有ったのですが、解説がわかりやすく、素のJavaScriptで作られていて、応用するのが簡単そうだったため。
ベースのコードはUNICO LABO様をご覧ください
横スクロールさせる仕組みは大本のUNICO LABO様のページをご覧いただくのがわかりやすいと思います。
サンプルのコードも公開されていて、DEMOページもご用意されています。
改造を施した箇所とそれぞれの数値変化
コードはサンプルをそのまま貼り付ければ、実装できるわけですが、それを応用しようとすると、どういう仕組みで動いているかを理解する必要があります。
加えて、私が実現したい内容がちょっと今回のものと異なるので、こちらをベースに手を加えていきます。
基本動作を再確認する
改造したJavaScriptの全体です。
このBLOGで使うために、2カラムでも動くように変更したのと、横スクロールを発動させる係数をちょっといじりました。
その他、クラス名やタグをちょっと変更しております。
スクロールエリアのHTML
<div class="sc_change_wrapper"> <div class="sc_sticky"> <h2>横スクロールエリア</h2> <p>一定量、スクロールしないと先に進みません。<br> スクロール量に応じて、画像が変化します。</p> <div class="sc_item_set"> <img src="画像のパス" width="760" height="507" > <img src="画像のパス" width="760" height="507" id="test" class="abs"> </div> <div class="sc_scroll_wrapper"> <div> a </div> <div> b </div> </div> </div> </div>
改造したJavaScript
"use strict"; window.addEventListener("load", () => { const screenWidth = window.innerWidth; const blog_body = document.getElementById("blog_body"); let window_size = document.body.clientWidth; let window_margin = 0; if (screenWidth > 768) { window_size = blog_body.clientWidth; window_margin = window_size; } const stickyContainers = document.querySelectorAll(".sc_change_wrapper"); stickyContainers.forEach((stickyContainer, index) => { const stickyItem = stickyContainer.querySelector(".sc_sticky"); const scroller = stickyContainer.querySelector(".sc_scroll_wrapper"); scroller.classList.add("nobar"); const updateStickyHeight = () => { const stickyHeight = scroller.scrollWidth - scroller.clientWidth + stickyItem.clientHeight; stickyContainer.style.setProperty("--sticky-container-height", `${stickyHeight}px`); }; updateStickyHeight(); new ResizeObserver(updateStickyHeight).observe(scroller); const syncScroll = () => { const rect = stickyContainer.getBoundingClientRect(); if (rect.top <= 0 && rect.bottom >= window.innerHeight) { scroller.scrollLeft = rect.top * -1; }else if(rect.top >= 1){ scroller.scrollLeft = 0; } const alfa = scroller.scrollLeft / (scroller.scrollWidth - window_size); document.getElementById("test").style.opacity = alfa; document.getElementById("test2").style.opacity = alfa; }; const boundSyncScroll = syncScroll.bind(this); const observer = new IntersectionObserver( (entries) => { entries.forEach((entry) => { if (entry.isIntersecting) { window.addEventListener("scroll", boundSyncScroll, { passive: true }); syncScroll(); } else { window.removeEventListener("scroll", boundSyncScroll); } }); }, { threshold: 0 } ); observer.observe(stickyContainer); }); });
スクロール発動のJavaScriptを解説
const syncScroll = () => { const rect = stickyContainer.getBoundingClientRect(); if (rect.top <= 0 && rect.bottom >= window.innerHeight) { scroller.scrollLeft = rect.top * -1; }else if(rect.top >= 1){ scroller.scrollLeft = 0; } const alfa = scroller.scrollLeft / (scroller.scrollWidth - window_size); document.getElementById("test").style.opacity = alfa; document.getElementById("test2").style.opacity = alfa; };
stickyContainerは、stickyのかかる領域になります。
const rect = stickyContainer.getBoundingClientRect();
で、stickyContainerの位置情報を取得し、そのTOPの位置が0以下の場合、かつ、下端がビューポートの下部よりしたの場合に発動します。
それがこのif文です。
if (rect.top <= 0 && rect.bottom >= window.innerHeight)
rectで指定されているエリアの位置情報の変動がよくわからなかったので、console.logで確認しながら、望み通りの数値が取れるように調整しました。
数値変動を表記させたものが以下です。
横スクロールのエリアはheight:0にして非表示にしていますが、それも表示するようにしてあります。
▼変動する数値の詳細
rect_top:0
rect_bottom:0
window.innerHeight:0
scroller.scrollWidth:0
window_size:0
scrollLeft:0
alfa:0
window.scrollY:0
スクロール量の取得と変換
rect.topが指しているのが以下。
const rect = stickyContainer.getBoundingClientRect();
stickyContainerがヴューポートの上端からどれだけ離れているかを取得できます。
stickyは常に指定した位置に留まるのですが、それは指定した要素の上部に疑似的に固定されているだけで、要素(divコンテナ)はスクロールされているため、数値が変動します。
なので、この数値をそのまま横スクロール量に当てました。
ただ、上端が上に行くほどマイナス値になるので、-1を変えて整数に変換しています。
scroller.scrollLeft = rect.top * -1;
アルファ値の取得と変換
スクロール量に応じてアルファ値(透明度)に変換します。
const alfa = scroller.scrollLeft / (scroller.scrollWidth - window_size);
横スクロールの移動量を表示領域に対して、何%移動したかを取得しています。
window_sizeはブログの記事エリアの横幅を取得しています。
レスポンシブでPCとスマホで横幅が変わるので表示サイズに応じて取得する領域を変更しています。
const screenWidth = window.innerWidth; const blog_body = document.getElementById("blog_body"); let window_size = document.body.clientWidth; let window_margin = 0; if (screenWidth > 768) { window_size = blog_body.clientWidth; window_margin = window_size; }
スクロールエリアの横幅から、表示領域を引いて、それをスクロール量で割ることで、パーセンテージを算出し、それをアルファ値にあてていると。
スクロール量を増やせば透明化の速度を調整できる
横スクロールエリアの要素を増やしたり、横幅のサイズを調整すると、スクロール量が増えます。
ので、増えた分だけ透明化の速度が遅くできます。
<div class="sc_scroll_wrapper"> <div> a </div> <div> b </div> </div>
透明度の調整以外もいろいろできそうな予感
まだやってないんですけど、この仕組みを応用することで、色々おもしろいことができそうな予感があります。
CSSのアニメーションを組み合わせたり、シンプルに横スクロールでなにかを表示させたり。
効果的に使えるかどうかが未知数ですが、なにか思いついたら組み込んでみたいと思っています。
ユーザー体験としてどうなのかがちょっと心配
参照元のUNICO LABOさんも書かれていましたが、ユーザー体験的に、スクロールしてもスクロールしないという動作が受け入れられるかどうかがちょっと不安ですね。
スクロールしたことで起きている変化がわかりづらいと、とまどいを与えそうではあります。
そういう違和感を起こさせること自体は必ずしも悪いことじゃないと個人的には思っていますが、それが不快に感じられるとマイナスにしかならないので、使い方を間違えないようにしたいと思います。
使用上の注意と対策
色々試行錯誤をして、違和感なく動くようにひとまず作れたのですが、1つだけ問題点があります。
sticky要素の高さが、横スクロールエリアよりも小さい場合、スクロールが途中で終了します。
これは、stickyでとどめていた要素の高さ分、横スクロールに変換しているのですが、縦スクロールが終わってしまうと、変換するスクロール量がなくなるため。
これを防ぐために、stickyエリアに充分な要素を確保する必要があります。
インターセクションオブザーバーを覚えておいてよかった
今回のコードに交差監視のインターセクションオブザーバーがでてきましたが、すでに私は解説記事を通して理解を深めておいた良かったと思います!
ここの仕組みをちゃんと理解してなければ(ちゃんとは理解してないんだけど)、あそこで躓いていたと思います。
インターセクションオブザーバーの解説記事はこちら