Skip to content

Instantly share code, notes, and snippets.

@kobitoDevelopment
Last active June 29, 2022 12:45
Show Gist options
  • Select an option

  • Save kobitoDevelopment/240f5a24cad236021776967bee4bf775 to your computer and use it in GitHub Desktop.

Select an option

Save kobitoDevelopment/240f5a24cad236021776967bee4bf775 to your computer and use it in GitHub Desktop.
<div class="js-scroll-area">
<div class="js-scroll-slider" data-scroll-area=".js-scroll-area" data-auto-speed-ratio="0.5" data-scroll-ratio="0.06">
<div class="js-scroll-slider__item"><img src="https://picsum.photos/id/275/400/400" alt=""></div>
<div class="js-scroll-slider__item"><img src="https://picsum.photos/id/200/400/400" alt=""></div>
<div class="js-scroll-slider__item"><img src="https://picsum.photos/id/1062/400/400" alt=""></div>
<div class="js-scroll-slider__item"><img src="https://picsum.photos/id/1003/400/400" alt=""></div>
<div class="js-scroll-slider__item"><img src="https://picsum.photos/id/1001/400/400" alt=""></div>
</div>
<div class="js-scroll-slider is-reverse" data-scroll-area=".js-scroll-area" data-auto-speed-ratio="0.5" data-scroll-ratio="0.06">
<div class="js-scroll-slider__item"><img src="https://picsum.photos/id/270/400/400" alt=""></div>
<div class="js-scroll-slider__item"><img src="https://picsum.photos/id/10/400/400" alt=""></div>
<div class="js-scroll-slider__item"><img src="https://picsum.photos/id/103/400/400" alt=""></div>
</div>
</div>
//要素数が変わっても一定の速度を保つ等速スクロールスライダー縦(ドラッグ動作、スクロール連動対応)
const scrollSlider = document.querySelectorAll(".js-scroll-slider");
if (scrollSlider.length > 0) {
scrollSlider.forEach(function (slider) {
//スクロールスピード設定
const speed = slider.dataset.autoSpeedRatio ? Number(slider.dataset.autoSpeedRatio) : 0.5;
//スライド要素を取得
const children = slider.children;
const childLength = children.length;
//スライド要素一式を文字列で取得(複製用)
let baseChildren = "";
for (let i = 0; i < children.length; i++) {
baseChildren += children[i].outerHTML;
}
//スライド要素の横幅を取得
const firstChild = slider.firstElementChild;
//スライダーの表示エリア
const area = document.querySelector(slider.getAttribute("data-scroll-area"));
//スライダー定義関数
let sliderHeight, areaHeight;
let countHeight = 0;
let addCount = 1;
const initializeSlider = function (countHeight, addCount) {
//表示領域の高さを取得
areaHeight = area.clientHeight;
//スライダー全体の高さを取得
const styles = getComputedStyle(firstChild);
const height = parseFloat(styles.height);
const marginBottom = parseFloat(styles.marginBottom);
sliderHeight = (height + marginBottom) * childLength;
//画面外まで表示を確保するため要素を複製
const checkHeight = areaHeight * 3;
countHeight = sliderHeight * addCount;
while (countHeight < checkHeight) {
slider.insertAdjacentHTML("beforeend", baseChildren);
++addCount;
countHeight = sliderHeight * addCount;
}
//スライダーの初期値設定
slider.style.marginTop = "-" + sliderHeight + "px";
};
initializeSlider(countHeight, addCount);
/*
アニメーション設定
*/
let requestID;
let y = 0;
//ウィンドウの高さ取得
let winHeight = window.innerHeight;
window.addEventListener("resize", function () {
winHeight = window.innerHeight;
});
//イージング(線形補完)
const lerp = function (x, y, p) {
return x + (y - x) * p;
};
//スクロール連動用変数
let startScrollY = 0;
let easingScrollY = 0;
let diffScrollY = 0;
const baseAmount = 1;
//反転処理判別
const isReverse = slider.classList.contains("is-reverse");
//スクロールイベント関数
const scrollRatio = slider.dataset.scrollRatio ? Number(slider.dataset.scrollRatio) : 0.05;
//表示エリアの位置
let areaPosi = area.getBoundingClientRect().top;
//スクロール関数
const scrollAnime = function () {
//スクロール開始位置からのスクロール量
startScrollY = winHeight - areaPosi;
//イージング(線形補完)を適用したスクロール量。startScrollYから少し遅れて追従する
easingScrollY = lerp(easingScrollY, startScrollY, scrollRatio);
//startScrollYとeasingScrollYの差。スクロールが止まれば、いずれゼロに近づく
diffScrollY = startScrollY - easingScrollY;
//スクロールによって増減する増加量
const scrollAmount = Math.abs(diffScrollY) / 10;
const getY = (baseAmount + scrollAmount) * speed;
//値を設定
if (isReverse) {
y += getY;
} else {
y -= getY;
}
//スライダーの終了ポイントを過ぎていたら位置を戻す
if ((!isReverse && y <= -sliderHeight) || (isReverse && y >= sliderHeight)) {
y = 0;
}
slider.style.transform = "translateY(" + y + "px)";
//画面内の時だけ再帰処理
if (isIntersecting) {
requestID = requestAnimationFrame(scrollAnime);
}
};
/*
ドラッグ対応
*/
//スライダーのstyleを取得
const sliderStyles = getComputedStyle(slider);
//クリック、タッチされているか判別
let isDown = false;
//ドラッグで下に動かしているか判別
let isBottomMove = false;
// クリック開始位置を保存
let startY;
//スライダー位置を保存
let sliderY;
//画面内に存在しているか判別
let isIntersecting = false;
//マウスダウン、タッチスタート(移動開始)
const startFunc = function (e) {
e.preventDefault();
slider.style.transition = "";
//画面上のy位置を取得
startY = e.pageY;
//スライダーにclass追加
slider.classList.add("is-drag");
isDown = true;
//マウスポインタを変更
slider.style.cursor = "grabbing";
//自動スクロール停止
cancelAnimationFrame(requestID);
//スライダー位置取得
sliderY = parseFloat(new DOMMatrix(sliderStyles.transform).m42);
};
//マウスムーブ、タッチムーブ(移動中)
const moveFunc = function (e) {
//マウスダウン、タッチスタート時のみ処理
if (!isDown) {
return;
}
e.preventDefault();
//移動距離を取得
let moveY = startY - e.pageY;
//上下どちらに移動しているか判別
if (moveY < 0) {
isBottomMove = true;
} else {
isBottomMove = false;
}
//スライダーを移動させる
requestID = requestAnimationFrame(function () {
y = sliderY - moveY;
slider.style.transform = "translateY(" + y + "px)";
});
};
//マウスアップ、タッチエンド(移動終了)
const endFunc = function (e) {
e.preventDefault();
//設定をリセット
slider.classList.remove("is-drag");
isDown = false;
//マウスポインタを変更
slider.style.cursor = "pointer";
//スライダーの終了ポイントを過ぎていたら位置を戻す
if (!isBottomMove && y * -1 >= sliderHeight) {
y = sliderHeight - y * -1;
slider.style.transform = "translateY(" + y + "px)";
} else if (isBottomMove && y >= 0) {
y = -(sliderHeight - y);
slider.style.transform = "translateY(" + y + "px)";
}
//アニメーションをリセットし画面内の場合は自動スクロール開始
cancelAnimationFrame(requestID);
if (isIntersecting) {
requestID = requestAnimationFrame(scrollAnime);
}
};
//マウスドラッグ対応
slider.addEventListener("mouseenter", function () {
//マウスポインタを変更
slider.style.cursor = "pointer";
});
slider.addEventListener("mousedown", startFunc);
slider.addEventListener("mousemove", moveFunc);
slider.addEventListener("mouseup", endFunc);
slider.addEventListener("mouseleave", endFunc);
//スクロール処理
const listener = {
handleEvent: function () {
//表示エリアの位置を更新(レイアウトシフトを起こすので、変更が起きたときのみrequestAnimationFrame外で処理する)
areaPosi = area.getBoundingClientRect().top;
},
};
/*
要素が画面内に入ったら処理開始
*/
const observer = new IntersectionObserver(function (entries) {
entries.forEach(function (entry) {
if (entry.isIntersecting) {
cancelAnimationFrame(requestID);
//自動スクロール開始
requestID = requestAnimationFrame(scrollAnime);
//画面に表示されている要素のみスクロールイベントへ追加
window.addEventListener("scroll", listener, { passive: true });
//画面内判定フラグ
isIntersecting = true;
} else {
//自動スクロール解除
cancelAnimationFrame(requestID);
//画面外のときはスクロールイベント削除
window.removeEventListener("scroll", listener, { passive: true });
//画面内判定フラグ
isIntersecting = false;
}
});
});
//observer監視開始
observer.observe(area);
/*
レスポンシブ対応
*/
//表示エリアリサイズ監視 ResizeObserver
const resizeObserver = new ResizeObserver(function () {
//スライダーが途切れないかチェック(画面外の要素が足りなければ追加する)
initializeSlider(countHeight, addCount);
//スライダー再定義
cancelAnimationFrame(requestID);
requestID = requestAnimationFrame(scrollAnime);
});
//リサイズ監視開始(スライダー本体ではなく、最初のスライダー内要素が可変すると発火)
resizeObserver.observe(firstChild);
});
}
.js-scroll-area {
position: relative;
overflow: hidden;
height: 100vh;
.js-scroll-slider {
position: absolute;
top: 0;
left: 0;
width: 45%;
&.is-scroll {
transition: transform 0.05s;
}
.js-scroll-slider__item {
margin-bottom: 20px;
}
& + .js-scroll-slider {
left: auto;
right: 0;
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment