Poznajemy przykład dobrze napisanej animacji w JS (OOP) i tworzymy jej drugą część, odpowiedzialną za efekt znikania efektu. Do dzieła.
Przykład pochodzi z dokumentacji biblioteki React, ale stanowi przykład dobrego kodu JS:
export default class FadeInAnimation {
constructor(node) {
this.node = node;
}
start(duration) {
this.duration = duration;
if (this.duration === 0) {
//(...)
} else {
//(...)
}
}
onFrame() {
//(...)
}
onProgress(progress) {
this.node.style.opacity = progress;
}
stop() {
cancelAnimationFrame(this.frameId);
this.startTime = null;
this.frameId = null;
this.duration = 0;
}
}
Ok, już teraz widzimy, że:
- konstruktor przyjmuje node
- metoda start będzie wywoływać animację
- metoda stop będzie zatrzymywać animację
- metoda onFrame będzie działać na każdej klatce
- w metodzie start mamy edge case z duration przekazanym jako 0 zapewniony
- po metodzie stop możemy się domyślić, że będzie używane request animation frame
- także po metodzie stop możemy wywnioskować, że gdzieś będą ustalane properties takie jak startTime, frameId
- metoda onProgress ustala opacity, które w CSS jest liczbą float od 0 do 1 (0 niewidoczne, 1 widoczne). Oznacza to, że gdzieś będziemy wyliczać czas, który minął i zamieniać go na liczbę od 0 do 1, aby z upływem czasu element stawał się bardziej widoczny.
Ok, to teraz metoda start:
start(duration) {
this.duration = duration;
if (this.duration === 0) {
// Jump to end immediately
this.onProgress(1);
} else {
this.onProgress(0);
// Start animating
this.startTime = performance.now();
this.frameId = requestAnimationFrame(() => this.onFrame());
}
}
Jak ktoś dał duration 0, to opacity ma być 1, element widoczny, żadnego animowania nie trzeba. Jak ktoś dał inny duration, to:
- po pierwsze – ustawiamy opacity na 0
- po drugie – ustawiamy startTime
- po trzecie – zapisujemy frameId, które zwraca requestAnimationFrame na metodzie onFrame
Pora poznać metodę onFrame:
onFrame() {
const timePassed = performance.now() - this.startTime;
const progress = Math.min(timePassed / this.duration, 1);
this.onProgress(progress);
if (progress < 1) {
// We still have more frames to paint
this.frameId = requestAnimationFrame(() => this.onFrame());
}
}
Ta metoda co klatkę:
- Oblicza ile czasu minęło od now odejmując startTime
- Wylicza progres, jakim jest minimum między liczbą 1, a liczbą float, jaką jest czas, który upłynął podzielony przez czas animacji
- Do onProgress przekazujemy albo liczbę 1, albo liczbę float (to będzie opacity). Czyli niech będzie, że upłynęło 1000ms a duration to 3000ms. To opacity będzie wynosiło 0.33. Po to jednak, aby się ustrzec takich głupot jak opacity powyżej 1.0 mamy ten min…
Ok, jeżeli progress jest mniejszy niż 1 to znaczy, że trzeba rekurencyjnie się wywoływać i akrualizować frameId.
Możemy sobie zaimportować (w przykładzie o rysowaniu) naszą animację i jej użyć:
import { drawCircle, drawLine } from "./drawing.js";
import FadeInAnimation from './fadein.js';
import FadeOutAnimation from "./faceout.js";
//(...)
let ani = new FadeInAnimation(div);
ani.start(1000);
Zakładam, że plik wrzuciliśmy do tego samego folderu. FadeOut jeszcze nie mamy, ale nie będzie to trudne:
export default class FadeOutAnimation {
constructor(node) {
this.node = node;
}
start(duration) {
this.duration = duration;
if (this.duration === 0) {
// Jump to end immediately
this.onProgress(0);
} else {
this.onProgress(1);
// Start animating
this.startTime = performance.now();
this.frameId = requestAnimationFrame(() => this.onFrame());
}
}
onFrame() {
//(...) BEZ ZMAIN
}
onProgress(progress) {
this.node.style.opacity = (1 - progress);
}
stop() {
//(...) BEZ ZMAIN
}
}
Też użyć możemy:
let ani2 = new FadeOutAnimation(div);
setTimeout(() => ani2.start(1000), 5000);
Podchwytliwe, bo teraz wartość progress odejmujemy od 1 i jako opacity ustawiamy, ale cóż, mam nadzieję, że coraz lepiej nam to idzie.