CSSのScroll-driven AnimationsによってJavaScriptを使わずにスクロール連動アニメーションを実現できるようになりました。しかし、現状ではまだ対応ブラウザが限られていたり、複雑な挙動やイージング、状態管理が必要な場面ではJavaScriptの力が不可欠です。
そこでJavaScriptを使ったスクロール連動アニメーションの基礎を作ってみます。
作り方
まずは基本構造から。以下のコードを使って、スクロールに応じてオブジェクトの角丸が変化するアニメーションを作成します。
HTML
|
1 2 3 4 5 6 7 |
<div class="container"> <section class="section"> <div class="viewer"> <div class="object"></div> </div> </section> </div> |
CSS
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 |
* { box-sizing: border-box; margin: 0; padding: 0; } body { background: #fff; } .container { padding: 80vh 0; } .section { height: 500vh; } .viewer { display: flex; justify-content: center; align-items: center; position: sticky; top: 0; height: 100vh; background: #f4f4f4; } .object { width: min(80vw, 80vh); aspect-ratio: 1 / 1; background: #fb2c36; } |
JavaScript
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 |
const section = document.querySelector('.section'); const viewer = document.querySelector('.viewer'); const object = document.querySelector('.object'); const data = { current: 0, target: 0, }; // スクロールイベント window.addEventListener('scroll', () => { // 移動量(viewerの上辺からsectionの上辺までの距離) const distance = viewer.offsetTop - section.offsetTop; // 移動可能範囲(sectionの高さ - viewerの高さ) const range = section.clientHeight - viewer.clientHeight; // 移動比率を算出 // 0の場合、viewerの上辺がsectionの上辺に到達 // 1の場合、viewerの下辺がsectionの下辺に到達 let ratio = distance / range; data.target = ratio; // 角丸のスタイルを適用 object.style.borderRadius = `${data.target * 50}%`; }); |
結果
See the Pen
スクロール連動アニメーションの基礎① by SPC-JM (@SPC-JM)
on CodePen.
解説
CSSの15~27行目で .section に対して適当に高さを指定し、その中に配置された .viewer を position: sticky によって追従させます。
|
15 16 17 18 19 20 21 22 23 24 25 26 27 |
.section { height: 500vh; } .viewer { display: flex; justify-content: center; align-items: center; position: sticky; top: 0; height: 100vh; background: #f4f4f4; } |
この構造により、ユーザーがスクロールするにつれて .viewer の位置が変化し、JavaScriptでその位置をトリガーにアニメーションを制御できます。
JavaScriptでは11~26行目でスクロールイベントによる処理を行っていますので細かく見ていきます。
|
11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 |
window.addEventListener('scroll', () => { // 移動量(viewerの上辺からsectionの上辺までの距離) const distance = viewer.offsetTop - section.offsetTop; // 移動可能範囲(sectionの高さ - viewerの高さ) const range = section.clientHeight - viewer.clientHeight; // 移動比率を算出 // 0の場合、viewerの上辺がsectionの上辺に到達 // 1の場合、viewerの下辺がsectionの下辺に到達 let ratio = distance / range; data.target = ratio; // 角丸のスタイルを適用 object.style.borderRadius = `${data.target * 50}%`; }); |
・13行目
ここではviewerがstickyによって移動した距離を取得しています。
移動していない場合、sectionの上辺に位置しているという事になり、移動量は0となります。
・16行目
ここではviewerが移動できる最大量を取得しています。
viewerの最終停止位置は、viewerの底辺がsectionの底辺に到達した時になりますが、13行目では上辺同士で移動量を計算している為、sectionの高さからviewerの高さを引き、viewerの上辺に対しての最終座標を取得しています。
・21行目
「移動量 ÷ 最大移動量」で移動率(0.0~1.0の値)を取得しています。
次の行で data.target に代入しています。
・25行目
「data.target * 50」で角丸率を計算しています。
viewerが移動していない場合、data.targetは0なので「0 * 50」となり角丸は0%です。
viewerが最大に移動している場合、data.targetは1なので「1 * 50」となり角丸は50%です。
これによりスクロール量に応じて角丸が0%~50%で変化するようになります。
イージング対応
先程までのソースコードでは、スクロール量に対して角丸が即座に変化していたので、滑らかに変化させるようにイージングを導入します。
以下は修正後のコードです:
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 |
const section = document.querySelector('.section'); const viewer = document.querySelector('.viewer'); const object = document.querySelector('.object'); const data = { current: 0, target: 0, }; // スクロールイベント window.addEventListener('scroll', () => { // 移動量(viewerの上辺からsectionの上辺までの距離) const distance = viewer.offsetTop - section.offsetTop; // 移動可能範囲(sectionの高さ - viewerの高さ) const range = section.clientHeight - viewer.clientHeight; // 移動比率を算出し、0~1の範囲に正規化 // 0の場合、viewerの上辺がsectionの上辺に到達 // 1の場合、viewerの下辺がsectionの下辺に到達 let ratio = distance / range; data.target = ratio; // 【コメントアウト】角丸のスタイルを適用 // object.style.borderRadius = `${data.target * 50}%`; }); // 【追加】ループ処理 function loop() { const multiplier = 0.05; // 値を小さくする程にイージングが強くなる data.current = lerp(data.current, data.target, multiplier); // 角丸のスタイルを適用 object.style.borderRadius = `${data.current * 50}%`; requestAnimationFrame(loop); } loop(); // 【追加】イージング関数 function lerp(start, end, multiplier) { return (1 - multiplier) * start + multiplier * end; } |
・25行目
ここでスタイルを適用させていたのが即座に変化する原因だったのでコメントアウトします。
・29~38行目
25行目の代わりにこの行を追加して変化の処理をします。
requestAnimationFrame() によって毎フレーム loop() 関数を実行し、data.current を徐々に data.target に近づけながらスタイルを適用する事でイージング効果が出ます。
・41~43行目
イージングにはlerp関数を使っています。
lerp(start, end, multiplier) は、start から end に向かって徐々に値を変化させる関数です。multiplier が小さいほど変化がゆっくりになります。
アニメーションに広く使われる基本テクニックです。
修正を加えた結果が以下のとおりです。
See the Pen
スクロール連動アニメーションの基礎② by SPC-JM (@SPC-JM)
on CodePen.
まとめ
スクロール連動アニメーションは、ユーザーの操作に応じて動的にUIを変化させることで、よりリッチな体験を提供できます。CSSだけでは難しい制御も、JavaScriptを使えば柔軟に対応可能です。
今回紹介した角丸の変化はシンプルな例ですが、応用すれば回転・拡大・色変化・タイムライン制御など、さまざまな演出に展開できます。イージングを加えることで、より自然で心地よい動きが実現できるのもポイントです。