Ćwiczenie - type writter

Jakiś czas temu zacząłem nagrywać filmy z zajęć. W sumie chciałem zobaczyć czy wyjdzie. Okazało się, że może nie jest to szczyt profesjonalizmu, ale nawet wychodzi.

Przy okazji wypróbowałem sobie kilka programów do nagrywania. Wyszło na to, że sposobów jest w moim przypadku kilka.

Najbardziej wygodny okazał się OBS Studio. Nie jest to idealny program, a nawet bym powiedział, że jest co najwyżej średni. Ale się sprawdza i najprzyjemniej mi się go używa.

Drugim programem, który najbardziej do mnie trafił jest Filmora9. Plusem tego programu (poza licznymi efektami i edycją video) jest to, że podczas nagrywania widać okienko z kamerą, które w razie potrzeby przesunąć w inne miejsce.

Ale i w przypadku OBS Studio jest na to rozwiązanie, którym jest dodatek do Chrome zwący się CamDesk. Przy czym gdy podczas nagrywania widzimy takie okienko na ekranie, to to wcale wygodne nie jest…

Do edycji filmów zacząłem używać Davinci Resolve. Kobyła straszna, ale nawet podstawowe funkcje wystarczają dla moich potrzeb.

W każdym bądź razie pomyślałem sobie, że dla takich filmów przydało by mi się jakieś mikro intro. Affter Effecta nigdy nie używałem, ale nie potrzebuję jakiś ultra wprowadzeń. No więc wymyśliłem, że równie dobrze mógł bym takie mini demko zrobić za pomocą CSS/JS. Jakiś prosty efekt pisania.

Szybko w necie wpisałem “JS type writter” i równie szybko znalazłem prosty plugin https://safi.me.uk/typewriterjs/

Pozostał efekt pęknięcia, które uzyskałem za pomocą 2 pozycjonowanym elementów, które przełamałem za pomocą clip-path.

W międzyczasie zajmowałem się poprawianiem tekstów na temat Promise i async/await (chociaż nie do końca i muszę jeszcze nad tym trochę posiedzieć).

Użyty plugin miał praktycznie wszystko czego chciałem, poza dwoma rzeczami. Nie ma możliwości reakcji na kolejne akcje - np. pisanie pojedynczej litery, kasowanie liter itp. Druga wada - nie wiedziałem w sumie jak jest stworzony kod tego plugina, a chciałem sam się zmierzyć z tym problemem.

W ramach treningu postanowiłem poćwiczyć Obietnice i napisać własną wersję takiego pluginu.

Pierwsza myśl, jaka mi przyszła do głowy, to to, że fajnie by było, gdyby można było - tak jak w tamtym pluginie - kolejkować kolejne akcje. Bo to fajna rzecz. Trzeba by pewnie odkładać takie akcje to jakiejś tablicy.

Stworzyłem więc klasę o ogólnej postaci:


class TypeMachine {
    _actions = []; //kolejne akcje do wykonania
    _text = ''; //aktualny tekst który będę wypisywał
    _count = 0; //pozycja kursora w tekście

    contructor() {

    }

    write(text, time = 0) {
        const actionWrite = () => new Promise((resolve, reject) => {
            this._text += text;
            const timeInt = setInterval(() => {
                this._count++;
                this._typeText(this._count, time)
                if (this._count >= this._text.length) {
                    clearInterval(timeInt);
                    resolve();
                }
            }, time)
        });
        this._actions.push(actionWrite);
    }
}

Nie jestem Reactowiec i zbyt często nie używałem nowej składni do zapisu właściwości poza konstruktorem (Class Field). Jest moment do przećwiczenia. Czy mi się podoba?… Jak sobie zobaczycie w powyższy link, to zobaczycie, że mógłbym prywatne właściwości tej klasy zapisać ze znakiem #. I działać działa, bo sprawdzałem (tylko Chrome, ale przy takich zabawach nie interesowało mnie wielkie wsparcie). Niestety mój Visual Studio Code gubił się na takiej składni. Pewnie kwestia doinstalowania czegoś, ale to cały ten kod traktuję jak swoisty eksperyment i zabawę i spokojnie może zostać przy znaku podłogi dla prywatnych zmiennych.

W zasadzie cała klasa opiera się o metody takie jak write(). Czyli każda taka metoda tworzy nową akcję, która zwraca Promise. Pomysł był taki, by wywołując kolejne metody (np. write(), pause() itp.) odkładać kolejne funkcje zwracające obietnice w tablicy _actions.


const tm = new TypeMachine(cfg);
tm.write("Kurs", 100); //odpalam metody, które odłożą Promisy
tm.pause(200);
tm.write(" Javask", 100)
tm.pause(400);
tm.back(3, 150);
tm.start(); //tą metodą odpalę wszystkie akcje

Gdy już wszystkie akcje będą przygotowane do odpalenia, odpalam metodę start(), gdzie wykonam pętlę po tej tablicy i wywołam wszystkie akcje jedna po drugiej:


class TypeWritter {
    async start() {
        for (const action of this._actions) {
            const a = await action();
        }
    }
}

Bez słowa await wszystkie akcje zostały by odpalone w tym samym momencie (wszystkie setTimeout odpaliły by się jednocześnie). Użycie await w takiej postaci jak powyżej sprawi, że każda kolejna akcja będzie wykonywać się synchronicznie czekając na wykonanie resolve() poprzedniej, co równoznaczne jest z poczekaniem na zakończenie się wykonywania poprzedniej akcji.

I w sumie pomysł się udał. Miałem po drodze mini problemy z metodą write(). Chciałem w interwale odkładać Promisy służące do wypisywania każdej pojedynczej litery (czyli metoda _typeText też powinna odkładać do akcji Promise). Niestety podejście takie powodowało by odłożenie Promisa, który po odpaleniu w intervale odkładał by kolejne Promisy, więc akcje budowały się w złej kolejności. Zostawiłem więc w metodzie write odłożenie Promise z interwałem tylko wypisującym litery. Później pomyślałem, że w sumie można by zbudować “pre akcje”, które by budowały właściwe akcje, ale szczerze nie chciało mi się już aż tak bawić.

Po zakończeniu budowania podstawowych metod, dodałem do kontruktora obiekt defaultOpts, który scalam z przekazanym do kontruktora obiektem. Dzięki temu otrzymuje obiekt opcji, który głównie służy mi do odpalania metod w odpowiednim momencie - np. przy rozpoczęciu pisania, przy wpisaniu litery, przy zakończeniu pisania itp.


class TypeWritter {
    ...

    constructor(cfg) {
        const defaultOpts = {
            onStart : function() {},
            onStartWrite : function() {},
            onEndWrite : function() {},
            onWrite : function(text, count) {
                console.log(text, count);
            },
            onEnd : function() {},
            onStartBack : function() {},
            onEndBack : function() {},
            onEndPause : function() {}
        }
        this._options = { ...defaultOpts, ...cfg };
    }

    write(text, time = 0) {
        const actionStartWrite = () => new Promise((resolve, reject) => {
            this._options.onStartWrite();
            resolve()
        });
        this._actions.push(actionStartWrite);

        const actionWrite = () => new Promise((resolve, reject) => {
            this._text += text;
            const timeInt = setInterval(() => {
                this._count++;
                this._typeText(this._count, time)
                if (this._count >= this._text.length) {
                    clearInterval(timeInt);
                    resolve();
                }
            }, time)
        });
        this._actions.push(actionWrite);

        const actionEndWrite = () => new Promise((resolve, reject) => {
            this._options.onEndWrite();
            resolve()
        });
        this._actions.push(actionEndWrite);
    }

    ...
}

I w sumie chyba tyle. Mikro klasa do pisania tekstów jest gotowa. Może jej użycie jest ciut bardziej upierdliwe, ale ma o wiele większe możliwości niż tamten skrypt. Chociażby teraz mogę dodać dźwięk pisania liter.

Cały kod


class TypeMachine {
    _actions = [];
    _text = '';
    _count = 0;
    _options = {};

    constructor(cfg) {
        const defaultOpts = {
            onStart : function() {
                console.log("start");
            },
            onStartWrite : function() {},
            onEndWrite : function() {},
            onWrite : function(text, count) {
                console.log(text, count);
            },
            onEnd : function() {
                console.log("end");
            },
            onStartBack : function() {},
            onEndBack : function() {},
            onEndPause : function() {}
        }
        this._options = { ...defaultOpts, ...cfg };
    }

    pause(time = 0) {
        const actionPause = () => new Promise((resolve, reject) => {
            setTimeout(() => {
                this._options.onEndPause();
                resolve();
            }, time)
        });

        this._actions.push(actionPause);
    }

    _typeText(to, time = 0) {
        this._options.onWrite(this._text.slice(0, to), to);
    }

    write(text, time = 0) {
        const actionStartWrite = () => new Promise((resolve, reject) => {
            this._options.onStartWrite();
            resolve()
        });
        this._actions.push(actionStartWrite);

        const actionWrite = () => new Promise((resolve, reject) => {
            this._text += text;
            const timeInt = setInterval(() => {
                this._count++;
                this._typeText(this._count, time)
                if (this._count >= this._text.length) {
                    clearInterval(timeInt);
                    resolve();
                }
            }, time)
        });
        this._actions.push(actionWrite);

        const actionEndWrite = () => new Promise((resolve, reject) => {
            this._options.onEndWrite();
            resolve()
        });
        this._actions.push(actionEndWrite);
    }

    back(count = 0, time = 0) {
        const actionStartBack = () => new Promise((resolve, reject) => {
            this._options.onStartBack();
            resolve()
        });
        this._actions.push(actionStartBack);

        const actionBack = () => new Promise((resolve, reject) => {
            if (count === "erase") {
                count = this._text.length;
            }
            let tick = 0;
            const timeInt = setInterval(() => {
                this._count--;
                tick++;

                this._typeText(this._count, time)

                if (tick >= count) {
                    this._text = this._text.substr(0, this._text.length - tick);
                    clearInterval(timeInt);
                    resolve();
                }
            }, time)
        });
        this._actions.push(actionBack);

        const actionEndBack = () => new Promise((resolve, reject) => {
            this._options.onEndBack();
            resolve()
        });
        this._actions.push(actionEndBack);
    }

    eraseAll(time) {
        this.back("erase", time);
    }

    async start() {
        const actionStart = () => new Promise((resolve, reject) => {
            this._options.onStart();
            resolve();
        });
        this._actions.unshift(actionStart);

        const actionEnd = () => new Promise((resolve, reject) => {
            this._options.onEnd();
            resolve();
        });
        this._actions.push(actionEnd);

        for (const action of this._actions) {
            const a = await action();
        }
    }

}

i użycie na banerze:


document.addEventListener('DOMContentLoaded', function() {

    const banner = document.querySelector(".text");
    const audio = new Audio('key-press.wav');

    const cfg = {
        onWrite(text, count) {
            banner.innerHTML = "";

            [...text].forEach(el => {
                const span = document.createElement('span');
                span.classList.add('letter');
                span.innerHTML = (el !== " ")? el : " ";
                banner.appendChild(span);
            })
            console.log(text)
            audio.play();
        },
        onStart() {
            console.warn('Początek');
        },
        onEnd() {
            console.warn('Koniec!!!');
            document.querySelector('.cursor').style.visibility = "hidden"
            document.querySelector(".banner").classList.add('banner-anim')
        },
        onStartWrite() {
            document.querySelector('.cursor').style.animationName = ""
        },
        onEndWrite() {
            document.querySelector('.cursor').style.animationName = 'anim';
        },
        onStartBack() {
            document.querySelector('.cursor').style.animationName = ""
        },
        onEndBack() {
            document.querySelector('.cursor').style.animationName = 'anim';
        }
    }

    const tm = new TypeMachine(cfg);
    tm.write("Kurs", 100);
    tm.pause(200);
    tm.write(" Javask", 100)
    tm.pause(400);
    tm.back(3, 150);
    tm.pause(400);
    tm.write("ascript", 100);
    tm.pause(300);
    tm.start();


});

Demo banera

Tak naprawdę efekt wyszedł taki sobie. Raczej nie nadaje się to na wejściówkę filmów. Pozostanie nauczyć się lepiej ogarniać Davinci Resolve, albo pobawić się chwilę After Effectem. A może i Blendera coś pomęczyć? W końcu właśnie co wyszła oficjalnie wersja 2.8. Piękna rzecz…

Gulp

W poniższym tekście zajmiemy się Gulpem. Będzie to jeden z kilku tekstów dotyczących ustawiania środowiska pracy jakie sam często stosuję w pracy. Przyda się w jednym z kolejnych artykułów o wordpressie, gdzie będziemy korzystać z takich technik.

Czytaj więcej

Klikalna mapa svg 2

Ten tekst będzie małą aktualizacją do wcześniejszego artykułu odnośnie budowania mapy w svg. No dobrze. Chcemy zrobić interaktywną mapę. Tym razem nie będzie to mapa naszego kraju, a mapa centrum handlowego.

Czytaj więcej
comments powered by Disqus
Poprzednia strona Następna strona