Dziś kolejny post z serii “Kodowanie na ekranie”. Na warsztat wrzucamy stworzenie zegarka odmierzającego czas!
Taki licznik pomaga mi podczas panelu dyskusyjnego, który prowadzę podczas każdej edycji WarsawJS Meetup. Jeśli chciałbyś np. odmierzać czas ugotowania jajka, to taki widget na pewno Ci się przyda.
Krok po kroku 👣
A teraz przedstawię w kilku krokach jak zrobić taki prosty widget:
Krok 1. Fundament
Dodam plik ze skryptem (main.js
) oraz plik (main.css
) odpowiedzialny za
wygląd. W markupie stworzę kontener h1
, który będzie prezentował czas zegarka.
<script src="main.js"></script>
<link rel="stylesheet" href="main.css"/>
<h1></h1>
Krok 2. Uruchomienie pierwszego skryptu
W pliku main.js
stworzyłem funkcję, która uruchomi kod, dopiero po
załadowaniu strony. Funkcja będzie wrzucała do wcześniej stworzonego
znacznika h1
przypadkowy tekst.
function setup() {
let $clock = document.querySelector('h1');
$clock.textContent = '12312';
}
window.addEventListener('DOMContentLoaded', setup);
Krok 3. Stworzenie klasy w JavaScript
W dziedzinie problemu jest rzeczownik “zegarek”. Warto stworzyć taką klasę, aby nadawać jej zachowania i przechowywać stan.
class Clock {
constructor() {
this.$clock = document.querySelector('h1');
}
render(string) {
this.$clock.textContent = string;
}
}
function setup() {
let clock1 = new Clock();
clock1.render("przykładowy tekst");
}
Krok 4. Stworzenie funkcji, która uruchomi odliczanie zegarka
Każdy proces ma swój początek. W naszym przypadku, odliczanie musiało się
kiedyś zacząć. Z tego powodu dobrze jest mieć jedną funkcję, która wszystko
rozpoczyna - funkcja start()
.
class Clock {
// ...
start() {
this.render("przykładowy tekst");
}
}
function setup() {
let clock1 = new Clock();
clock1.start()
}
Krok 5. Parsowanie czasu
Stworzyłem funkcję parseSeconds()
, która będzie przyjmować czas w stylu
"10:00"
, a zwracać liczbę milisekund.
Dlaczego funkcja jest statyczna? Ponieważ nie wymaga ona instancji klasy
(nie ma w niej odwołania do this
).
Ciekawym aspektem jest konwersja tablicy stringów na tablicę liczb.
Wykorzystałem do tego funkcję map()
oraz konstruktor Number
.
W stałej ONE_SECOND
jest liczba milisekund w sekundzie. Przydaje się ona we
operacjach matematycznych.
const ONE_SECOND = 1000;
class Clock {
constructor() {
this.limitTime = null;
}
// ...
start(formattedTime) {
this.limitTime = Clock.parseSeconds(formattedTime);
console.log(formattedTime);
}
static parseSeconds(time) {
let [minutes, seconds] = time.split(':').map(Number);
return minutes * 60 * ONE_SECOND + seconds * ONE_SECOND;
}
}
function setup() {
let clock1 = new Clock();
clock1.start('10:00');
}
Krok 6. Renderowanie czasu
Podstawowym zadaniem jest wyświetlanie czasu, który zbudowany jest na podstawie zdefiniowanego na starcie limitu oraz czasu, który rośnie co sekundę.
Wykorzystując te 2 stany mamy możliwość stworzenie rożnicy (ang. diff), dzięki czemu uzyskamy efekt, że czas się odlicza do zera.
const ONE_SECOND = 1000;
class Clock {
constructor() {
// ...
this.currentTime = 0;
}
// ...
start(formattedTime) {
this.limitTime = Clock.parseSeconds(formattedTime);
let diff = this.limitTime - this.currentTime;
this.render(diff);
}
}
Krok 7. Wykorzystanie interwału
Na początku, w konstruktorze tworzę nową właściwość obiektu, która będzie
przechowywała numer kolejnego interwału (to zwraca funkcją setInterval()
).
Kiedy proces odliczania będzie się zaczynał to aby mógł on trwać w czasie, wykorzystałem funkcję tworzącą interwał.
class Clock {
constructor() {
// ...
this.clock = null;
}
// ...
start(formattedTime) {
this.limitTime = Clock.parseSeconds(formattedTime);
this.clock = setInterval(() => {
this.currentTime += ONE_SECOND;
let diff = this.limitTime - this.currentTime;
this.render(diff);
}, ONE_SECOND);
}
}
Krok 8. Zatrzymanie zegarka
Wykorzystałem funkcję clearInterval()
, po to, aby zatrzymać obecny interwał.
Wskazówka
Nie ma możliwość wznawiania interwału. Możesz stworzyć kolejny.
class Clock {
// ...
start(formattedTime) {
// ...
this.clock = setInterval(() => {
// ...
if (this.isFinished()) {
this.stop();
}
}, ONE_SECOND);
}
stop() {
clearInterval(this.clock);
}
isFinished() {
return this.currentTime === this.limitTime;
}
}
Krok 9. Problem: brak prezentacji początkowego czasu
Aby poradzić sobie z tym problemem, musisz przed uruchomieniem odmierzania
czasu przez zegarek wyrenderować obecny czas. Stąd też funkcja update()
jest uruchomiona przez setInterval()
, ale też i w nim.
class Clock {
// ...
start(formattedTime) {
// ...
this.update();
this.clock = setInterval(() => {
// ...
this.update();
// ...
}, ONE_SECOND);
}
update() {
let remain = this.getRemainingTime();
let time = Clock.formattedTime(remain);
this.render(time);
}
getRemainingTime() {
return this.limitTime - this.currentTime;
}
// ...
}
Krok 10. Prezentacja sformatowanego czasu
Zegarek obecnie wyświetla milisekundy. W takim stanie nie przyda się on wielu
osobom. Należy sformatować czas do postaci czytelnej, tj. MM:SS
(minuty i
sekundy).
Ważne jest zrzutowanie liczby minut do postaci integera (w dół), stąd też wykorzystuję funkcję
Math.floor()
. Nie ma sensu pokazywać niewymiernych liczb.
class Clock {
// ...
update() {
let remain = this.getRemainingTime();
let time = Clock.formattedTime(remain);
this.render(time);
}
// ...
static formattedTime(milliseconds) {
let minutes = Math.floor(milliseconds / ONE_SECOND / 60);
let seconds = milliseconds / ONE_SECOND % 60;
return `${minutes}:${seconds}`;
}
}
Krok 11. Prezentacja minut i sekund jako grupa 2 cyfr
Wykorzystałem najnowsze (ES2017) możliwości łańcucha znaków, jakim jest dynamiczne dodawanie znaków przed nim, ale też i po.
class Clock {
// ...
static formattedTime(milliseconds) {
let minutes = Math.floor(milliseconds / ONE_SECOND / 60);
let seconds = milliseconds / ONE_SECOND % 60;
minutes = String(minutes).padStart(2, '0');
seconds = String(seconds).padStart(2, '0');
return `${minutes}:${seconds}`;
}
}
Krok 12. Centrowanie tekstu
Nigdy nie przypuszczałem, że centrowanie tekstu w przyszłości będzie skupiało się tylko na 3 linijkach 😄
body {
display: flex;
align-items: center;
justify-content: center;
}
Krok 13. Kolorowanie tekstu kiedy czas się skończy
Niesamowicie potrzebny feature, który poinformuje bardzo szybko, że czas się skończył kolorując go na czerwono.
Plik: main.css
/* ... */
.red-color {
color: red;
text-shadow: 1px 1px 3px #000;
}
Plik: main.js
class Clock {
// ....
stop() {
clearInterval(this.clock);
this.$clock.classList.add('red-color');
}
// ....
}
Krok 14. Dodanie muzyki
Wykorzystuję konstruktor Audio
, do stworzenia obiektu, który to załaduje i
odtworzy dowolnie wskazany plik muzyczny.
class Clock {
// ...
stop() {
clearInterval(this.clock);
this.$clock.classList.add('red-color');
playAudio('alarm.mp3');
}
// ...
}
function playAudio(src) {
let audio = new Audio(src);
audio.load();
audio.play();
}
// ...
Krok 15. Pobieranie czasu z URLa
Czas umieszczę w hash
u URLa. Aby go pobrać, wykorzystałem właściwość
hash
w obiekcie window.location
. Dodałem też feature, że jeśli
chcesz zmienić hasha, to przeładowuję stronę, aby uruchomić od nowa zegarek.
Wszystko po to, aby wchodząć na stronę np.
-
http://localhost/countdown/#10:00
- odliczało się 10 minut
-
http://localhost/countdown/#05:00
- odliczało się 5 minut
-
http://localhost/countdown/#03:15
- odliczało się 3 minut i 15 sekund
// ...
function setup() {
let clock1 = new Clock();
clock1.start(location.hash.slice(1));
}
// ...
window.addEventListener('hashchange', () => {
window.location.reload();
});
The end 🎉
Podsumowanie
Chcesz zobaczyć countdown w praktyce?