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.