Poznajemy requestAnimationFrame w JavaScript. Nasze pierwsze kroki z animacjami w JS. Do dzieła!
Ok, nasz markup:
<h1 id="header" style="position: absolute;">Heading</h1>
<script src="./aniframe.js" defer></script>
Position absolute, bo będziemy przesuwać tekst w prawo, taka nasza animacja. Ok, teraz JS:
let header = document.querySelector("#header");
let headerRect = header.getBoundingClientRect();
let leftpos = headerRect.left;
let aniReq;
Mamy header, DOMContentLoaded niepotrzebny (zazwyczaj, w prawdziwym projekcie by nie zaszkodził) bo mamy defer. Ponadto pragnę przypomnieć, czy raczej nadmienić, że getBoundingClientRect nie jest reaktywny, nie jest observerem, pokazuje te pozycje w chwili ich pobrania. I tyle.
Ale tyle nam wystarczy, left to jest na ile w lewo jest przesunięty nasz header. Tworzymy aniReq, podobnie jak w timeoutach/intervalach potrzebny jest handle, jeżeli będziemy chcieli później to anulować.
Ok, teraz animacja:
function moveHeader(timestamp){
leftpos += 5;
if((leftpos + header.offsetWidth) > window.innerWidth)
return cancelAnimationFrame(aniReq);
header.style.left = leftpos + 'px';
requestAnimationFrame(moveHeader);
}
aniReq = requestAnimationFrame(moveHeader);
Zawsze do requestAnimationFrame przekazujemy funkcję callback, a ten callback zawsze przyjmuje timestamp jako argument. To jest po to, bo nie każda animacja ma się wywoływać co klatkę, więc można mieć więcej swobody w wywoływaniu kolejnego requesta.
Ok, ifa możemy zlać, ważne jest, że left to jest wartość w pikselach, więc możemy tam dodać coś i przypisać do style razem z odpowiednią jednostką i już jest przesunięcie.
Dalej wywołujemy kolejny animation frame, gdyby to była inna animacja, to byśmy poczekali odrobinkę, ale my chcemy co klatkę.
Ok, a if? Cóż, bierzemy leftpos (przesunięcie w lewo, wartość left naszego absolutnie pozycjonowanego elementu), dodajemy do niej offsetWidth (czyli „cała” szerokość, jaką zajmuje, bez pseudoelementów before i after, ale tak w zasadzie cała) i sprawdzamy, czy to nie jest większe, niż window.innerWidth.
I tym sposobem animacja się nam zatrzyma zanim element pomyśli o wyleceniu poza viewport. I to niezależnie od wielkości viewportu.
Mała uwaga – tam musi być return cancelAnimationFrame. Względnie – cancel i pusty return linię niżej.
Bo inaczej to logika jest taka, że co prawda zrobiliśmy cancelAnimationFrame, ale dalej przesuwamy i dalej robimy kolejnego requesta. Można wrzucić blok if-else, ale nie chciałem tego robić, żebyśmy porządnie zrobili cleanup.
A z resztą – zróbmy to sobie nieporządnie:
function moveHeader(timestamp){
leftpos += 5;
if((leftpos + header.offsetWidth) > window.innerWidth){
cancelAnimationFrame(aniReq);
}
header.style.left = leftpos + 'px';
requestAnimationFrame(moveHeader);
}
aniReq = requestAnimationFrame(moveHeader);
Teraz jest źle, nie zatrzymuje się. Ok, a teraz:
function moveHeader(timestamp){
leftpos += 5;
if((leftpos + header.offsetWidth) > window.innerWidth){
console.log("I run");
cancelAnimationFrame(aniReq);
} else {
header.style.left = leftpos + 'px';
requestAnimationFrame(moveHeader);
}
}
aniReq = requestAnimationFrame(moveHeader);
Teraz się zatrzymuje, I run odpalane jeden raz. Ok, inna wersja:
function moveHeader(timestamp){
leftpos += 5;
if((leftpos + header.offsetWidth) > window.innerWidth){
console.log("I run");
return;
} else {
header.style.left = leftpos + 'px';
requestAnimationFrame(moveHeader);
}
}
aniReq = requestAnimationFrame(moveHeader);
Cwaniak się zatrzymał (nie tego się spodziewałem). Mądry. The more you know… To ten aniReq nam niepotrzebny:
let header = document.querySelector("#header");
let headerRect = header.getBoundingClientRect();
let leftpos = headerRect.left;
function moveHeader(timestamp){
leftpos += 5;
if((leftpos + header.offsetWidth) > window.innerWidth){
return;
} else {
header.style.left = leftpos + 'px';
requestAnimationFrame(moveHeader);
}
}
requestAnimationFrame(moveHeader);
Znaczy byłby potrzebny, gdybyśmy chcieli gdzieś indziej zatrzymać tę animację. Swoją drogą – aż musiałem sprawdzić, czy return w callbacku wyczyści mi interwał:
let num = 0;
setInterval(function(){
console.log(num++);
if(num > 5){
console.log("Bigger... lets find out if return clears the interval");
return;
}
}, 300);
//NIE WYCZYŚCI....
Ale w requestAnimationFrame zdaje się to działać. Tym niemniej mi się wydaje, że bardziej elegancko jest używać cancelAnimationFrame, choć z drugiej strony nie potrzebujemy zbyt dużo zmiennych globalnych, to nie jest dobry wzorzec, ale gdy gdzieś indziej będziemy chcieli zrobić cancel…
Swoją drogą, cancelAnimationFrame zwraca undefined zaś request – handle do robienia cancel.