Kawałek Kodu

Jeśli lubisz wpatrywać się w ekranu telefonu i nie bolą Cię oczy od odczytywania mikrowiadomości, to dziś dzisiejszy odcinek jest dla Ciebie. Treść w nim zawarta oczywiście nie będzie "z czapy", ale sposób w jaki będziemy prezentować informacje może się wydawać trochę niekonwencjonalny.

history.back()

Musimy się wrócić do poprzedniego wpisu, dokładnie Jak prześcignąć żółwia, czyli o nieaktywnej zakładce, setInterval oraz Workerze. W nim przedstawiłem mechanizm, który pozwala na cykliczne wykonywanie określonej czynności (zazwyczaj przez setTimeout) nawet przy nieaktywnej zakładce. A dlaczego ta nieaktywna zakładka jest w tym przypadku tak ważna? Bo funkcjonalność, którą przedstawię będzie szczególnie przydatna właśnie w takiej sytuacji. Być może widziałeś informacje podawane w tagu TITLE, a co za tym idzie widoczne w zakładce przeglądarki (jeśli nie, to być może wrócimy do tego kiedyś), a przecież nic nie stoi na przeszkodzie, aby tekst pokazywać w favicon. Oczywiście ze względu na rozmiar ikony (16x16) nie zmieści się cały tekst, więc zrobimy tekst przewijany.

Co się przewinie przez nasz edytor?

Będziemy potrzebować jakiejś klasy, Workera, oryginalnej favicony i CANVAS. Całość kodu obudujemy w klasę. Wykorzystamy w niej Worker, o czym już wcześnie pisałem, po to aby stworzyć płynnie przewijany tekst. Tekst będziemy generować na CANVAS i przekazywać w postaci base64 do atrybutu href favicony. Jeśli okaże się, że tekst się skończy, to przywrócimy oryginalną ikonkę. Muszę Cię zmartwić, rozwiązanie przydatne jest tylko na przeglądarkach desktopowych, bo tam favicona jest widoczna. Swoją drogą może ktoś z Was zna metodę, aby w przeglądarce mobilnej wyświetlać faviconę podczas normalnej pracy?

Robol i konstruktywne podejście.

Zacznijmy więc. O dziwo dziś nie będzie potrzeba dodatkowego kodu HTML, no może oprócz tego gdzie osadzimy nasz skrypt i faviconę. Worker będzie wyglądać tak:

let isPaused = true;
setInterval(function() {
  if (!isPaused) {
    postMessage("");
  }
}, 50);

addEventListener("message", function(event) {
  isPaused = "pause" === event.data;
});

Jego kod wyglada trochę inaczej niż kod Worker'a z poprzedniego wpisu, ale róznice są niewielkie. Ponieważ do naszej głównej klasy (o niej za chwilę) będzie można przekazać kod do wyświetlenia w dowolnym momencie, to oznacza, że czasem Worker nie będzie musiał wysyłać do nas "interwałowej" wiadomości. Jeśli tak będzie, to wyślemy do niego (tak, tak, on też może odbierać wiadomości) informację i ustawimy flagę isPaused na true (początkowo Worker też nie wysyła wiadomości). Jeśli tak będzie, to wiadomość nie będzie wysyłana, a co za tym idzie odbierana w głównym wątku. Nie będzie więc generowany sztuczny interwał dla tego ostatniego. Jeśli wyślemy jakąkolwiek inną wiadomość, np. run, do Worker'a, ten wznowi swoją mozolną robotę.

Może Cię zdziwić dlaczego obsługa zdarzenia nie jest podpięta pod window. Ano, dlatego, że Worker nie ma dostępu do instancji Window, jak również do DOM. Jego naturalnym środowiskiem jest WorkerGlobalScope i do niego przypięte jest zdarzenie. Można również wywołać tą metodę na zmiennej self, która wskazuje na instancję WorkerGlobalScope.

Czas na główny skrypt:

 class FavMessage {
   constructor(options) {
     this.options = {
       fontName: "Arial",
       fontSize: "8pt",
       fontColor: "#000",
       bgColor: "none",
       upperCase: false,
       loop: false
     };
     this.options = Object.assign(this.options, options);
     this.messages = [];
     this.width = 16;
     this.height = 16;
     this.init();
   }
   init() {
     this.worker = new Worker("favicon-worker.js");
     this.favicon = document.querySelector('link[rel="icon"]');
     this.origSrc = this.favicon.href;
     this.canvas = document.createElement("canvas");
     this.canvas.width = this.width;
     this.canvas.height = this.height;
     this.ctx = this.canvas.getContext("2d");
     this.ctx.font = this.options.fontSize + " " + this.options.fontName;
     this.offset = this.width;
     this.textTop =
       (this.height - parseInt(this.options.fontSize)) / 2 +
       parseInt(this.options.fontSize);
   }
   alert(message) {
     message = this.options.upperCase ? message.toUpperCase() : message;
     this.messages.push({
       message: message,
       width: this.ctx.measureText(message).width
     });
     this.worker.postMessage("run");
     this.worker.onmessage = function() {
       if (this.messages.length === 0 && !this.options.loop) {
         this.favicon.href = this.origSrc;
         this.worker.postMessage("pause");
       } else {
         if (this.options.bgColor === "none") {
           this.ctx.clearRect(0, 0, this.width, this.height);
         } else {
           this.ctx.fillStyle = this.options.bgColor;
           this.ctx.fillRect(0, 0, this.width, this.height);
         }
         this.ctx.fillStyle = this.options.fontColor;
         this.ctx.fillText(
           this.messages[0].message,
           this.offset--,
           this.textTop
         );
         if (-this.offset >= this.messages[0].width) {
           this.offset = this.width;
           const firstMessage = this.messages.shift();
           if (this.options.loop) {
             this.messages.push(firstMessage);
           }
         }
         this.favicon.href = this.canvas.toDataURL();
       }
     }.bind(this);
   }
 }

Zacznijmy od konstruktora. Do niego przekazujemy opcje, bo nasz przewijany tekst ma możliwość regulacji ustawień. Tak więc inicjujemy opcje domyślnymi wartościami i sklejamy poprzez metodę assign z wartościami przekazanymi do konstruktora przez użytkownika. Inicjujemy jeszcze tablicę wiadomości oraz wymiary favicon.

W metodzie init startujemy Worker, zapamiętujemy referencję na favikonkę oraz jej oryginalne źródło. Tworzymy CANVAS i zapamiętujemy jej kontekst. Ustalamy wartość offset na szerokość ikonki, bo będziemy zaczynać kreślenie tekstu od prawej krawędzi ikony (właściwie 1 piksel poza nią) oraz położenie tekstu w pionie (staramy się go wyśrodkować). W tym momencie jesteśmy prawie gotowi na odebranie i wyświetlenie pierwszej wiadomości od użytkownika. A tym zajmować się będzie metoda alert.

Tu wstawiamy otrzymany tekst do tabeli tekstów, przy okazji obliczając jego szerokość. Przyda nam się do tego aby stwierdzić czy tekst przewinął się cały, a skoro rozmiar fontu i sam font nie zmienia się w trakcie działania, to nie ma sensu obliczać tej wartości na bieżąco. Uruchamiamy Worker i czekamy na pierwszy quasi-interwał. Czekamy na niego obsługując zdarzenie onMessage. Za każdym wywołaniem "interwału" sprawdzamy czy mamy pustą tablicę wiadomości. Jeśli tak jest i nie mamy ustawionej opcji zapętlania, to pauzujemy Worker i przywracamy oryginalną favikonę. Jeśli mamy jakiś tekst do wyświetlenia to kreślimy go w określonym kolorze na przezroczystym lub pełnym tle. Kreślimy go poczynając od wartości 16, czyli zmiennej offset i tą zmniejszamy co cykl. Jeśli offset przekroczył długość tekstu, to resetujemy go, a pierwszy tekst z tablicy (czyli ten właśnie wyświetlony) wyrzucamy, albo jeśli pracujemy w pętli, to wrzucamy na koniec tablicy. Wykreśloną zawartość CANVAS wrzucamy do favikonki.

Przykładowe wywołanie:

const favMessage = new FavMessage({
  fontColor: "#f00"
});

/* pierwszy tekst wrzucamy po 2 sekundach */
setTimeout(function() {
  favMessage.alert("Skończyłem obliczenia. Wynik: 3.14159");
}, 2000);

/* drugi tekst wrzucamy po 15 sekundach */
setTimeout(function() {
  favMessage.alert("Anka pisze: Przyjdź zaraz, bo zupa gotowa!");
}, 15000);

Całość skryptu stanowi swego rodzaju kolejkę - możemy wrzucać coraz to nowe wiadomości i nie ma obaw o kolejność ich wyświetlania, bo dopóki nie skończy się wyświetlać poprzednia, to nie zacznie kolejna.

Aha... a tu przewijający się tekst w favikonce w działaniu.

Pamiętajcie, aby w ferworze testów skryptu nie przewinąć przez przypadek sąsiada. Do następnego wpisu!

 

Przydatne linki:
Jak prześcignąć żółwia, czyli o nieaktywnej zakładce, setInterval oraz Workerze
Metoda assign dla Object
Metoda measureText dla kontekstu Canvas