Aplikacja do rysowania
W poniższym tekście spróbujemy zebrać różne informacje z tego kursu i wykorzystać je do zbudowania aplikacji służącej do rysowania.
Zacznijmy od przygotowania prostej struktury projektu oraz zainstalowania odpowiednich paczek. W rozdziale o bundlerach omawialiśmy sobie tworzenie konfiguracji za pomocą webpacka, gulpa czy parcela. W poniższym tekście użyjemy czegoś innego.
Vite - bo o nim mowa, to nowsze rozwiązanie, które działa nieco inaczej od swoich braci. Przede wszystkim swoje działanie opiera o nieco rozwinięte moduły ES6.
Co to oznacza? Narzędzie to w odróżnieniu od tamtych rozwiązań nie musi w tle każdorazowo przebudowywać wszystkich naszych skryptów, a zamiast tego serwuje je bezpośrednio w przeglądarce korzystając z tego, że nowe przeglądarki natywnie wspierają już moduły ES6. Dzięki temu praca jest o wiele, wiele szybsza w porównaniu z takim Parcelem czy Webpackiem. Dodatkowo ten mechanizm rozwija. W przypadku stosowania modułów w natywnej postaci musimy podawać pełne ścieżki wraz z rozszerzeniem - co jest szczególnie upierdliwe, gdy odwołujemy się do paczek zainstalowanych w katalogu node_modules. Vite rozwiązuje ten problem, dzięki czemu możemy pracować podobnie jak przy innych bundlerach (np. podając ścieżki bez rozszerzeń czy bezpośrednio odwołując się do zainstalowanych w node_modules paczek).
Początkowa instalacja
Przechodzimy na stronę https://vitejs.dev/ i po kliknięciu "Get started" przechodzimy do opisu początkowej instalacji.
Nie musisz tworzyć nowego katalogu, bo instalacja odpowiednio ci to ułatwi. Zgodnie z instrukcją odpalamy więc polecenie:
Pojawi się kilka pytań. Podajemy nazwę projektu, wybieramy vanilla i Javascript.
Właśnie zakończyłeś instalowanie Vite. Możesz odpalić projekt.
Po zainstalowaniu powstanie katalog z bardzo prostą strukturą plików. Głównym plikiem jest index.html, do którego dołączony jest plik main.js, a w którym to rozpoczniemy pracę.
Dodatkowo w pliku package.json pojawiły się odpowiednie skrypty, które wykorzystamy do odpalenia naszego projektu:
Aby wiec odpalić projekt, wystarczy użyć polecenia npm run dev, a do zbudowania czy podglądu wersji produkcyjnej użyjemy poleceń npm run build i npm run serve.
Aby to jednak zrobić powinieneś wcześniej doinstalować brakujące paczki poleceniem npm i, ponieważ powyższe polecenie tworzące projekt tego nie robi.
Inna struktura katalogów
W tym momencie moglibyśmy spokojnie już ruszyć naszą pracę. Zanim jednak przejdziemy dalej, spróbujmy nieco zmodyfikować domyślną strukturę plików, wprowadzając do niej podział na katalogi src i dist (podobnie jak to robiliśmy przy innych bundlerach). Katalog dist, do którego trafi zbudowana aplikacja tworzony jest automatycznie przez Vite gdy użyjemy polecenia npm run build.
Stwórzmy więc katalog src, przerzućmy do niego plik index.html, a dodatkowo utwórzmy w nim odpowiednią strukturę plików:
Dodatkowo popraw ścieżkę w index.html by wskazywała na nowe miejsce pliku main.js (uwaga relatywne ścieżki zaczynaj od kropek: ./js/main.js).
Jak widzisz dodaliśmy też plik main.scss, bo właśnie w tym języku chcemy pisać css. Krok ten traktuj jako sprawdzenie funkcjonalności Vite, ponieważ w naszym projekcie zyskasz dokładnie zero (zamiast tego bez problemu możesz zostać na zwykłym css).
Domyślnie Vite obsługuje SASSa, natomiast by używać tego języka, musimy doinstalować dodatkową paczkę za pomocą polecenia (1):
Ostatnia rzecz i możemy zaczynać. Domyślnie Vite ustawiony jest na index.html mieszczący się w głównym katalogu. My go przenieśliśmy do katalogu src. Aby zmienić zachowanie Vite podobnie do innych narzędzi utwórzmy w głównym katalogu plik konfiguracyjny vite.config.js z mini konfiguracją:
Nasz projekt jest gotowy do pracy. Spróbuj go odpalić poleceniem npm dev i przejdź na odpowiedni adres w przeglądarce.
Podział aplikacji
Nasza aplikacja w założeniu ma przypominać prosty Paint. Będzie miała kilka podstawowych narzędzi takich jak pędzel, rysowanie linii, prostokątów itp, a także umożliwi zmianę rozmiaru czy koloru.
Podział na pliki mniej więcej będzie wyglądał tak:
Klasa app
Nasza pierwsza klasa będzie miała za zadanie uruchomienie aplikacji.
Jak widzisz nie za wiele się tutaj dzieje. Żeby używać scss musimy go zaimportować. Same css zostaną wygenerowane i automatycznie dołączone do html.
Dodatkowo importujemy klasę board, która będzie odpowiedzialna za utworzenie planszy do rysowania.
Przy okazji dodajmy do src/index.html, główny element, w którym będzie znajdować się nasza aplikacja:
Klasa board
Nasza aplikacja będzie składać się z dwóch elementów typu canvas.
Jeden z nich - główny - będzie zawierać narysowane figury. Drugie posłuży do dynamicznego rysowania figur. Nie wiem, czy miałeś kiedyś styczność z dowolnym programem graficznym. Gdy rysujesz pędzlem, po prostu mażesz po ekranie. Gdy jednak rysujesz dowolną figurę (np. kwadrat), trzymając klawisz myszy i przeciągając kursorem ustawiasz jej rozmiar. Narysowanie figury zostaje zatwierdzone w momencie puszczenia klawisza myszy. Właśnie do tego celu wykorzystamy drugie płótno.
Oba elementu ułożymy w tym samym miejscu za pomocą pozycjonowania absolutnego.
Nasza klasa musi więc stworzyć sobie dwa elementy canvas. Żeby nie duplikować kodu, możemy wykorzystać do tego funkcję pomocniczą:
Omówmy sobie poszczególne części:
Raczej samo opisujący się kod. Tworzymy dwa canvasy i wrzucamy je w odpowiedni element na stronie (#canvasCnt). Następnie pobieramy ich context, bo to głównie z nich będziemy korzystać.
Dodatkowo tworzymy zmienne - mouse z pozycją myszki, currentTool, w której będziemy przechowywać aktualne narzędzie oraz toolParams, gdzie będziemy trzymać ustawienia narzędzia (kolor, rozmiar).
Czyli zwykłe podpięcie zdarzeń pod główny canvas. Robiliśmy to setki razy w rozdziałach o zdarzeniach. Zauważ, że dla zdarzenia mousemove dodatkowo aktualizuję zmienną mouse.
W każdym ze zdarzeń wywołuję odpowiednią metodę dla aktualnie używanego narzędzia, co oznacza, że każde z nich będzie takie metody posiadać (patrz klasa Tool).
Kolejne funkcje posłużą nam do ustawiania parametrów używanych narzędzi. Po ustawieniu odpalimy dodatkowo metodę onmouseMove, by zaktualizować stan narzędzia na planszy (widoczny dla użytkownika wskaźnik - tym zajmiemy się później).
Na koniec tworzymy pojedynczą instancję i wystawiamy ją na zewnątrz. Tutaj pojawia się dość ważna cecha importów w modułach ES6. Każda rzecz, którą w taki sposób importujesz jest pojedyncza dla wszystkich miejsc gdzie używasz danego pliku. Stworzyliśmy i wyeksportowaliśmy instancję na bazie klasy Board. Jeżeli teraz zaimportujemy ją w kilku innych plikach, za każdym razem będzie to ta sama instancja.
Dla naszej planszy dodajmy też odpowiednie stylowanie w pliku main.scss:
Po rozpoczęciu aplikacji ustawmy początkowy stan:
Do utworzenia narzędzia wykorzystamy funkcję makeTool(), którą stwórzmy w oddzielnym pliku:
Wstępne narzędzia
Do początkowych testów stwórzmy trzy różne narzędzia jako oddzielne klasy. Żeby nie duplikować kodu, zróbmy je jako klasy dziedziczące po klasie Tool:
Sprawdź teraz czy tworzenie narzędzi działa, zmieniając w pliku main.js na odpowiednie narzędzie np. makeTool("line").
Mini konfiguracja
Zmiana narzędzi jak i kolorów powinna się odbywać za pomocą odpowiednich klawiszy. Obsłużymy to w klasie Control.
Żeby nasz kod był łatwiejszy w zarządzaniu, zróbmy dodatkowy plik z małą konfiguracją:
Klasa Control
Klasa Control ma za zadanie obsłużyć wszystkie klawisze używane w naszej aplikacji.
Przypatrzmy się najbardziej istotnym częściom tej klasy:
Podpinamy pod odpowiednie zdarzenia wywołanie metod z tej klasy. Pamiętaj, że gdy podpinasz zdarzenie pod dany element (w powyższym przypadku document), wewnątrz podpiętej funkcji this wskazuje na element, do którego podpiąłeś nasłuchiwanie zdarzenia. My chcemy aby this wskazywał na dany obiekt. Żeby to uzyskać tak samo jak w rozdziale o zaawansowanym this możemy użyć funkcji strzałkowej, albo metody bind().
Nie chciałem tego robić bezpośrednio w momencie podawania metody do podpięcia:
ponieważ potem byłby potencjalny problem z odpięciem takiej funkcji. Mówiliśmy sobie o tym problemie tutaj.
Kolejny fragment to:
Robimy pętlę po tablicy tools i colors (patrz konfiguracja). Jeżeli naciśnięty klawisz równa się właściwości key danego elementu w tablicy, to tworzymy nowe narzędzie lub kolor o danej nazwie.
Naszą klasę importujemy w głównym pliku:
Komunikacja między komponentami
Po naciśnięciu klawisza danego narzędzia klasa Control tworzy odpowiednie narzędzie i odpala metodę setTool dla Board. Przydało by się przy okazji poinformować inne komponenty o takiej zmianie, bo a nóż będą chciały zaktualizować swoje informacje.
Wykorzystamy do tego wzorzec Observer. Gdy wrócisz do tekstu z tamtego rozdziału, zobaczysz, że do tematu można podejść na kilka sposobów, gdzie praktycznie każdy z nich jest równie dobry. Ja wybiorę sposób z sygnałami.
Tworzymy więc klasę eventObserver:
I na jej podstawie tworzę nowy plik z kilkoma zmiennych będącymi sygnałami:
A następnie dodaję go do klasy Board:
Zanim przejdziemy dalej, spróbujmy w pliku main.js dodać testowe podłączenie do powyższych sygnałów:
Spróbuj teraz przełączyć narzędzia klawiszami 1-3, kolory klawiszami r, g, b oraz wielkość narzędzi za pomocą kółka myszy.
Jeżeli wszystko jest ok, przechodzimy dalej.
Interfejs graficzny
Klasa Gui będzie odpowiedzialna za stworzenie prostego interfejsu z informacjami takimi jak kolor, rozmiar czy wybrane urządzenie. Wyglądem samego Gui zajmiemy się na końcu. Teraz bardzo prosta wersja:
Dołączamy ją do głównego pliku:
Oraz dodajmy proste stylowanie:
Sprawdź teraz czy gui reaguje na zmiany parametrów narzędzia.
Klasa Tool
W naszej aplikacji będziemy mieli kilka narzędzi. Większość z nich będzie obsługiwane za pomocą myszy. Musimy więc obsłużyć trzy zdarzenia: mousemove, mousedown, mouseup.
Klasa Brush
Pierwszym z narzędzi będzie Brush, służące do swobodnego rysowania. Podczas ruszania kursorem po płótnie, chcemy użytkownikowi pokazywać pomocniczy wskaźnik. Posłuży do tego funkcja drawPointer(). Funkcja ta dodatkowo jest odpalana przy zmianie wielkości i koloru narzędzia (patrz klasa Tool).
Klasa Line
Kolejna klasa - Line - posłuży do rysowania pojedynczych linii. W tym przypadku musimy postąpić nieco inaczej niż przy rysowaniu swobodnym.
Użytkownik naciska klawisz i poruszając kursorem zaczyna dynamicznie kreślić linię. Aby było to możliwe, musimy podczas ruchu czyścić i ponownie rysować wygląd linii. Wykorzystamy do tego pomocnicze płótno. Gdy użytkownik puści klawisz myszy, linia powinna zostać na stałę narysowana - tym razem na głównym płótnie.
Klasa Rectangle
Kolejne narzędzie posłuży do rysowania kwadratów. Jego działanie będzie bliźniaczo podobne do powyższego. Różnica est tutaj w rysowaniu samego kwadratu. W poprzedniej klasie używaliśmy metody lineTo(x, y, x2, y2) dla której wystarczyło podać odpowiednie pozycje. W tym przypadku użyjemy metody strokeRect(x, y, width, height). Wymaga ona podania szerokości i wysokości rysowanego prostokąta, dlatego musimy dokonać lekkich wyliczeń.
Klasa koła
Zanim przejdziemy do poprawy wyglądu naszej aplikacji, spróbujmy dodać jeszcze jedno narzędzie - tym razem służące do rysowania kół czy też owali.
Po pierwsze dodajmy odpowiedni zapis w konfiguracji:
Po drugie stwórzmy odpowiedni plik z klasą i dodajmy ją do funkcji tworzącej odpowiednie narzędzia:
i dodajmy go do funkcji tworzącej narzędzia:
Zanim przejdziesz dalej, sprawdź czy możesz przełączyć się na powyższe narzędzie.
I tu jest dobry moment, byś spróbował sam napisać odpowiedni kod w klasie Circle.
Dobrze. Ja tym czasem spróbuję zrobić to po swojemu. Jeżeli już skończysz, spójrz na moje rozwiązanie.
Poprawiamy wygląd gui
Wreszcie coś normalnego. Pobawmy się wyglądem.
Omówmy poszczególne części. Na początku generuję cały HTML dla gui.
Tworzymy element div z klasą .gui, a następnie wypełniamy go odpowiednim HTML. Wewnątrz listy .gui-tools robiąc pętlę po config.tools (tak samo jak to robiliśmy w klasie Control) generujemy kolejne ikonki pobierając ich wygląd z tablicy icons. Same ikonki są w postaci SVG. Możesz je pobrać między innymi ze stron: https://boxicons.com/ i https://remixicon.com/.
Wewnątrz funkcji bindEvents() po pierwsze podpinamy kliknięcie wygenerowanym wcześniej ikonkom narzędzi. Po kliknięciu - podobnie jak to robiliśmy w pliku main.js generujemy pojedyncze narzędzie, a potem informujemy o tym resztę komponentów. Pozostaje podłączyć się pod odpowiednie sygnały.
Najprostsze funkcje w zestawieniu - odpalane są gdy dostaniemy informację o zmianie narzędzia czy zmianie koloru. Pobieramy tutaj odpowiedni element i aktualizujemy mu klasę.
Pozostaje dodać odpowiednie stylowanie. Idealnym rozwiązaniem będzie stworzenie dedykowanego pliku _gui.scss i dołączenie go do pliku main.scss:
Last updated