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.