Показване при скрол (част 2 – JavaScript)
След като сме добавили основните стилове, но преди да напишем реално някакъв JavaScript е редно да се погрижим за това какви анимации точно ще имаме.
Animate.CSS
Ето тук има съвкупност от CSS анимации, които можете да ползвате наготово: https://daneden.github.io/animate.css/ .
На работа най-често клиентите искат ефекта fadeInUp
, който ще използваме и тук.
Предполагам, че вече сте изтеглили source файловете и дори сте добавили нужния <link>
таг в документа си, за да заредите декларациите.
Тъй като Intro секцията ни ще бъде видима още при зареждане на сайта, на нея можем да зададем изпълняване на анимация още при зареждането на страницата.
За целта добавете просто класове animated
и fadeInUp
веднага след intro
класа.
Pending
Сега вече нека набележим елементите, които ще искаме да покажем със същия ефект щом се появят на екрана при скрол от посетителите на сайта.
Аз съм добавил клас pending
на:
.featured__title
- всеки един
.feature__item
.columns__title
- двете
.column__item
.gallery__list
Нека ги направим първоначално прозрачни със следния CSS:
.pending { opacity: 0; }
JavaScript
Дойде и така чаканият момент: да задвижим нещата!
Да започнем със селектиране на всички елементи, които искаме да анимираме:
const pendingAnimation = document.querySelectorAll('.pending');
querySelectorAll()
приема като аргумент CSS селектор и връща всички елементи, които му отговарят.
В случая сме изпълнили метода директно на document
, което ще хване всички елементи с клас pending
. Но можехме да изпълним querySelectorAll()
на друг елемент и щяхме да вземем само неговите деца, които отговарят на селектора.
После нека създадем една празна функция, която ще върши работата и да се уверим, че ще се изпълнява на всеки scroll и resize, тъй като тогава може да се наложи анимиране:
window.addEventListener('scroll', animateIn);
window.addEventListener('resize', animateIn);
function animateIn (e) {
}
requestAnimationFrame()
Нека вътре в animateIn()
добавим:
requestAnimationFrame(function () {
});
Това казва на браузъра да изпълни кода от callback функцията, чак след като начертае следващия кадър. Което все пак се случва по 60 пъти в секунда
Подобно отлагане на кода е много важно при обработка на събития, които могат да настъпят бързо и по много пъти (повече от 60) в секунда:
- scroll
- resize
- mousemove
Използвайки requestAnimationFrame()
се уверяваме, че браузърът няма да се опита да прави едни и същи изчисления по 2-3 и повече пъти между 2 поредни кадъра, при положение, че един единствен update на кадъра ще се види от посетителите.
Винаги влагайте кода от handler функция на горните събития в requestAnimationFrame()
.
fadeInUp
Нека сега при scroll проверим за всеки pending елемент, дали е видим:
const scrollInPosition = window.scrollY + window.innerHeight * 0.9;
pendingAnimation.forEach(function (el) {
if (el.offsetTop < scrollInPosition) {
el.classList.remove('pending');
el.classList.add('animated', 'fadeInUp');
} else {
el.classList.remove('animated', 'fadeInUp');
el.classList.add('pending');
}
});
window.scrollY
е колко сме scroll-нали надолу екрана.
window.innerHeight
мисля, че сами можете да се досетите, че е височината на екрана. Умножаваме го по 0.9, защото искаме анимацията да настъпи, когато елементът е изминал 10% от долния край на екрана. Трябва да е поне малко видим, иначе анимацията ще остане извън екрана и целият ефект се губи.
forEach()
ни позволява да изциклим всички резултати от pendingAnimation
колекцията. Тази колекция не е обикновен масив (говорили сме преди за масиви), а е NodeList
. Каква е разликата не е толкова съществено. Важното е да знаете че обикновените for
и while
цикли се прилагат с повече писане, а forEach()
може да се приложи както върху масив, така и върху NodeList
.
el.offsetTop
ни казва на колко пиксела вертикално от началото на страницата се намира елемента. Ако стойността е по-малка от scrollInPosition
(ако е по-нагоре) – значи вече е време да покажем елемента.
classList.remove()
и classList.add()
отново трябва да е пределно ясно, че добавят / премахват класове на текущия елемент.
Накрая имаме един else
, който обръща логиката. Горната част казва:
Щом елемента мине над 10% от долния край на това, което в момента се вижда след scroll – добави му класовете за анимиране.
След else
-а казваме:
Щом scroll-нем нагоре и елемент се върне пак твърде надолу – махни му класовете за анимиране и го направи пак скрит.
Това ни позволява при scroll обратно нагоре и после пак надолу анимацията да се изпълни повторно. Ако вместо това искате веднъж показал се елемент да си остане така без повече да мърда, можете да махнете else частта. На мене така обаче ми е по-забавно и поне по време на разработка ми позволява да виждам анимацията лесно пак и пак, без да презареждам страницата.
Последната секция удължава страницата
Ако много бързо scroll-нете докрай додолу ще видите, че галерията става по-висока и после става пак по-ниска.
Това е защото елементите при показването си не само се анимират до пълна видимост, но и се преместват нагоре. За да има подобно преместване нагоре, те първо трябва да са отишли надолу. Това прави секцията по-висока (за да събира елементите), а при изместването им нагоре височината на контейнера обратно намалява.
Нека просто добавим overflow: hidden
на section
, за да избегнем това.
Забавяне при поредни елементи
Различните колони от .features
и .columns
обаче да излизат така едновременно не е забавно. Някак естествено ми се иска поне на мене всеки следващ елемент от поредица да има някакво малко забавяне.
Там където имаме до 2-3-4 поредни елемента можем да го направим с малко допълнителен CSS:
.feature__item.animated:nth-child(2) { animation-delay: 200ms; }
.feature__item.animated:nth-child(3) { animation-delay: 400ms; }
.column__item.animated:nth-child(2) { animation-delay: 200ms; }
nth-child
указва кой пореден елемент искаме да селектираме. Използваме animation-delay
за да отложим старта на анимацията за този елемент (в случая с 200 или 400 милисекунди).
Забавяне при всяка снимка от галерията
Тъй като в галерията обаче имаме 16 снимки, ако трябва да опишем забавянето за всяка от тях по подобен начин ще има да изпишем доста еднообразен код. Това би трябвало да ни говори, че нещо не го правим както трябва.
Вместо това е по-добре да прибегнем към още малко JS код, за да направим ефекта да работи с безкрайно много картинки от галерията.
Първо все пак да добавим клас pending
на първата картинка. Тя ще служи за старт и трябва да има нещо, което да я „побутне“. Нека това е JS кодът, който вече имаме.
После нека селектираме всички картинки:
const galleryItems = document.querySelectorAll('.gallery__item');
Нека за всяка една зададем opacity: 0
чрез JS, тъй като нямат клас pending
, който да приложи такива стилове:
galleryItems.forEach(function (gi) {
gi.style.opacity = 0;
gi.addEventListener('animationstart', function (e) {
const next = e.currentTarget.nextElementSibling;
if (next !== null) {
setTimeout(function () {
next.classList.add('animated', 'fadeInUp');
}, 100);
}
})
});
И нека за всяка картинка добавим event handler, който да извика функция при начало на някаква анимация за тази картинка.
Тъй като сме селектирали всички, това включва и първата, която има клас pending
. Така когато тя се покаже на екрана ще започне нейната анимация. Това ще предизвика изпълнението на event handler-а.
Това, което се случва там е:
- взимаме следващия елемент (следващото
<li>
) - проверяваме дали наистина сме взели нещо (в случай на обработка на последната картинка – няма какво следващо за вземем)
- задаваме
setTimeout()
, който да се изпълни след 100 милисекунди - При изпълнение на timeout-а прилагаме нови класове на „следващата“ картинка
- Тъй като тя започва да се анимира, предизвиква верижна реакция, селектирайки следващата и анимирайки я след нови 100 милисекунди
За да работи обаче скриването на елементите при scroll нагоре, трябва да добавим и следния код в else-а на animateIn
функцията ни:
if (el.classList.contains('gallery__item')) {
galleryItems.forEach(function (gi) {
gi.classList.remove('animated', 'fadeInUp');
gi.style.opacity = 0;
});
}
Просто при скриване на първата картинка (тази с клас pending
), премахваме animated
и fadeInUp
класовете и на всички други.
Пример
Demo на финалната версия можете да видите на https://magadanski.com/demo/parallax-fade-in-up/.