「トランジション採用」でフリーランスの正社員転換を支援 他
- 隔週月曜更新!フリーランス・副業ニュース
コンテンツに奥行きを与える視差効果は、サイトのデザインを洗練させ、上質なサイトづくりに役立ちます。その一方、実装にはハードルが高いと思われることも。
結論から言えば、視差効果を実装するのはそれほど難しくありません。これを活かした「視差効果スライダー」も、以下の3ステップで簡単に作れます。
今回はGSAP、CSS Grid、Flexboxを使ったスクロール/ドラッグ可能な視差効果スライダーの作り方を、動画を使いながら紹介します。
以下の動画を見てみましょう。右上のボタンにカーソルをあわせると、ビューポートの右から画像が出てきます。
<button class="button-slider-open js-slider-open" type="button">
<svg>...</svg>
</button>
<div class="placeholders js-placeholders">
<div class="placeholders__img-wrap js-img-wrap" style="--aspect-ratio: 0.8;">
<img
src="https://images.unsplash.com/photo-1479839672679-a46483c0e7c8?ixlib=rb-1.2.1&ixid=eyJhcHBfaWQiOjEyMDd9&auto=format&fit=crop&w=800&q=80&ar=0.8"
class="placeholders__img"
>
</div>
...
</div>
画像など、すべてのDOM要素はthis.domに格納します。
アプリを初期化する際にsetHoverAnimationを呼び出し、デフォルトをpausedに設定したGSAPタイムラインを作成。タイムラインでは、3つの画像にビューポート内でアニメーションをつけ、画像が一部だけ見えるよう設定します。
こう設定すれば、ユーザーからは「クリックすると展開できる」ように見えますよね。
this.dom = {};
this.dom.el = document.querySelector('.js-placeholders');
this.dom.images = this.dom.el.querySelectorAll('.js-img-wrap');
this.dom.buttonOpen = document.querySelector('.js-slider-open');
setHoverAnimation() {
this.tl = gsap.timeline({ paused: true });
this.tl
.addLabel('start')
.set(this.dom.el, { autoAlpha: 1 })
.set(this.dom.images, { scale: 0.5, x: (window.innerWidth / 12) * 1.2, rotation: 0 })
.to(this.dom.images, { duration: 1, stagger: 0.07, ease: 'power3.inOut', x: 0, y: 0 })
.to(this.dom.images[0], { duration: 1, ease: 'power3.inOut', rotation: -4 }, 'start')
.to(this.dom.images[1], { duration: 1, ease: 'power3.inOut', rotation: -2 }, 'start');
}
ホバーをアニメーションのトリガーとするために、2つのイベントを作成しました。
handleMouseenterとhandleMouseleaveです。これらはそれぞれ、GSAPのタイムラインを再生、アニメーションを逆再生します。
this.dom.buttonOpen.addEventListener('mouseenter', this.handleMouseenter);
this.dom.buttonOpen.addEventListener('mouseleave', this.handleMouseleave);
handleMouseenter() {
this.tl.play();
}
handleMouseleave() {
this.tl.reverse();
}
ここからは、ホバーすると画像が3列のグリッドになるよう設定していきます。
スライダーには3個以上アイテムがありますが、ビューポートに表示されるのは最初の3個だけなので、他のアイテムにアニメーションをつける必要はありません。
プレースホルダーアイテムを正しく配置したら、実際のスライダーアイテムを下に配置し、プレースホルダーアイテムを削除しましょう。
まず、プレースホルダーアイテムが動く位置を計算します。プレースホルダーアイテムのleftの位置からスライダーアイテムのleftの位置を引くと、正しいx座標が割り出せます。y座標についても原理は同じで、leftをtopに置き換えます。
const x1 = this.bounds.left - slider.items[0].bounds.left - 20;
const x2 = this.bounds.left - slider.items[1].bounds.left + 10;
const x3 = this.bounds.left - slider.items[2].bounds.left;
const y1 = this.bounds.top - slider.items[0].bounds.top + 10;
const y2 = this.bounds.top - slider.items[1].bounds.top - 30;
const y3 = this.bounds.top - slider.items[2].bounds.top + 30;
プレースホルダーアイテムはスライダーアイテムよりも小さいため、拡大する必要がありますよね。プレースホルダーアイテムの幅をスライダーアイテムの一つぶんの幅で割ると(すべて同じサイズなのでどれでも問題ありません)、正しい値が割り出せます。
const scale = slider.items[0].bounds.width / this.bounds.width;
コンテナ内画像の最初のx座標を設定するために、intersectX1 X2 X3を使いましょう。スライダー内の各画像には、それぞれx座標が設定されています。コンテナ内のスライダー画像にアニメーションをつけるためにx座標を使い、視差効果を作っていきます。
const intersectX1 = slider.items[0].x;
const intersectX2 = slider.items[1].x;
const intersectX3 = slider.items[2].x;
拡大時のアニメーション用に、新しいGSAPタイムラインを作成します。タイムラインが完了すると、setHoverAnimationが起動されます。その後、プレースホルダーアイテムがリセットされて、再度アニメーションが再生される準備が整う仕組みです。
なお、文字や閉じるボタンなどのスライダー要素にもアニメーションをつけますが、文字のアニメーションについては触れません。
this.tl = gsap.timeline({
onComplete: () => {
this.setHoverAnimation();
slider.open();
}
});
3個のプレースホルダー画像は、先ほど定義したx座標とy座標に沿って動きます。GASPタイムラインを見てみましょう。
this.tl
.addLabel('start')
.to(this.dom.images[0], { duration: 1.67, ease: 'power3.inOut', x: -x1, y: -y1, scale, rotation: 0 }, 'start')
.to(this.dom.images[1], { duration: 1.67, ease: 'power3.inOut', x: -x2, y: -y2, scale, rotation: 0 }, 'start')
.to(this.dom.images[2], { duration: 1.67, ease: 'power3.inOut', x: -x3, y: -y3, scale, rotation: 0 }, 'start')
.to(this.dom.images[0].querySelector('img'), { duration: 1.67, ease: 'power3.inOut', x: intersectX1 }, 'start')
.to(this.dom.images[1].querySelector('img'), { duration: 1.67, ease: 'power3.inOut', x: intersectX2 }, 'start')
.to(this.dom.images[2].querySelector('img'), { duration: 1.67, ease: 'power3.inOut', x: intersectX3 }, 'start',)
.set(this.dom.el, { autoAlpha: 0 }, 'start+=1.67')
画像は「overflow:hidden」に設定したコンテナ内でスケールされ、スライダーを動かすと、ビューポート内の画像が若干異なるスピードで動きます。
<div class="slider js-slider">
<div class="slider__container js-container" data-scroll>
<div class="slider__item js-item" style="--aspect-ratio: 0.8;">
<div class="slider__item-img-wrap js-img-wrap js-img" style="--aspect-ratio: 0.8;">
<img
src="https://images.unsplash.com/photo-1472835560847-37d024ebacdc?ixlib=rb-1.2.1&ixid=eyJhcHBfaWQiOjEyMDd9&auto=format&fit=crop&w=800&q=80&ar=0.8"
class="slider__item-img"
>
</div>
<div class="slider__item-content">
<div class="slider__item-heading-wrap">
<h3 class="slider__item-heading">
Indigo
</h3>
</div>
<div class="slider__item-button-wrap">
<button class="button slider__item-button" type="button">
Read more
</button>
</div>
</div>
</div>
...
</div>
</div>
<div class="slider__progress-wrap js-progress-wrap">
<div class="slider__progress js-progress"></div>
</div>
Virtual Scrollを土台にしてスムーススクロールを作っていますが、ここではその詳細について触れません。補間を使うと、滑らかな視差効果スクロールが実装できます。
再度this.domオブジェクトを用意し、必要なDOM要素をすべて格納しましょう。
this.dom = {};
this.dom.el = document.querySelector('.js-slider');
this.dom.container = this.dom.el.querySelector('.js-container');
this.dom.items = this.dom.el.querySelectorAll('.js-item');
this.dom.images = this.dom.el.querySelectorAll('.js-img-wrap');
this.dom.progress = this.dom.el.querySelector('.js-progress');
スクロールの状況を把握するために、スライダーを動き具合を定義するシンプルなプログレスバーを作りました。これは、スライダーコンテナのx座標をあらわす0から1の間の値を計算します。
この値はrequestAnimationFrameで更新され、プログレスバーのscaleXの値のアニメーションに使われます。
const max = -this.dom.container.offsetWidth + window.innerWidth;
const progress = ((scroll.state.last - 0) * 100) / (max - 0) / 100;
this.dom.progress.style.transform = `scaleX(${progress})`;
画像への視差効果のアニメーションをはじめる前に、まず各スライダーアイテムのデータをthis.itemsに格納します。この配列に含まれるのは、画像要素、境界線、画像のx座標です。
setCache() {
this.items = [];
[...this.dom.items].forEach((el) => {
const bounds = el.getBoundingClientRect();
this.items.push({
img: el.querySelector('img'),
bounds,
x: 0,
});
});
}
ここからは、いよいよ画像に視差効果をつけていきましょう。以下のコードはrequestAnimationFrame内のレンダリング関数で実行され、またスクロールの補間値であるscroll.state.lastを使用します。
パフォーマンスを向上させるために、ビューポートに表示されているアイテムを検出し、ビューポート内の画像だけにアニメーションをつけましょう。
const { bounds } = item;
const inView = scrollLast + window.innerWidth >= bounds.left && scrollLast < bounds.right;
アイテムがビューポート内に表示されている場合、対象要素がビューポートに表示されている割合を0から100(percentage)で計算します。
const min = bounds.left - window.innerWidth;
const max = bounds.right;
const percentage = ((scrollLast - min) * 100) / (max - min);
その値を保存したら、percentageに基づいて新しい値を計算し、以下のようにピクセル値に変換します。
const newMin = -(window.innerWidth / 12) * 3;
const newMax = 0;
item.x = ((percentage - 0) / (100 - 0)) * (newMax - newMin) + newMin;
最終的なx座標を計算したら、以下のようにコンテナ内の画像にアニメーションをつけましょう。
item.img.style.transform = `translate3d(${item.x}px, 0, 0)`;
renderは以下のようになります。
render() {
const scrollLast = scroll.state.last;
this.items.forEach((item) => {
const { bounds } = item;
const inView = scrollLast + window.innerWidth >= bounds.left && scrollLast < bounds.right;
if (inView) {
const min = bounds.left - window.innerWidth;
const max = bounds.right;
const percentage = ((scrollLast - min) * 100) / (max - min);
const newMin = -(window.innerWidth / 12) * 3;
const newMax = 0;
item.x = ((percentage - 0) / (100 - 0)) * (newMax - newMin) + newMin;
item.img.style.transform = `translate3d(${item.x}px, 0, 0) scale(1.75)`;
}
});
}
視差効果スライダーを作るのは、それほど難しいことではありません。ぜひ本記事を参考に、オリジナルの視差効果スライダー制作にチャレンジしてみてください。
なお、今回例にしている視差効果スライダーは、Nieuw BergenのWebサイトに実装されています。
(執筆:Ruud Luijten 翻訳:Asuka Nakajima 編集:Saito Hayato 提供元:codrops)
カルーセルスライダーおすすめ7選。サイト内にスライドショーを手軽に実装
Workship MAGAZINE
インパクトで目を惹く!アコーディオンスライダー10選
Workship MAGAZINE