Canvas - tworzymy Painta

W ramach treningu poznanych rzeczy w tym artykule stworzymy prostą aplikację do rysowania.
Zanim zaczniemy pracę, zobacz skończoną aplikację.

HTML

Klasycznie zaczynamy od HTML.


<div class="paint">
    <div class="paint-bar">
        <input id="paintSize" type="range" class="paint-size" min="1" max="50" value="5">
        <output for="paintSize" class="paint-size-val"></span>

        <input type="color" class="paint-color">

        <div class="paint-buttons-cnt">
            <button data-mode="draw" class="button-mode" type="button">
                Draw
            </button>
            <button data-mode="line" class="button-mode" type="button">
                Line
            </button>
            <button data-mode="rectangle" class="button-mode" type="button">
                Rectangle
            </button>
        </div>
    </div>

    <div class="paint-canvas-cnt">
    </div>
</div>

Nasza aplikacja składa się z głównej belki z narzędziami .paint-bar oraz miejsca .paint-canvas-cnt do którego będziemy wstawiać dynamicznie utworzone canvasy.

Belka narzędzi zawiera input:range służący do wyboru wielkości pędzla do rysowania. Nie stylowałem tego elementu, bo jest to ogólnie trochę problematyczne, a i nie chciałem za bardzo śmiecić w kodzie. Jeżeli uznasz, że chcesz bardziej ten element upiększyć, warto skorzystać z jakiegoś pluginu, lub skorzystać z porad np. z tego artykułu lub z tego.

Dodatkowo belka posiada input:color - służący do wyboru koloru, oraz 3 buttony do wyboru aktualnego trybu rysowania - draw, line i rectangle. Każdy taki button ma atrubyt data-mode, który określa aktualny tryb rysowania. Dostaniemy się do nich przez dataset, ale to trochę później.

CSS

Zanim przejdziemy do pisania właściwej aplikacji, dodajmy do niej odpowiednie stylowanie. Zwróć uwagę, że canvas jest pozycjonowany absolutnie względem rodzica .paint-canvas-cnt. Wróci to do nas gdy będziemy rysować linie.

Dla przycisków trybów rysowania użyta jest grafika:

ikony przycisków painta

Poszczególne ikony uzyskujemy przez przesunięcie tła. Jest to znana technika stosowana w css sprites.


* {
    box-sizing: border-box;
}

.paint {
    width:1000px;
}

.paint-bar {
    padding:20px;
    border:1px solid #ccc;
    display: flex;
    justify-content: space-between;
    align-items: center;
    box-shadow: 0 1px 4px rgba(0,0,0,0.2);
    z-index: 3;
    position: relative;
    font-family: sans-serif;
}

/* range input */
.paint-bar .paint-size {
    cursor:pointer;
}

/* informacja o wartości range. Domyślnie range czegoś takiego nie ma */
.paint-bar .paint-size-val {
    min-width:2.3rem;
    border-radius: 3px;
    padding:0 10px;
    color:#333;
    background: #333;
    padding:5px;
    margin-right:30px;
    margin-left:10px;
    color:#fff;
    display: inline-block;
    text-align: center;
    position: relative;
}
.paint-bar .paint-size-val:before {
    content:'';
    position: absolute;
    left:-10px;
    top:50%;
    transform: translate(0, -50%);
    width:0;
    height:0;
    border:5px solid transparent;
    border-right-color:#333;
}

/* przycisk wyboru koloru */
.paint-bar .paint-color {
    height:60px;
    width:60px;
    border:1px solid #aaa;
    border-radius: 3px;
}
.paint-bar .paint-color::-webkit-color-swatch-wrapper {
    padding: 3px;
    border:0;
}
.paint-bar .paint-color::-webkit-color-swatch {
    border: none;
    border-radius: 3px;
}

/* przyciski z akcjami - draw, line, rectangle */
.paint-bar .paint-buttons-cnt {
    margin-left:auto;
}
.paint-bar .button-mode {
    cursor:pointer;
    height:60px;
    background: transparent;
    margin:0 5px;
    border:0;
    background: url(icons.png) no-repeat;
    border:1px solid #ccc;
    color:#aaa;
    border-radius: 3px;
    text-indent:-9999px;
    overflow: hidden;
    width:62px;
    height:62px;
}
.paint-bar .button-mode[data-mode="draw"] {
    background-position: 0 0;
}
.paint-bar .button-mode[data-mode="line"] {
    background-position: -60px 0;
}
.paint-bar .button-mode[data-mode="rectangle"] {
    background-position: -120px 0;
}
.paint-bar .button-mode:focus {
    outline: none;
}
.paint-bar .button-mode.active {
    border-color: red;
    box-shadow: inset 0 0 0 1px red;
}

/* miejsce na canvasy */
.paint-canvas-cnt {
    width:1000px;
    height:600px;
    position: relative;
}
.paint-canvas-cnt canvas {
    position: absolute;
    left:0;
    top:0;
    width:100%;
    height:100%;
}

Obiekt paint

Zaczynamy pisać skrypt. Nasz paint zrobimy jako klasę.


class Paint {
    constructor() {
        ..tutaj początkowe ustawienia
    }
}

Chcemy by nasza aplikacja miała jakieś tło, po którym będziemy rysować. W necie jest wiele takich tekstur - chociażby tutaj: https://www.freepik.com/free-photos-vectors/paper-texture.
Ja ściągnąłem jedną z nich i przeskalowałem na nasze potrzeby. Gotowe tło znajdziesz tutaj. Jeżeli jest ono dla Ciebie zbyt smutne, albo zwyczajnie nieciekawe, użyj swojego.
Pozostaje je użyć i po wczytaniu zacząć wykonywać akcje. Podobne działania już robiliśmy w poprzednich rozdziałach.


class Paint {
    constructor() {
        this.img = new Image();
        this.img.addEventListener("load", e => {

            ...

        });
        this.img.src = "canvas-bg.png";
    }
}

Żeby mieć dostęp do obiektu paint wewnątrz eventu load użyliśmy funkcji strzałkowej.

Tworzenie canvas

Pobierzmy teraz miejsce na wrzucenie canvasu i wrzućmy tam dynamicznie tworzony canvas, który za chwilę wygenerujemy


class Paint {
    constructor() {
        this.img = new Image();
        this.img.addEventListener("load", e => {
            //kontener dla canvasu
            this.canvasCnt = document.querySelector(".paint-canvas-cnt");
            this.createCanvas();
        });
        this.img.src = "canvas-bg.png";
    }
}

Do utworzenia canvasu skorzystamy z metody createCanvas() naszej klasy. Napiszmy ją:


class Paint {
    constructor() {
        this.img = new Image();
        this.img.addEventListener("load", e => {
            this.canvasCnt = document.querySelector(".paint-canvas-cnt");
            this.createCanvas();
        });
        this.img.src = "canvas-bg.png";
    }

    createCanvas() {
        const canvas = document.createElement("canvas");
        canvas.width = this.canvasCnt.offsetWidth;
        canvas.height = this.canvasCnt.offsetHeight;

        this.canvas = this.createCanvas();
        this.canvasCnt.appendChild(this.canvas);
        this.ctx = this.canvas.getContext("2d");
    }
}

Pobieranie elementów

Kolejnym krokiem będzie pobranie reszty elementów na których będziemy działać: input:range do pobierania wielkości pędzla, input:color do pobierania koloru:


class Paint {
    constructor() {
        this.img = new Image();
        this.img.addEventListener("load", e => {
            //elementy belki
            this.canvasCnt = document.querySelector(".paint-canvas-cnt");

            this.createCanvas();
            this.setControls();
        });
        this.img.src = "canvas-bg.png";
    }

    createCanvas() {
        ...
    }

    setControls() {
        //element pobierania wielkości pędzla
        this.sizeElem = document.querySelector(".paint-size");

        //element pokazujący wielkość pędzla
        this.sizeElemVal = document.querySelector(".paint-size-val");
        this.sizeElemVal.innerText = this.sizeElem.value;

        //element do pobierania koloru
        this.colorElem = document.querySelector(".paint-color");
    }
}

Dodatkowo pobierzmy buttony do przełączania trybu rysowania. Buttony pobieramy jako kolekcja elementów. Żeby łatwiej na takiej kolekcji działać, zamieńmy ją na tablicę. Dzięki temu będziemy mogli na niej wykonywać takie metody jak filter, forEach itp:


class Paint {
    constructor() {
        this.img = new Image();
        this.img.addEventListener("load", e => {
            //elementy belki
            this.canvasCnt = document.querySelector(".paint-canvas-cnt");

            this.createCanvas();
            this.setupControls();
        });
        this.img.src = "canvas-bg.png";
    }

    createCanvas() {
        ...
    }

    setupControls() {
        //element pobierania wielkości pędzla
        this.sizeElem = document.querySelector(".paint-size");

        //element pokazujący wielkość pędzla
        this.sizeElemVal = document.querySelector(".paint-size-val");
        this.sizeElemVal.innerText = this.sizeElem.value;

        //element do pobierania koloru
        this.colorElem = document.querySelector(".paint-color");

        //przyciski akcji - zamieniamy je na tablicę by łatwiej działać
        this.btnsMode = [...document.querySelectorAll(".paint-buttons-cnt .button-mode")];

        //dla przycisku z trybem draw dodajemy klasę active
        this.btnsMode.filter(el => {
            return el.dataset.mode === "draw"
        })[0].classList.add("active");
    }
}

Po pobraniu wszystkich elementów ustawiamy aktualny tryb rysowania na draw oraz czy można rysować. Nasza aplikacja będzie reagować na myszkę. Jeżeli lewy klawisz myszki będzie wciśnięty, oznacza to, że można rysować. Jeżeli będzie można rysować, wtedy wykryjemy pozycję myszki (mousemove) na canvasie i wykonamy odpowiednią akcję.


class Paint {
    contructor() {
        this.img = new Image();
        this.img.addEventListener("load", e => {
            ...

            //czy mozemy rysowac
            this.canDraw = false;
            this.mode = "draw";
        });
        this.img.src = "canvas-bg.png";
    }

    ...
}

Wstępne ustawienia dla canvas

Zanim to wszystko napiszemy, musimy zająć się wstępnym ustawieniem właściwości canvas - np. ustawieniem wielkości i koloru początkowego pędzla, tła canvasu itp.


class Paint {
    constructor() {
        this.img = new Image();
        this.img.addEventListener("load", e => {
            ...
            this.canDraw = false;
            this.mode = "draw";
            this.setupInitialCtx();
        });
        this.img.src = "canvas-bg.png";
    }

    ...

    setupInitialCtx() {
        //tło canvasu
        this.ctx.drawImage(this.canvasBg, 0, 0);

        //początkowe ustawienia pędzla
        this.ctx.lineWidth = this.sizeElem.value;
        this.ctx.lineJoin = "round";
        this.ctx.lineCap = "round";
        this.ctx.strokeStyle = this.colorElem.value;
    }
}

paint.init();

Podpięcie elementów

Kolejną metodą, którą napiszemy będzie metoda podpinająca wszystkie elementy w naszej aplikacji:


class Paint {
    constructor() {
        this.img = new Image();
        this.img.addEventListener("load", e => {
            ...
            this.canDraw = false;
            this.mode = "draw";
            this.setupInitialCtx();
            this.setupControls();
            this.bindControls();
        });
        this.img.src = "canvas-bg.png";
    }

    ...

    bindControls() {
        //dla każdego elementu przypinamy metody bindem
        //bo chcemy w nich mieć dostęp do naszego obiektu paint
        this.sizeElem.addEventListener("change", this.changeSize.bind(this));
        this.sizeElem.addEventListener("input", this.changeSize.bind(this));

        this.colorElem.addEventListener("change", this.changeColor.bind(this))

        this.canvasCnt.addEventListener("mousemove", this.mouseMove.bind(this));
        this.canvasCnt.addEventListener("mouseup", this.mouseDisable.bind(this));
        this.canvasCnt.addEventListener("mousedown", this.mouseEnable.bind(this));

        //po kliknięciu w przycisk zmiany trybu rysowania
        //wszystkim jego braciom wyłączamy klasę .active, a włączamy tylko temu klikniętemu
        //dodatkowo ustawiamy tryb rysowania na pobrany z dataset.mode klikniętego przycisku
        for (const el of this.btnsMode) {
            el.addEventListener("click", (e) => {
                e.currentTarget.classList.add("active");
                this.mode = e.currentTarget.dataset.mode;

                for (const el of this.btnsMode) {
                    if (el !== e.currentTarget) {
                        el.classList.remove("active");
                    }
                };
            });
        }
    }
}

Jak widzisz metoda ta podpina do wcześniej pobranych elementów różne metody - changeSize() - do zmiany wielkości pędzla, changeColor() - do zmiany koloru, oraz najważniejsze - obsługę myszy dla płótna.

W kolejnych krokach zajmiemy się właśnie tymi metodami.

Zmiana wielkości i koloru pędzla

Kolejnym krokiem jest napisanie metod do zmiany wielkości i koloru pędzla:


class Paint {
    ...

    changeSize(e) {
        //wartość wyświetlana przy input:range
        this.sizeElemVal.innerText = e.target.value;
        //zmieniamy wielkość rysowania
        this.ctx.lineWidth = e.target.value;
    }

    changeColor(e) {
        //zmieniamy kolor rysowania
        const color = this.colorElem.value;
        this.ctx.strokeStyle = color;
    }
}

Przełączanie trybu rysowania

Po kliknięciu w przyciski zmieniające tryb rysowania odpalamy metodę enableMode, która przełącza aktualny tryb. Tryb taki pobieramy z atrybutu data-mode za pomocą dataset klikniętego przycisku (robimy to w powyżej napisanej metodzie bindElements). Żeby się zabezpieczyć przed błędami (albo i celowym działaniem - np. ręczną zmianą tych atrybutów), zróbmy dodatkowe sprawdzanie, czy włączany tryb pasuje do zadeklarowanych przez nas:


class Paint {
    constructor() {
        this.avaibleMode = ["draw", "line", "rectangle"];
        this.img = new Image();
        this.img.addEventListener("load", e => {
            ...
        });
        this.img.src = "canvas-bg.png";
    }

    ...

    enableMode(mode) {
        //sprawdzamy czy włączany tryb jest poprawny
        if (this.avaibleMode.indexOf(mode) !== -1) {
            this.mode = mode;
        }
    }
}

Rysowanie

Przejdźmy teraz do najważniejszych metod służących do rysowania po płótnie.
Jak takie rysowanie wygląda w praktyce? Będąc kursorem na płótnie użytkownik naciska klawisz myszki (mousedown). Trzymając go wciśniętego rusza kursorem (mousemove) co powoduje rysowanie. Gdy puści klawisz myszy (mouseup) rysowanie jest przerywane.

W naszym przypadku wykorzystamy trzy zdarzenia: mousedown, mousemove i mouseup. Gdy użytkownik naciśnie na płótnie lewy klawisz myszy, wtedy ustawimy zmienną canDraw na true. Jeżeli taki klawisz puści, canDraw zostanie ustawione na false.
W zdarzeniu mousemove będziemy sprawdzać, czy zmienna canDraw jest ustawiona na true. Jeżeli tak, znaczy to że użytkownik właśnie rysuje. Jeżeli nie - nie rysuje. Proste.

Pseudokod mógłby wyglądać tak:


let canDraw = false;

canvas.mouseDown = function() {
    canDraw = true;
}

canvas.mouseUp = function() {
    canDraw = false;
}

canvas.mouseMove = function() {
    if (canDraw) {
        //rysujemy
    } else {
        //nie rysujemy
    }
}

Odpowiednie zdarzenia już podpięliśmy w metodzie bindElements.

W naszym przypadku po kliknięciu klawisza myszki wywołujemy metodę mouseEnable, która rozpoczyna path w danym punkcie. Potem następuje przesuwanie kursora (metoda mouseMove) w której używana jest metoda lineTo() i stroke() - zupełnie tak jak w rozdziale o canvas. Po zakończeniu rysowania przestawiamy zmienną canDraw na false:


class Paint {
    ...

    mouseEnable(e) {
        this.canDraw = true;
        this.ctx.beginPath();
        this.ctx.moveTo(this.startX, this.startY);
    }

    mouseDisable(e) {
        this.canDraw = false;
    },

    mouseMove(e) {
        if (this.canDraw) {
            const mousePos = this.getMousePosition(e);

            if (this.mode === "draw") {
                this.ctx.lineTo(mousePos.x, mousePos.y);
                this.ctx.stroke();
            }
        }
    }

    bindControls() {
        ...
        this.canvasCnt.addEventListener("mousemove", this.mouseMove.bind(this));
        this.canvasCnt.addEventListener("mouseup", this.mouseDisable.bind(this));
        this.canvasCnt.addEventListener("mousedown", this.mouseEnable.bind(this));

        for (const el of this.btnsMode) {
            ...
        }
    },
}

Zauważ, że w metodzie mouseMove() do pobrania pozycji kursora na canvasie wykorzystujemy metodę getMousePosition(), której jeszce nie mamy. Napiszmy ją teraz:


class Paint {
    ...

    getMousePosition(e) {
        const mouseX = e.pageX - this.getElementPos(this.canvas).left;
        const mouseY = e.pageY - this.getElementPos(this.canvas).top;

        return {
            x: mouseX,
            y: mouseY
        };
    }

    ...
}

Tak jak to robiliśmy w rozdziale o myszce za pomocą e.pageX i e.pageY pobieramy pozycję kursora od lewego górnego narożnika strony.

Aby obliczyć pozycję kursora na naszym obszarze rysowania, od takiej pozycji myszki musimy odjąć aktualną pozycję elementu canvas.

Do wyliczenia takiej pozycji canvasu musimy użyć właściwości offsetLeft i offsetTop, które zwracają pozycję względem rodzica danego elementu. Taki rodzic też może być odsunięty od swojego rodzica, a ten z kolei od swojego itp. Dlatego prawdziwą pozycję canvas musimy wyliczyć w pętli while, w której pobieramy kolejnych rodziców (aż do body) i sumujemy ich przesunięcia.


class Paint {
    ...

    getMousePosition(e){
        const mouseX = e.pageX - this.getElementPos(this.canvas).left;
        const mouseY = e.pageY - this.getElementPos(this.canvas).top;

        return {
            x: mouseX,
            y: mouseY
        };
    }

    getElementPos(obj) {
        let top = 0;
        let left = 0;
        while (obj && obj.tagName != "BODY") {
            top += obj.offsetTop - obj.scrollTop;
            left += obj.offsetLeft - obj.scrollLeft;
            obj = obj.offsetParent;
        }
        return {
            top: top,
            left: left
        };
    }

    ...
}

W tym momencie nasza aplikacja ma już zaimplementowane podstawowe rysowanie:

Demo 1

Rysowanie linii

Kolejnym trybem, który zaprogramujemy będzie rysowanie linii.

Chcemy by takie rysowanie działało tak jak w popularnych programach graficznych (np. Adobe Photoshop).

Użytkownik naciska lewy klawisz myszy. Następnie nie puszczając go i przeciągając kursorem rysuje linię. Gdy puści klawisz myszy linia zostanie narysowana.

Poniżej nagrałem jak takie rysowanie wygląda w Photoshopie:

animacja rysowania linii

Powiedzmy, że my robimy wersję z Photoshopa CS6, więc ten pomocniczy Tooltip, który lata przy kursorze nas nie interesuje.

Aby stworzyć takie "dynamiczne" rysowanie linii musielibyśmy w każdej klatce przerysowywać cały canvas. W przeciwnym razie rysowana w każdej klatce nowa pozycja linii zamazała by nasz canvas, bo w każdej klatce rysowalibyśmy nową linię.

Moglibyśmy spróbować tutaj użyć dodatkowej zmiennej, do której przenosilibyśmy obecny stan canvasu, po czym rysowalibyśmy linię, przenosilibyśmy zapisany stan ze zmiennej na canvas itp. Działanie takie było by dla nas jednak mało wygodne, a i mało wydajne, bo oznaczało by całościowe przenoszenie całego canvasu przy każdej klatce rysowania.

W większości programów graficznych, jeżeli chcesz rysować coś, co nie wpłynie na resztę grafiki, tworzysz nową warstwę i właśnie na niej zaczynasz swoje rysowanie. Dzięki temu możesz po niej mazać, czyścić ją itp, a całość nie ma wpływu na resztę grafiki. W naszym przypadku taką warstwą będzie kolejny canvas, który za pomocą pozycjonowania absolutnego umieścimy bezpośrednio na obecnym canvasie. Po zakończeniu rysowania przeniesiemy jego zawartość na canvas właściwy.

warstwy canvas

Zanim przejdziemy do właściwego rysowania linii, musimy stworzyć drugi canvas. Zrobimy go tak samo jak pierwszy czyli w metodzie createCanvas():


class Paint {
    ...

    createCanvas() {
        this.canvas = document.createElement("canvas");
        this.canvas.width = this.canvasCnt.offsetWidth;
        this.canvas.height = this.canvasCnt.offsetHeight;
        this.canvasCnt.appendChild(this.canvas);
        this.ctx = this.canvas.getContext("2d");

        this.canvas2 = document.createElement("canvas");
        this.canvas2.width = this.canvasCnt.offsetWidth;
        this.canvas2.height = this.canvasCnt.offsetHeight;
        this.canvasCnt.appendChild(this.canvas2);
        this.ctx2 = this.canvas2.getContext("2d");
    }

    ...
}

Podobnie jak dla pierwszego canvas musimy ustawić podstawowe ustawienia tego canvasu:


class Paint {
    ...

    setupInitialCtx() {
        //tło canvasu
        this.ctx.drawImage(this.canvasBg, 0, 0);

        //początkowe ustawienia pędzla
        this.ctx.lineWidth = this.sizeElem.value;
        this.ctx.lineJoin = "round";
        this.ctx.lineCap = "round";
        this.ctx.strokeStyle = this.colorElem.value;

        //zaokrąglenia nie musimy tutaj ustawiać
        this.ctx2.lineWidth = this.sizeElem.value;
        this.ctx2.strokeStyle = this.colorElem.value;
    }

    ...
}

Oraz obsłużyć zmianę wielkości pędzla i jego koloru:


class Paint {
    ...

    changeSize(e) {
        //wartość wyświetlana przy input:range
        this.sizeElemVal.innerText = e.target.value;
        //zmieniamy wielkość rysowania
        this.ctx.lineWidth = e.target.value;
        this.ctx2.lineWidth = e.target.value;
    }

    changeColor(e) {
        //zmieniamy kolor rysowania
        const color = this.colorElem.value;
        this.ctx.strokeStyle = color;
        this.ctx2.strokeStyle = color;
    }

    ...
}

Aby narysować linię musimy przerobić nasze funkcje służące do obsługi myszki.

Pierwszą, którą się zajmiemy to mouseEnable(). Aby rysować dynamicznie linię, musimy ją rysować od pozycji pierwszego kliknięcia (początkowej), do aktualnej pozycji kursora gdy trzymany jest lewy klawisz myszki. Tą początkową pozycję musimy zapamiętać pod jakimiś zmiennymi, które będą dostępne dla pozostałych metod:


class Paint {
    ...

    mouseEnable(e) {
        this.canDraw = true;

        const mousePos = this.getMousePosition(e);
        this.startX  = mousePos.x;
        this.startY = mousePos.y;

        this.ctx.beginPath();
        this.ctx.moveTo(this.startX, this.startY);
    }

    ...
}

Mając pozycję pierwszego kliknięcia możemy zacząć rysować linie. Nie będziemy jej rysować po naszym pierwszym płótnie, a po drugim. Przerabiamy więc metodę mouseMove():


class Paint {
    ...

    mouseMove(e) {
        if (this.canDraw) {
            const mousePos = this.getMousePosition(e);

            if (this.mode === "draw") {
                this.ctx.lineTo(mousePos.x, mousePos.y);
                this.ctx.stroke();
            }
            if (this.mode === "line") {
                //w każdej klatce czyścimy canvas2
                this.ctx2.clearRect(0, 0, this.canvas2.width, this.canvas2.height);
                this.ctx2.beginPath();
                //rysujemy linię od początkowej pozycji
                this.ctx2.moveTo(this.startX, this.startY);
                //do aktualnej pozycji kursora
                this.ctx2.lineTo(mousePos.x, mousePos.y);
                this.ctx2.closePath();
                this.ctx2.stroke();
            }
        }
    }

    ...
}

Ostatnia metoda którą przerobimy to mouseDisable(). Właśnie skończyliśmy rysować linię na 2 canvasie. Po zakończeniu rysowania - czyli właśnie w tej metodzie musimy cały 2 canvas przenieść na pierwszy. Po takim przeniesieniu 2 canvas wyczyścimy.


class Paint {
    ...

    mouseDisable(e) {
        this.canDraw = false;

        if (this.mode === "line") {
            //klonujemy canvas2 na 1
            this.ctx.drawImage(this.canvas2, 0, 0);
            //czyścimy 2 canvas
            this.ctx2.clearRect(0, 0, this.canvas2.width, this.canvas2.height);
        }

    }

    ...
}

W tej chwili nasza aplikacja zyskała możliwość rysowania linii:

Demo 2

Rysowanie kwadratu

Rysowanie kwadratu będzie podlegać takim samym zasadom jak rysowanie linii. Znowu - rysujemy dynamicznie na 2 canvasie. Po narysowaniu, całość przenosimy na pierwszy canvas.

Canvas mamy już stworzony, wszystkie metody są gotowe, wystarczy tylko wprowadzić małe poprawki.
W metodzie mouseMove() dodajemy dodatkowy wariant dla rysowania kwadratu, a w przypadku mouseDisable() dodajemy kolejny wariant dla czyszczenia drugiego canvas:


class Paint {
    ...

    mouseMove(e) {
        if (this.canDraw) {
            const mousePos = this.getMousePosition(e);

            if (this.mode === "draw") {
                this.ctx.lineTo(mousePos.x, mousePos.y);
                this.ctx.stroke();
            }
            if (this.mode === "line") {
                this.ctx2.clearRect(0, 0, this.canvas2.width, this.canvas2.height);
                this.ctx2.beginPath();
                this.ctx2.moveTo(this.startX, this.startY);
                this.ctx2.lineTo(mousePos.x, mousePos.y);
                this.ctx2.closePath();
                this.ctx2.stroke();
            }
            if (this.mode === "rectangle") {
                this.ctx2.clearRect(0, 0, this.canvas2.width, this.canvas2.height);
                this.ctx2.beginPath();
                this.ctx2.moveTo(this.startX, this.startY);
                this.ctx2.rect(this.startX, this.startY, mousePos.x-this.startX, mousePos.y-this.startY);
                this.ctx2.closePath();
                this.ctx2.stroke();
            }
        }
    }

    mouseDisable(e) {
        this.canDraw = false;

        if (this.mode === "line" || this.mode === "rectangle") {
            //klonujemy canvas2 na 1
            this.ctx.drawImage(this.canvas2, 0, 0);
            //czyścimy 2 canvas
            this.ctx2.clearRect(0, 0, this.canvas2.width, this.canvas2.height);
        }
    }

    ...
}

I to wszystko. Nasza aplikacja może nie równa się Photoshopowi, ale kwadrat narysuje:

Demo 3

Paczkę z gotową aplikacją możesz pobrać tutaj.

Trening czyni mistrza

Poniżej zamieszczam kilka zadań, które w ramach ćwiczenia możesz wykonać:

  1. Do naszego painta dodaj rysowanie kół i trójkątów. Całość będzie bardzo podobna do rysowania kwadratów.
    Dodatkowo będziesz musiał stworzyć 2 przyciski z odpowiednimi ikonami.

    ikony dodatkowe

    W przypadku trójkąta możesz wykorzystać path (rysowanie linii) i podobne podejście co w przypadku prostokąta:

    trójkąt

Wszelkie prawa zastrzeżone. Jeżeli chcesz używać jakiejś części tego kursu, skontaktuj się z autorem.
Aha - i ta strona korzysta z ciasteczek.

Menu