ModifiersPluginとUtilityメソッド
スライダーを作ってみる
ModifiersPlugin(コアのプラグイン)で逆方向の動きをつけたり、Utilityメソッドのwrap()を使うとカウンターのロジックが簡単になります
おおまかな流れ
- cssでスライダーのアイテムのラッパーはvisibility: hidden;とopacity:0;にする
- gsap.set(‘ラッパー’, { autoAlpha: 1 }); gsap.set(‘スライダーのアイテム’, { x: ‘-100%’ })をセットする
- スライダーがINする時のタイムラインfromTo:(x: ‘-100%’,autoAlpha: 0, からx:0 ,autoAlpha: 1)
- スライダーがOUTする時のタイムラインto: (x:300, autoAlpha: 0)へ
- outからinへのトランジションのタイムライン(add(out).add(in))
add()メソッドを使用すると、タイムラインの任意の場所にトゥイーン、タイムライン、ラベル、コールバックを追加できます。 - 初めにINのタイムライン 、そのあとはクリックイベントでトランジションのタイムラインが発火する
INのタイムラインで背景色も変化させる
onStart :アニメーションの開始時に呼び出す関数
onStartParams :onStart関数を渡すためのパラメーターを配列で渡します
//INのタイムラインを返す関数
function timeLineIn(index){
//スライダーに0から連番のclass名を付けておく
const el = document.querySelector(`div.item${index}`);
const tlIn = gsap.timeline();
tlIn.fromTo(el, {
x: '-100%',
autoAlpha: 0,
}, {
duration: 0.7,
x: 0,
autoAlpha: 1,
onStart: updateClass,
onStartParams: [`no-${index}`],
})
return tlIn;
}
//アニメーション開始時に背景色をつけるラッパー要素にクラスを追加する
function updateClass(parame) {
document.querySelector('.wrapper').className = `wrapper ${parame}`;
}
utils.wrap()は数値を指定された範囲に配置し、最大値を超えると最初に折り返され、最小値よりも小さい場合は最後に折り返されるようにします
let currentIndex = 0;
const totalItems = document.querySelectorAll('.item').length;
const wrapper = gsap.utils.wrap(0, totalItems);
//カウントの値が最後なら0に
const nextIndex = wrapper(currentIndex + 1);
//カウントの値が0なら最終の値に
const prevIndex = wrapper(currentIndex - 1);
ModifiersPluginはGSAP通常適用する値をインターセプトし、それを最初のパラメーターとしてGSAPが適用する新しい値を返します
ModifiersPluginを利用してprevボタンを押した時は、INとOUTの逆方向のアニメーションを作成します
またutils.unitize()でフィードするときに単位を取り除き、結果に単位を追加します
//INのタイムラインを返す関数
function timeLineIn(direction, index) {
//引数が'prev'ならtrue
const prev = direction === 'prev';
const el = document.querySelector(`div.item${index}`);
const button = el.querySelector('.to-section');
const tlIn = gsap.timeline({
//画像とSECTIONへのボタン共通なのでdefaults
defaults: {
// 逆方向
modifiers: {
x: gsap.utils.unitize(function(x){
return prev ? Math.abs(x) : x;
})
}
}
});
tlIn.fromTo(el, {
x: '-100%',
autoAlpha: 0,
}, {
duration: 0.7,
x: 0,
autoAlpha: 1,
onStart: updateClass,
onStartParams: [`no-${index}`],
})
.from(button, { duration: 0.2, x:-20, autoAlpha: 0 })
return tlIn;
}
//OUTのタイムラインを返す関数
function timeLineOut(direction, index) {
const prev = direction === 'prev';
const el = document.querySelector(`div.item${index}`);
const tlOut = gsap.timeline();
tlOut.to(el, {
duration: 0.7,
x: 300,
autoAlpha: 0,
// 逆方向
modifiers: {
x: gsap.utils.unitize(function (x) {
return prev ? -x : x;
})
}
})
return tlOut;
}
アニメーション中かの判別
function isTweening() {
return gsap.isTweening('.item');
}
コードを見る
<div class="wrapper">
<div class="items">
<button class="btn prev"><span class="screen-reader-text">Prev</span></button>
<button class="btn next"><span class="screen-reader-text">Next</span></button>
<div class="item item0">
<div class="item-image">
<img src="https://koro-koro.com/wp-content/uploads/2022/05/1.png" alt="">
</div>
<p class="to-section"><a href="#" class="button" target="_blank" rel="noopener">SECTION 1</a></p>
</div>
<div class="item item1">
<div class="item-image">
<img src="https://koro-koro.com/wp-content/uploads/2022/05/2.png" alt="">
</div>
<p class="to-section"><a class="button" href="#" target="_blank" rel="noopener">SECTION 2</a></p>
</div>
<div class="item item2">
<div class="item-image">
<img src="https://koro-koro.com/wp-content/uploads/2022/05/3.png" alt="">
</div>
<p class="to-section"><a class="button" href="#" target="_blank" rel="noopener">SECTION 3</a></p>
</div>
<div class="item item3">
<div class="item-image">
<img src="https://koro-koro.com/wp-content/uploads/2022/05/5.png" alt="">
</div>
<p class="to-section"><a class="button" href="#" target="_blank" rel="noopener">SECTION 4</a></p>
</div>
<div class="item item4">
<div class="item-image">
<img src="https://koro-koro.com/wp-content/uploads/2022/05/4.png" alt="">
</div>
<p class="to-section"><a class="button" href="#" target="_blank" rel="noopener">SECTION 5</a></p>
</div>
</div>
</div>
.wrapper{ overflow-x: hidden;}
.wrapper.no-0 {background-color: #9a63b4;}
.wrapper.no-1 {background-color: #cf7836;}
.wrapper.no-2 {background-color: #2e82b9;}
.wrapper.no-3 {background-color: #2e6a19;}
.wrapper.no-4 {background-color: #e4b640;}
.items {
width: 90%;
margin: 0 auto;
height: calc(25vh + 250px);
position: relative;
visibility: hidden;
opacity:0;
}
.item {
text-align: center;
position: absolute;
top: 0;
left: 0;
bottom: 0;
right: 0;
}
.item-image,
.item-image img{border-radius: 50%;}
.item-image {
position: relative;
width: 250px;
height: 250px;
margin: 5vh auto 0 auto;
}
.item-image img {
position: relative;
display: block;
}
.button-container {
position: relative;
}
a.button {
text-decoration: none;
width: 150px;
display: inline-block;
margin: 0 auto;
color: rgba(255,255,255,0.7);
padding: 8px 0;
border: 1px rgba(255,255,255,1) solid;
border-radius: 2px;
}
button.btn {
display: block;
width: 70px;
height: 70px;
background-color: transparent;
border: none;
color: rgba(255,255,255,0.7);
border-radius: 50%;
transition: all 0.3s linear;
overflow: hidden;
position: absolute;
top: 25vh;
z-index: 1;
}
button.btn.next {
left: auto;
right: 0;
}
button.btn:hover,
button.btn:focus,
button.btn:active {
outline: none;
background-color: rgba(0,0,0,0.4);
border-radius: 50%;
cursor: pointer;
}
button.btn:before {
content: "\2039";
display: block;
position: absolute;
top: -13px;
font-size: 70px;
font-weight: 300;
}
button.btn.prev:before {
content: "\2039";
left: 23px;
}
button.btn.next:before {
content: "\203a";
left: 28px;
}
.dots {
display: flex;
align-items: center;
justify-content: center;
position: absolute;
bottom: 2vh;
left: 0;
right: 0;
top: auto;
}
.dot {
overflow: hidden;
background-color: rgba(255,255,255,0.2);
width: 20px;
height: 20px;
text-indent: -9999em;
border: none;
border-radius: 50%;
transition: all 0.3s linear;
margin: 0 8px;
cursor: pointer;
}
.dot:focus,
.dot:active,
.dot:hover {
outline: none;
background-color: rgba(255,255,255,0.6);
}
.dot.active {
background-color: rgba(0,0,0,0.4);
}
//INのタイムラインを返す関数
function timeLineIn(direction, index) {
const prev = direction === 'prev';
const el = document.querySelector(`div.item${index}`);
const button = el.querySelector('.to-section');
const tlIn = gsap.timeline({
defaults: {
modifiers: {
x: gsap.utils.unitize(function(x){
return prev ? Math.abs(x) : x;
})
}
}
});
tlIn.fromTo(el, {
x: '-100%',
autoAlpha: 0,
}, {
duration: 0.7,
x: 0,
autoAlpha: 1,
onStart: updateClass,
onStartParams: [`no-${index}`],
})
.from(button, { duration: 0.2, x:-300, autoAlpha: 0 })
return tlIn;
}
//INアニメーション開始時のコールバック
function updateClass(parame) {
document.querySelector('.wrapper').className = `wrapper ${parame}`;
}
//OUTのタイムラインを返す関数
function timeLineOut(direction, index) {
const prev = direction === 'prev';
const el = document.querySelector(`div.item${index}`);
const tlOut = gsap.timeline();
tlOut.to(el, {
duration: 0.7,
x: 300,
autoAlpha: 0,
modifiers: {
x: gsap.utils.unitize(function (x) {
return prev ? -x : x;
})
}
})
return tlOut;
}
//トランジションのタイムラインを返す関数
function timeLineTransition(direction, toIndex) {
const tlTransition = gsap.timeline();
tlTransition
.add(timeLineOut(direction, currentIndex))
.add(timeLineIn(direction, toIndex));
// アニメーションの開始時に呼び出す関数
onStart: updateCurrentIndex(toIndex)
return tlTransition;
}
//トランジションアニメーション開始時のコールバック
function updateCurrentIndex(toIndex) {
currentIndex = toIndex;
//ドットにactiveクラスをつける
document.querySelectorAll('.dot').forEach(function (el, index) {
el.setAttribute('class', 'dot')
if (index === currentIndex) {
el.classList.add('active')
}
})
}
//アニメーション中かを判別
function isTweening() {
return gsap.isTweening('.item');
}
//ドットを作成する関数
function createDot() {
const newDiv = document.createElement('div');
newDiv.setAttribute('class', 'dots');
for (let index = 0; index < totalItems; index++){
const element = document.createElement('button');
const text = document.createTextNode(index);
element.appendChild(text);
element.setAttribute('class', 'dot');
if (currentIndex === index) {
element.classList.add('active')
}
//クリックされた時
element.addEventListener('click', function () {
if (!isTweening()) {
const direction = index > currentIndex ? 'next' : 'prev'
timeLineTransition(direction, index);
}
})
newDiv.appendChild(element);
}
document.querySelector('.items').appendChild(newDiv);
}
//set
gsap.set('.items', { autoAlpha: 1 });
gsap.set('.item', { x: '-100%' })
let currentIndex = 0;
const totalItems = document.querySelectorAll('.item').length;
const wrapper = gsap.utils.wrap(0, totalItems);
//最初のスライダーがINする
timeLineIn('next', currentIndex)
//ドット
createDot()
//次へのボタンが押された時
document.querySelector('button.next').addEventListener('click', function (e) {
e.preventDefault();
const nextIndex = wrapper(currentIndex + 1);
!isTweening() && timeLineTransition('next', nextIndex);
})
//前へのボタンが押された時
document.querySelector('button.prev').addEventListener('click', function(e){
e.preventDefault();
const prevIndex = wrapper(currentIndex - 1);
!isTweening() && timeLineTransition('prev', prevIndex);
})
ScrollTrigger
100pxスクロールした時に、bodyにis-fixedクラスをつける場合は、これだけでOK
gsap.registerPlugin(ScrollTrigger);
ScrollTrigger.create({
start: 100,
//endがないと最後までスクロールした時にクラスが外れる
end: 'bottom bottom-=100',
toggleClass: {
targets: 'body',
className: 'is-fixed'
}
})
UtilityメソッドのtoArrayは、ほとんどすべての配列のようなオブジェクトを配列に変換します
とても便利です
セクションごとにアニメーションをつけます
最もシンプルな例が
ビューポートのボトムにトリガー要素のトップが現れた時にクラスをつける
ビューポートのトップにトリガー要素のボトムが現れた時にクラスが外れる
*要するにトリガー要素が、ビューポートに入っている間はクラスがついている状態です
gsap.registerPlugin(ScrollTrigger);
//toArrayを使うことで、配列として扱えます
const allSections = gsap.utils.toArray('section');
allSections.forEach((section, index) => {
gsap.set(section, {
scrollTrigger: {
trigger: section,
toggleClass: 'active',
start: "top bottom-=250px",
end: "bottom 250px",
//idはマーカーに追加表示されます(ScrollTrigger.getById()で取得もできる)
id: `sec-${index+1}`,
markers: true
}
})
})
- start
- ScrollTriggerの開始位置(スクローラーの上部/左側が基準です)
*スクローラーのデフォルトはビューポートです - 文字列
“top center”:トリガーの上部がスクローラーの中央に当たったとき
“top bottom-=100px”:トリガーの上部がスクローラーの下部から100px上に当たったとき
数値
200: 200ピクセルスクロールした時
- endTrigger
- ScrollTriggerの終了位置の計算に使用される要素(または要素のセレクター)
*トリガー要素が異なる場合のみ設定します
- end
- ScrollTriggerの終了位置
- 文字列
“bottom center”:endTriggerの下部がスクローラーの中央に当たったとき
“center 100px”:endTriggerの中心がスクローラーの上部から100px下に当たったとき
startとendでスクローラー(ビューポート)を調節できます
*top center bottomの使い方が感覚的でないような気がするw(ややこしい〜😂 )
scrollTrigger: {
trigger: section,
toggleClass: 'active',
//ビューポートのボトムから250px上
start: "top bottom-=250px",
//ビューポートの上部から250px下
end: "bottom 250px",
}
<img>にwidthやheight属性がない場合などで、高さが反映されない時があります
その時は、ScrollTrigger.refresh()で、ラッパー要素を生成し直してくれます
mousemoveイベントで遊んでみる
gsap.utils.toArrayは、document.querySelectorAllの代わりに使っています
*親の要素のCSSには、perspective: 1000pxを設定して奥行きを出します
document.querySelector('.images').addEventListener('mousemove', moveImges)
function moveImges(e) {
//offsetX,Y イベントが発生した要素上のX座標とY座標
const { offsetX, offsetY, target } = e
const { clientWidth, clientHeight } = target
const xPos = (offsetX / clientWidth) - 0.5;
const yPos = (offsetY / clientHeight) - 0.5;
const images = gsap.utils.toArray('.image')
images.forEach((image, index) => {
gsap.to(image, {
duration: 1.2,
x: xPos * 20 *(index + 0.5),
y: yPos * 100 * (index * 1.5 + 0.5),
rotationY: xPos * 100,
rotationX: yPos * 10,
ease:'Power3.out'
});
})
}
PC(マウスイベント)




