👌
スクロールした時、指定した画面領域内にいる時のみアニメーションを実行するテスト
スクロールした際、指定した画面領域内でのみアニメーションを発火させるサンプルです。
交差オブザーバーで検知しているので、scrollイベントよりパフォーマンスは良いかと思います。
どちびさん https://zenn.dev/con_ns_pgm が作成したアニメーションクラス
サカタさん https://zenn.dev/sakata_kazuma が追加したRAF,FPS関係を落とし込み
対象をquerySelectorからquerySelectorAllに変更、rootMarginは数値のみでなく"-10%"など%対応、初期位置追加など改良を加えました。
new ScrollFunctionAll(".js-sf-target01", 500, -150, 300, 1000);
new ScrollFunctionAll(".js-sf-target02", 500, "-30%", 300, 1000);
new ScrollFunctionAll(".js-sf-target03", 500, "-30%", "5%", "80%");
'use strict';
import { GetScrollNum } from './_GetScrollNum';
export class ScrollFunctionAll {
constructor(target, scrollMax, rootMargin, min, max, fps = 60) {
this.target = document.querySelectorAll(target); //ターゲット
if (!this.target) return;
this.scrollMax = scrollMax; //任意のスクロール量の最大値
this.rootMargin = rootMargin; //画面のどこにtargetが来たら検知開始するか
this.min = min; //任意の範囲の最小値
this.max = max; //任意の範囲の最大値
this.framesPerSecond = fps; //フレームレート値
this.resultNormal = 0; //linearでmin~maxの間を変化する変数
this.resultEase = 0; //イージングを適応してmin~maxまで変化する変数
this._scrollAnimation();
}
_scrollAnimation() {
const _that = this;
let rootMarginPercentFlg = false;
const rootMarginPercent = _that.rootMargin;
let rootMarginNum;
if(typeof rootMarginPercent == "string" && rootMarginPercent.includes("%")) {
rootMarginPercentFlg = true;
const length = rootMarginPercent.lastIndexOf("%");
const regexp = new RegExp('.{0,' + length + '}', 'g');
rootMarginNum = rootMarginPercent.match(regexp)[0];
}
+ /**
+ * 最小値・最大値が%だった時の処理
+ */
+ let winWidth;
+ const percentProcessingMin = () => {
+ let convertedNum;
+ if(typeof this.min == "string" && this.min.includes("%")) {
+ winWidth = window.innerWidth;
+ const length = this.min.lastIndexOf("%");
+ const regexp = new RegExp('.{0,' + length + '}', 'g');
+ convertedNum = this.min.match(regexp)[0];
+ this.min = winWidth * convertedNum /100;
+ }
+ }
+ percentProcessingMin();
+ const percentProcessingMax = () => {
+ let convertedNum;
+ if(typeof this.max == "string" && this.max.includes("%")) {
+ winWidth = window.innerWidth;
+ const length = this.max.lastIndexOf("%");
+ const regexp = new RegExp('.{0,' + length + '}', 'g');
+ convertedNum = this.max.match(regexp)[0];
+ this.max = winWidth * convertedNum /100;
+ }
+ }
+ percentProcessingMax();
this.target.forEach(el => {
/*
リフローを起こす処理はスクロール中に実行しない
https://gist.github.com/paulirish/5d52fb081b3570c81e3a
*/
let offsetTop; //ページの一番上からの位置
let endPosition; //要素の終了位置
let winHeight; //ウィンドウ高さ
/**
* ポジション取得
*/
const getPosition = () => {
const scrollTop = window.pageYOffset;
/*
getBoundingClientRect().topは画面上の相対値を返すので、
「現在のスクロール量 + 画面内の相対値」で
ページの一番上からの位置が取得できる(jQueryのoffset().topと同じ)
*/
offsetTop = el.getBoundingClientRect().top + scrollTop;
//要素の終了位置 clientHeightやinnerHeightなどサイズ取得系もリフローが発生する
endPosition = offsetTop + el.clientHeight;
winHeight = window.innerHeight;
}
getPosition();
window.addEventListener('resize', getPosition);
//requestAnimationFrameをリセットするためのIDを格納
let frameId;
//直前のスクロール位置を格納する変数
let lastPosition = -1;
//繰り返し処理を行っても良いか判別用フラグ
let isVisible = false;
/**
* フレームレート設定
* 参考:https://www.kirupa.com/animations/fixing_frame_rate_for_consistent_animations.htm
*/
// const framesPerSecond = 60;
const interval = Math.floor(1000 / _that.framesPerSecond);
const startTime = performance.now();
let previousTime = startTime;
let currentTime = 0;
let elapsed = 0;
+ //要素の初期位置
+ el.setAttribute("style", `transform: translateX(${_that.min}px)`);
/**
* スクロールアニメ実行関数
*/
const scrollAnime = (timestamp) => {
/*
window.pageYOffsetは↓のリフローを起こすリストにもないので、
リフローは発生していない(?)
https://gist.github.com/paulirish/5d52fb081b3570c81e3a
以下の記事でも「window.pageYOffsetで取得しろ」と書いてありました。
https://gist.github.com/Warry/4254579#beware-of-reflows
"use window.pageYOffset to get the scroll position"
*/
//スクロール量を取得
const scrollTop = window.pageYOffset;
//直前のスクロール位置と比較して変化がない場合
if (lastPosition === scrollTop) {
//リクエストID取得
frameId = requestAnimationFrame(scrollAnime);
return;
}
//直前のスクロール位置と比較して変化がある場合のみ、直前の位置を更新
lastPosition = scrollTop;
/**
* フレームレート調整
* https://www.kirupa.com/animations/fixing_frame_rate_for_consistent_animations.htm
*/
currentTime = timestamp;
elapsed = currentTime - previousTime;
if (elapsed <= interval) {
frameId = requestAnimationFrame(scrollAnime);
return;
}
previousTime = currentTime - (elapsed % interval);
//画面下部のスクロール位置
const scrollBottom = scrollTop + winHeight;
//isIntersecting領域に入る位置を取得する関数
function targetScroll() {
// return scrollTop - offsetTop + innerHeight + this.rootMargin;
if(rootMarginPercentFlg) {
return scrollTop - offsetTop + winHeight + ( winHeight * rootMarginNum / 100);
}
return scrollTop - offsetTop + winHeight + _that.rootMargin;
}
//アニメーション量の計算
const calculation = () => {
const normalizeNum = _that._norm(targetScroll(), 0, _that.scrollMax); //0~1
const easeNum = _that._easeInOutBack(normalizeNum); //イージングが効いた値
_that.resultNormal = _that._lerp(_that.min, _that.max, normalizeNum);
_that.resultEase = _that._lerp(_that.min, _that.max, easeNum);
}
calculation();
- //要素の初期位置
- el.style.transform = `translateX(${_that.min}px)`;
//要素が画面下から完全に出るまでの範囲
if (scrollBottom > offsetTop && scrollBottom < endPosition) {
//console.log('こんにちはあああああ!!');
//要素が画面上から出ていくまでの範囲
} else if (scrollTop > offsetTop && scrollTop < endPosition) {
//console.log('さようならあああああ!!');
//画面内で完全に見えている状態
} else if (scrollBottom > offsetTop && scrollTop < endPosition) {
//console.log('完全に画面内にいるよおおおお!!');
if (_that.resultNormal < _that.min) {
_that.resultNormal = _that.min
} else {
if (_that.resultNormal > _that.max) {
_that.resultNormal = _that.max
}
}
- el.style.transform = `translateX(${_that.resultNormal}px)`;
+ el.setAttribute("style", `transform: translateX(${_that.resultNormal}px)`);
}
/*
entry.isIntersectingがtrueの時だけループする
これがないと画面外に出た時、
出る直前に実行された関数でループ処理が持続してしまうことがある
*/
if (isVisible) {
frameId = requestAnimationFrame(scrollAnime);
}
}
frameId = requestAnimationFrame(scrollAnime);
//画面に入った時だけ処理する
const observer = new IntersectionObserver(entries => {
entries.forEach(entry => {
isVisible = entry.isIntersecting;
if (isVisible) {
frameId = requestAnimationFrame(scrollAnime);
} else {
//画面外のときは削除
cancelAnimationFrame(frameId);
}
});
});
//observer監視開始
observer.observe(el);
});
}
/**
* イージング関数
*/
_easeInOutQuint(x) {
return x < 0.5 ? 16 * x * x * x * x * x : 1 - Math.pow(-2 * x + 2, 5) / 2;
}
_easeInQuint(x) {
return x * x * x * x * x;
}
_easeInOutBack(x) {
const c1 = 1.70158;
const c2 = c1 * 1.525;
return x < 0.5
? (Math.pow(2 * x, 2) * ((c2 + 1) * 2 * x - c2)) / 2
: (Math.pow(2 * x - 2, 2) * ((c2 + 1) * (x * 2 - 2) + c2) + 2) / 2;
};
//正規化
_norm(v, a, b) {
return (v - a) / (b - a);
}
//線形補完
_lerp(a, b, t) {
return a + (b - a) * t;
}
}
'use strict';
export class GetScrollNum {
constructor() {
this.scrollY = scrollY;
this.normalizeScrollY = "";
this.scrollRate = "";
this.viewH = document.documentElement.scrollHeight;
this.totalScrollY = this.viewH - innerHeight;
this._getResult();
}
_getResult() {
this.scrollY = window.pageYOffset;//スクロール量
this.normalizeScrollY = this.scrollY / this.totalScrollY;//スクロール量正規化
this.scrollRate = Math.floor(this.normalizeScrollY * 100);//スクロール率
}
}
Discussion