Przejdź do treści

Jak zbudować licznik czasu?

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.

Baner promujący artykuł

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 hashu 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?

See the pen on CodePen.