Cztery kafelki na stronie głównej pokazywały wtedy liczby, które dobijały do wartości docelowej. Animacja działała od pierwszej wersji i przez pół dnia byłem przekonany, że jest skończona.
Nie była. Odliczanie startowało wtedy, gdy obserwator przecięcia zgłaszał kafelek — a to nie to samo, co „kafelek widać”.
Trzy różne momenty
Przy takiej animacji w grę wchodzą trzy zdarzenia i łatwo je pomylić, bo na komputerze programisty zwykle zachodzą niemal jednocześnie.
1. element wchodzi w obszar obserwacji (IntersectionObserver)
2. element kończy się odsłaniać (przejście krycia)
3. element jest w widocznym kadrze (pozycja względem okna)Na telefonie te trzy momenty rozjeżdżają się o sekundy. Kafelek wchodzi w obszar obserwacji dolną krawędzią, gdy widać z niego kilkanaście pikseli. Odsłania się z opóźnieniem, bo czeka na swoją kolej w sekwencji wejścia. A w kadrze jest naprawdę dopiero po przewinięciu.
Pierwsza wersja reagowała na pierwsze zdarzenie. Licznik przelatywał całą drogę od zera do wyniku, zanim kafelek zdążył się pojawić — i po dojechaniu wzrokiem stała tam gotowa liczba.
Naprawa przez pytanie o skutek
Pierwszy odruch: zduplikować w komponencie liczby tę samą logikę opóźnień, którą ma komponent odsłaniający. Zły odruch — dwa miejsca liczące to samo rozjadą się przy pierwszej zmianie.
Lepiej zapytać wprost o skutek. Komponent liczby nie musi wiedzieć, kiedy nastąpi odsłonięcie; musi wiedzieć, czy już nastąpiło.
const gdyWidoczna = () => {
const r = el.getBoundingClientRect();
const wEkranie = r.top < window.innerHeight * 0.92 && r.bottom > 0;
if (!wEkranie || krycieSkuteczne(el) < 0.9) {
czekanie = requestAnimationFrame(gdyWidoczna);
return;
}
// dopiero tutaj rusza odliczanie
};Dwie rzeczy, które mnie zaskoczyły przy pisaniu tego warunku.
Pierwsza: **własne krycie elementu zawsze wynosi jeden**. Przezroczystość siedzi na kontenerze odsłaniającym, więc trzeba przemnożyć wartości rodziców. Sprawdzanie samego elementu nie wykrywa niczego — a wygląda na poprawne.
Druga: **warunek położenia jest tu równie ważny jak krycie**. Przy pierwszym malowaniu, zanim ekran powitalny dostanie swoją docelową wysokość, kafelki stoją wyżej niż ostatecznie. Obserwator zgłasza je od razu, całkiem słusznie — tylko że dwie klatki później są już gdzie indziej.
Bezpiecznik
Została jeszcze jedna rzecz, o której łatwo zapomnieć, bo objawia się tylko u kogoś innego. Skoro liczba czeka na widoczność, to kafelek, do którego nikt nie dojedzie, zostaje z zerem.
Serwer renderuje wartość docelową — i tak ma być, bo bez JavaScriptu, w czytniku ekranu i dla robota wyszukiwarki liczba musi tam stać. Ale wyzerowanie jej po stronie przeglądarki sprawia, że ktoś, kto nie przewinie, widzi zero zamiast wyniku.
Stąd bezpiecznik: po pięciu sekundach bez wejścia w kadr wraca wartość docelowa i obserwator jest zamykany. **Liczba jest treścią, animacja tylko ją odsłania.** Lepiej stracić efekt niż pokazać nieprawdę.
To zresztą wniosek szerszy niż ten jeden licznik. Każda animacja, która podmienia treść na czas trwania, powinna mieć odpowiedź na pytanie: co zobaczy ktoś, u kogo ta animacja nigdy się nie uruchomi.