今回は、テイストを変えて様々な箇所でよく使う「一文字ずつ登場させるアニメーション」の基礎をソースコードとともに解説していきたいと思います。まず、完成版はこんな感じです。スクロールしてみてください!
See the Pen 1文字ずつ登場するアニメーション by Takuro Sakai (@sakaccho) on CodePen.
※解説のために.js-observe-target
を2回も変数に代入していますが、適宜変更してください。
以下の3つのパートに分け、解説していきます。
これはHTMLの段階で手書きで<span>
タグで一文字ずつ括ってしまっても良いです。というより、そのほうがパフォーマンス的には○です。「動的に変わるので構造上難しい」とか「いちいち手書きでなんかやってらんないよ!」という方は以下のJSで自動で一文字ずつ括るようにしてください。
<div class="text js-observe-target">テキスト 1</div>
上記のような要素があったとします。JSは以下のようになります。
const text = document.querySelectorAll('.js-observe-target');
// .text要素の数だけループ処理
text.forEach(element => {
let html = '';
const letters = element.textContent.split(''); // 1文字ずつ分解して配列へ
// 一文字ずつ回して<span>タグで括る処理
letters.forEach(letter => {
const str = letter.replace(/\s| /g, ' ');
html += '<span>' + str + '</span>';
});
element.innerHTML = html; // .text要素内を置き換える
});
簡単に処理の中身を解説します。
.text
クラスを持った要素だけループしますsplit('')
で一文字ずつ分解してletters
という配列に入れますletters = ['テ', 'キ', 'ス', 'ト', ' ', '1']
となっているのでこれをループします
に置換します<span>テ</span>
となるようにしてhtmlという変数に足していきますinnerHTML
で.text
要素に返しますこれで以下のようにテキストが分解されたはずです。
<div class="text js-observe-target">
<span>テ</span>
<span>キ</span>
<span>ス</span>
<span>ト</span>
<span> </span>
<span>1</span>
</div>
HTMLの準備ができたところでスタイルをあてていきましょう。今回はSCSSを使います。ざっと以下のスタイル実装が必要となります。
<span>
ごとに遅延させるまずは文字が「見えない状態」を作ります。今回はclip-path
プロパティのinset(X)
を用います。これはclip-path: inset(上辺 右辺 下辺 左辺);
というように各辺から%で指定することで表示領域をコントロールできるものです。
上の図をみてもらうとわかりやすいですが、inset(0 0 0 0)
とすると四角形の全域が表示され、inset(0 0 50% 0)
とすると下辺から50%の位置まで表示領域が狭められた状態になります。つまりinset(0 0 100% 0)
とすれば表示領域は見えなくなります。また、これはあくまで表示領域の設定なので内部のコンテンツが潰れたりすることはありません。
ここでは<span>
タグごとにinset(0 0 100% 0)
にして文字が見えないようにしておきましょう。
.text {
span {
clip-path: inset(0 0 100% 0);
}
}
さて、このclip-path
はtransition
がかけられるので、これを利用してアニメーションを実装します。今回は1文字ずつ少しだけ登場アニメーションを遅延させたいので、ループを使って以下のように1文字ごとに0.06秒遅延するtransition-delay
もつけます。また、.text
に-visible
というクラスが付与されたら文字が現れるようにしておきましょう。
.text {
span {
clip-path: inset(0 0 100% 0);
transition: all cubic-bezier(0.215, 0.61, 0.355, 1) 1.0s
@for $i from 1 through 6 {
&:nth-child(#{$i}) {
$delay: $i * 0.06 + s;
transition-delay: $delay;
}
}
}
&.-visible {
span {
clip-path: inset(0 0 0 0);
}
}
}
繰り返し部分は、実際のCSSでは以下のように出力されます。
.text span:nth-child(1) {
transition-delay: .06s
}
.text span:nth-child(2) {
transition-delay: .12s
}
.text span:nth-child(3) {
transition-delay: .18s
}
「スクロールイベントでスクロール量を取得し、アニメーションさせたい要素の位置から計算してクラスを付与する」という実装方法もありますが今回は交差オブザーバーAPI(Intersection Observer API)を用いて実装します。こちらのほうがパフォーマンスも良いとのことです。
交差オブザーバーAPIですが、その名の通りブラウザで見えている領域とターゲット要素が交差したら、つまり要素が見えたら何かしらの処理を行う、といったことができる仕組みです。今回実現したい処理に必要なコードは以下のとおりです。
const setObserver = () => {
// ④スクロールの度に実行される関数
const intersectionCallback = (entries) => {
entries.forEach( (entry) => {
if (entry.isIntersecting) {
// entry.targetが領域に入ったら(=見えたら)特定の処理を実行
entry.target.classList.add('-visible');
}
});
}
// ①交差判定となるボーダーの設定&インスタンス生成
const options = {
rootMargin: '0px 0px -200px 0px',
}
const observer = new IntersectionObserver(intersectionCallback, options);
// ②監視対象となる要素
const targets = document.querySelectorAll('.js-observe-target');
// ③「②」の要素を監視対象として登録
targets.forEach( (target) => {
observer.observe(target);
});
}
setObserver();
ここで設定しているオプションのrootMargin
という考え方が少し癖ありです。これまでのscrollTop
を取得する方法だとブラウザビューの上辺か、ブラウザ高さを足したりして下辺が要素を通過したら・・・のような処理をよく作っていましたが、rootMargin
はブラウザビューの上下左右4辺からの距離を指定して境界を設定できます。
rootMargin: '上 右 下 左'
という風に設定するので、上記のrootMargin: '0px 0px -200px 0px'
は「下辺から200pxの位置」を境界線にするということですね。
境界線に交差したかを監視したい要素を取得します。ふつうにquerySelectorAll
で取得しときます。
②で取得した要素が複数なので、forEachでまわして各要素を監視するように登録します。
あとは交差した要素だけentry.isIntersecting = true
となるので、交差したときに行いたい処理をここに書いておきます。今回は交差した要素に-visible
クラスを付与しています。
可視の判定処理は基本的に今回紹介したIntersection Observerがパフォーマンスもよく、実装も楽なのでよいのかなと思います。あとはイージング変えたり、時間をいじってテイストを変更してみてください。