IntersectionObserver + テキストアニメーション用クラスで沼ったところを共有したい
記事の目的
クラス構文は全くもって不慣れで、知見が浅いのですがCodemafia先生のUdemy講座を参考にして
タイトルのクラス二つを作成してみました。
しかし、コラボさせた時に何度やってもエラーになり困惑しました。
今回、解決(パワープレイ)できたので共有したいのと自分の整理のために記事を書きます。
コード全体像
document.addEventListener("DOMContentLoaded", function () {
const cb = function (el,isIntersecting) {
if (isIntersecting) {
const targetList = el.classList;//elにはDOMが渡ってくるのでクラスをListで取得
const target = '.' + targetList[0];//セレクタに書き換える
const ta = new SplitTextAnimation(target);
ta.fadeUpText();
}
}
const so = new ScrollObserver('.target',cb);
});
//----------------------------------------------------------------
//引数にDOMを渡して文字列分割テキストアニメーションをする用のクラス
class SplitTextAnimation {
constructor(el) {
this.el = document.querySelector(el);
this.chars = this.el.innerText.trim();
this.concatStr = "";
this.el.innerHTML = this._splitText(); //クラスに渡された引数が分割された状態のDOM
this.animations = [];
}
//テキストがcharクラスをもつspanで1文字ずつ分割される関数
_splitText() {
for (let c of this.chars) {
c = c.replace(" ", " ");
this.concatStr += `<span class="char">${c}</span>`;
}
return this.concatStr;
}
//1文字ずつfadeUpする
fadeUpText() {
//タイミング制御用オブジェクトを定義
let timings = {
easing: "ease-out",
fill: "forwards",
};
const chars = this.el.querySelectorAll(".char"); //指定した要素(el)のcharクラスを取得する
chars.forEach((char, i) => {
char.style.display = "inline-block";//デフォルトのスタイルを当てる
char.style.opacity = 0;
char.style.transform = "translateY(100px)";
timings.delay = i * 20;
timings.duration = 800;
const animation1 = char.animate(
[{ transform: "translateY(100px) " }, { transform: "translateY(0px) " }],
timings
);
animation1.cancel();
this.animations.push(animation1);
timings.duration = 800;
const animation2 = char.animate([{ opacity: 0 }, { opacity: 1 }], timings);
animation2.cancel();
this.animations.push(animation2);
});
this.animations.forEach((anim) => {
anim.play();
});
}
}
//使い方
//1.インスタンス化
// const ta = new SplitTextAnimation(ここにアニメーションさせたい要素のセレクタを渡す)
//2.アニメーション関数の実行
// ta.fadeUpText()
//----------------------------------------------------------------
//----------------------------------------------------------------
//IntersectionObserver用クラス
class ScrollObserver {
constructor(els, cb, options) {
this.els = document.querySelectorAll(els);
const defaultOptions = {
root: null, //交差対象
rootMargin: "0px", //交差判定境界線
threshold: 0//targetのどこで交差判定するか
};
this.cb = cb;
this.options = Object.assign(defaultOptions, options); //オブジェクトを合体させる
this._init();
}
//初期化
_init() {
const callback = function (entries, observer) {
entries.forEach(entry => {
if (entry.isIntersecting) {
//画面内に入った時
this.cb(entry.target, true);
observer.unobserve(entry.target); //監視を終了する
} else {
//画面外に出た時
this.cb(entry.target, false);
}
});
};
const io = new IntersectionObserver(callback.bind(this), this.options);
this.els.forEach(el => io.observe(el));
}
}
//使い方
//1.コールバック関数を定義する
// const cb = function (el, isIntersecting) {
// if (isIntersecting) {
// ここに画面内に入ったら行いたい処理をかく
// }
// }
//※上記のelにはセレクタではなくDOM自体が渡ることに注意する
//2.インスタンス化する
// const so = new ScrollObserver('監視対象セレクタ', cb, options:あってもなくても良い);
//----------------------------------------------------------------
詰まったところ
上記コードでは「画面内検知、一度検知したら監視終了、画面内に入ったらテキストアニメーションの実行」
までを行なっています。
今回詰まったところは、コメントアウトの注意書きでも書いていますが、以下です。
ScrollObserverに渡すcbのelにはDOMが格納されている
考えてみれば当たり前の話でした。
cbはつまり、this.cbな訳で
this.cbにはinitの中で「entry.target」と「boolean」が渡されています。
_init() {
const callback = function (entries, observer) {
entries.forEach(entry => {
if (entry.isIntersecting) {
//画面内に入った時
this.cb(entry.target, true);//この部分
observer.unobserve(entry.target); //監視を終了する
} else {
//画面外に出た時
this.cb(entry.target, false);//この部分
}
});
cbを定義する際に最初に僕がやっていたことは、以下です
const cb = function (el,isIntersecting) {
if (isIntersecting) {
const ta = new SplitTextAnimation(el);
ta.fadeUpText();
}
}
今回はScrollObserverクラスに「.target」クラスを渡しているので
そのDOMが
<p class='target'>TEXT</p>
だった場合、cbの中は以下のようになります。(elを置き換えています)
const cb = function (<p class='target'>TEXT</p>,isIntersecting) {
if (isIntersecting) {
const ta = new SplitTextAnimation(<p class='target'>TEXT</p>);
ta.fadeUpText();
}
}
そうなると、SplitTextAnimationクラスのコンストラクタ内でエラーが発生しました。
constructor(el) {
this.el = document.querySelector(el);//この部分
this.chars = this.el.innerText.trim();
this.concatStr = "";
this.el.innerHTML = this._splitText(); //クラスに渡された引数が分割された状態のDOM
this.animations = [];
}
上記のelにDOMが入ってくるので、querySelectorが実行できるわけがありません。
そんなこんなで、DOMからパワープレイで強引にセレクタに書き換えて困難を乗り切りました。
const cb = function (el,isIntersecting) {
if (isIntersecting) {
const targetList = el.classList;//elにはDOMが渡ってくるのでクラスをListで取得
const target = '.' + targetList[0];//セレクタに書き換える
const ta = new SplitTextAnimation(target);
ta.fadeUpText();
}
}
まとめ
とりあえず解決はしましたが、この書き方果たして良いのだろうか。いや、きっと良くない。
と思っています。そう思っているものを記事にするのもどうかと思いましたが、
クラス構文の練習になったので、学んだことを整理したいという思いで書きました。
あくまでご参考程度に(参考にもならんと思いますが)お願いします。
11/18追記:コード全体像
document.addEventListener("DOMContentLoaded", function () {
const cb = function (el,isIntersecting) {
const targetList = el.classList;//elにはDOMが渡ってくるのでクラスをListで取得
const target = '.' + targetList[0];//セレクタに書き換える
const ta = new SplitTextAnimation(target);
if (isIntersecting) {
ta.fadeUpText();
}
}
const so = new ScrollObserver('.target',cb);
});
//----------------------------------------------------------------
//引数にDOMを渡して文字列分割テキストアニメーションをする用のクラス
class SplitTextAnimation {
constructor(el) {
this.el = document.querySelector(el);
this.chars = this.el.innerText.trim();
this.concatStr = "";
this.el.innerHTML = this._splitText(); //クラスに渡された引数が分割された状態のDOM
this.animations = [];
this.chars = '';
this.transY = "170px";//transformY
this.outer = document.createElement('div');//対象の親にdivを追加
this._clip();
this._init();
}
//テキストがcharクラスをもつspanで1文字ずつ分割される関数
_splitText() {
for (let c of this.chars) {
c = c.replace(" ", " ");
this.concatStr += `<span class="char">${c}</span>`;
}
return this.concatStr;
}
//分割したテキストにデフォルトスタイルを付与
_init() {
this.chars = this.el.querySelectorAll(".char"); //指定した要素(el)のcharクラスを取得する
this.chars.forEach(char => {
char.style.display = 'inline-block';
// char.style.opacity = 0;
char.style.transform = `translateY(${this.transY})`;
})
}
//アニメーションの対象をdivで囲みclip-path
_clip() {
this.outer.classList.add('js-outer');//囲むdivにクラスを付与
this.el.parentNode.insertBefore(this.outer, this.el);//対象の親要素の子要素にdivを挿入
this.outer.appendChild(this.el);//生成したdivの中に対象を入れる
this.outer.style.clipPath = "polygon(0 0,100% 0,100% 100%,0 100%)";//divより外側をclip-pathで切り取る
}
//1文字ずつfadeUpする
fadeUpText() {
//タイミング制御用オブジェクトを定義
let timings = {
easing: "ease-in-out",
fill: "forwards",
};
let x,easing;
this.chars = this.el.querySelectorAll(".char"); //指定した要素(el)のcharクラスを取得する
this.chars.forEach((char, i) => {
x = i / (this.chars.length - 1);//0 ~ 1
const maxDelay = 170;//delay最大値
//イージング関数
function ease(x){
return x < 0.5 ? 8 * x * x * x * x : 1 - Math.pow(-2 * x + 2, 4) / 2;
}
easing = ease(x);
timings.delay = easing * maxDelay;
timings.duration = 550;
const animation1 = char.animate(
[{ transform: `translateY(${this.transY}) rotateZ(20deg)` }, { transform: "translateY(0px) rotateZ(0)" }],
timings
);
animation1.cancel();
this.animations.push(animation1);
const animation2 = char.animate([{ opacity: 0 }, { opacity: 1 }], timings);
animation2.cancel();
// this.animations.push(animation2);
});
this.animations.forEach((anim) => {
anim.play();
});
}
}
//使い方
//1.インスタンス化
// const ta = new SplitTextAnimation(ここにアニメーションさせたい要素のセレクタを渡す)
//2.アニメーション関数の実行
// ta.fadeUpText()
//----------------------------------------------------------------
//----------------------------------------------------------------
//IntersectionObserver用クラス
class ScrollObserver {
constructor(els, cb, options) {
this.els = document.querySelectorAll(els);
const defaultOptions = {
root: null, //交差対象
rootMargin: "-100px", //交差判定境界線
threshold: 0//targetのどこで交差判定するか
};
this.cb = cb;
this.options = Object.assign(defaultOptions, options); //オブジェクトを合体させる
this._init();
}
//初期化
_init() {
const callback = function (entries, observer) {
entries.forEach(entry => {
if (entry.isIntersecting) {
//画面内に入った時
this.cb(entry.target, true);
observer.unobserve(entry.target); //監視を終了する
} else {
//画面外に出た時
this.cb(entry.target, false);
}
});
};
const io = new IntersectionObserver(callback.bind(this), this.options);
this.els.forEach(el => io.observe(el));
}
}
//使い方
//1.コールバック関数を定義する
// const cb = function (el, isIntersecting) {
// if (isIntersecting) {
// ここに画面内に入ったら行いたい処理をかく
// }
// }
//※上記のelにはセレクタではなくDOM自体が渡ることに注意する
//2.インスタンス化する
// const so = new ScrollObserver('監視対象セレクタ', cb, options:あってもなくても良い);
//----------------------------------------------------------------
11/22追記(パワープレイから修正)
一応jsのコード全体も載せます。修正前コードもコメントアウトで残してあります。
document.addEventListener("DOMContentLoaded", function () {
//IntersectionObserver+テキストアニメーションで使用するコールバック関数を定義
// const cb = function (el, isIntersecting) {
// //画面内に入る前に初期化処理をする
// const targetList = el.classList;//elにはDOMが渡ってくるのでクラスをListで取得
// const target = '.' + targetList[0];//セレクタに書き換える
// const ta = new SplitTextAnimation(target);
// if (isIntersecting) {
// //画面内に入ったらアニメーションの実行
// ta.fadeUpText();
// }
// }
//elにはScrollObserver内のentry.targetが渡ってくる(DOM Element)
const cb = function (el, isIntersecting) {
const ta = new SplitTextAnimation(el);
if (isIntersecting) {
ta.fadeUpText();
}
}
//.targetに対してfadeUpアニメーションを実行
// const so = new ScrollObserver('.target',cb);
const so = new ScrollObserver(document.querySelectorAll('.target'),cb);
});
//----------------------------------------------------------------
//引数にDOMを渡して文字列分割テキストアニメーションをする用のクラス
class SplitTextAnimation {
constructor(el) {
// this.el = document.querySelector(el);
this.el = el;//Elementを渡す
this.chars = this.el.innerText.trim();
this.concatStr = "";
this.el.innerHTML = this._splitText(); //クラスに渡された引数が分割された状態のDOM
this.animations = [];
this.chars = '';
this.transY = "170px";//transformY
this.outer = document.createElement('div');//対象の親にdivを追加
// this._clip();
this._init();
}
//テキストがcharクラスをもつspanで1文字ずつ分割される関数
_splitText() {
for (let c of this.chars) {
c = c.replace(" ", " ");
this.concatStr += `<span class="char">${c}</span>`;
}
return this.concatStr;
}
//分割したテキストにデフォルトスタイルを付与
_init() {
this.chars = this.el.querySelectorAll(".char"); //指定した要素(el)のcharクラスを取得する
this.chars.forEach(char => {
char.style.display = 'inline-block';
// char.style.opacity = 0;
char.style.transform = `translateY(${this.transY})`;
})
}
//アニメーションの対象をdivで囲みclip-path
_clip() {
this.outer.classList.add('js-outer');//囲むdivにクラスを付与
this.el.parentNode.insertBefore(this.outer, this.el);//対象の親要素の子要素にdivを挿入
this.outer.appendChild(this.el);//生成したdivの中に対象を入れる
this.outer.style.clipPath = "polygon(0 0,100% 0,100% 100%,0 100%)";//divより外側をclip-pathで切り取る
}
//1文字ずつfadeUpする
fadeUpText() {
this._clip();//isIntersectingになったら実行される
//タイミング制御用オブジェクトを定義
let timings = {
easing: "ease-in-out",
fill: "forwards",
};
let x,easing;
this.chars = this.el.querySelectorAll(".char"); //指定した要素(el)のcharクラスを取得する
this.chars.forEach((char, i) => {
x = i / (this.chars.length - 1);//0 ~ 1
const maxDelay = 170;//delay最大値
//イージング関数
function ease(x){
return x < 0.5 ? 8 * x * x * x * x : 1 - Math.pow(-2 * x + 2, 4) / 2;
}
easing = ease(x);
timings.delay = easing * maxDelay;
timings.duration = 550;
const animation1 = char.animate(
[{ transform: `translateY(${this.transY}) rotateZ(20deg)` }, { transform: "translateY(0px) rotateZ(0)" }],
timings
);
animation1.cancel();
this.animations.push(animation1);
const animation2 = char.animate([{ opacity: 0 }, { opacity: 1 }], timings);
animation2.cancel();
// this.animations.push(animation2);
});
this.animations.forEach((anim) => {
anim.play();
});
}
}
//使い方
//1.インスタンス化
// const ta = new SplitTextAnimation(ここにアニメーションさせたい要素のセレクタを渡す)
//2.アニメーション関数の実行
// ta.fadeUpText()
//----------------------------------------------------------------
//----------------------------------------------------------------
//IntersectionObserver用クラス
class ScrollObserver {
constructor(els, cb, options) {
// this.els = document.querySelectorAll(els);
this.els = els;//nodelistを渡す
const defaultOptions = {
root: null, //交差対象
rootMargin: "-100px", //交差判定境界線
threshold: 0//targetのどこで交差判定するか
};
this.cb = cb;
this.options = Object.assign(defaultOptions, options); //オブジェクトを合体させる
this._init();
}
//初期化
_init() {
const callback = function (entries, observer) {
entries.forEach(entry => {
if (entry.isIntersecting) {
//画面内に入った時
this.cb(entry.target, true);
observer.unobserve(entry.target); //監視を終了する
} else {
//画面外に出た時
this.cb(entry.target, false);
}
});
};
const io = new IntersectionObserver(callback.bind(this), this.options);
this.els.forEach(el => io.observe(el));
}
}
//使い方
//1.コールバック関数を定義する
// const cb = function (el, isIntersecting) {
// if (isIntersecting) {
// ここに画面内に入ったら行いたい処理をかく
// }
// }
//※上記のelにはセレクタではなくDOM自体が渡ることに注意する
//2.インスタンス化する
// const so = new ScrollObserver('監視対象セレクタ', cb, options:あってもなくても良い);
//----------------------------------------------------------------
Discussion
SplitTextAnimation
のコンストラクタの引数を、セレクタではなくElement
にするとよいのではないでしょうか。ついでにScrollObserver
のコンストラクタの引数もNodeList
にします。使用している部分は次のようになります。
コメントありがとうございます!!
実際に検証してみたのですが、確かにコンストラクタ内でqueryselestorしなくても
引数に渡す時にすれば良い話でした。。
お恥ずかしながらその発想が思い浮かばず、逆に自分で自分の首を絞めてしまっていました。
御提案頂いた方法をとることで、問題なく動作しかつ、一度インスタンス化すれば
targetクラスを持つ要素全てを監視してアニメーションを適応することができました!!
ありがとうございます!!