Canvas - tworzymy Painta

W ramach treningu poznanych rzeczy w tym artykule stworzymy prostą aplikację do rysowania.

Zanim zaczniemy pracę, zobacz skończoną aplikację. Właście coś takiego jest naszym celem.

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 zwykły obiekt.


const paint = {
    init : function() {
        ..tutaj poczatkowe ustawienia
    }
}

paint.init(); //od razu odpalamy metodę inicjującą

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.


const paint = {
    init : function() {
        this.img = new Image();
        this.img.addEventListener('load', function() {

            ...

        }.bind(this));
        this.img.src = 'canvas-bg.png';
    }
}

paint.init();

Żeby mieć dostęp do obiektu paint wewnątrz eventu load podpielipodpinamy śmy mu "właściwe" this za pomocą metody bind()

Tworzenie canvas

Pobierzmy teraz miejsce na wrzucenie canvasu i wrzućmy tam dynamicznie twrzony canvas:


const paint = {
    init : function() {
        this.img = new Image();
        this.img.addEventListener('load', function() {
            //kontener dla canvasu
            this.canvasCnt = document.querySelector('.paint-canvas-cnt');

            //canvas
            this.canvasElem = this.createCanvas();
            this.canvasCnt.appendChild(this.canvasElem);
            this.ctx = this.canvasElem.getContext('2d');
        }.bind(this));
        this.img.src = 'canvas-bg.png';
    }
}

paint.init();

Do utworzenia canvasu skorzystamy z metody createCanvas() naszego obiektu. Napiszmy ją:


const paint = {
    createCanvas : function() {
        const canvasElem = document.createElement('canvas');
        canvasElem.width = this.canvasCnt.offsetWidth;
        canvasElem.height = this.canvasCnt.offsetHeight;
        return canvasElem;
    },

    init : function() {
        this.img = new Image();
        this.img.addEventListener('load', function() {
            //kontener dla canvasu
            this.canvasCnt = document.querySelector('.paint-canvas-cnt');

            //canvas
            this.canvasElem = this.createCanvas();
            this.canvasCnt.appendChild(this.canvasElem);
            this.ctx = this.canvasElem.getContext('2d');
        }.bind(this));
        this.img.src = 'canvas-bg.png';
    }
}

paint.init();

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:


const paint = {
    createCanvas : function() {
        ...
    },
    init : function() {
        this.img = new Image();
        this.img.addEventListener('load', function() {
            //elementy belki
            this.canvasCnt = document.querySelector('.paint-canvas-cnt');

            //canvas
            this.canvasElem = this.createCanvas();
            this.canvasCnt.appendChild(this.canvasElem);
            this.ctx = this.canvasElem.getContext('2d');

            //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');

        }.bind(this));
        this.img.src = 'canvas-bg.png';
    }
}

paint.init();

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:


const paint = {
    createCanvas : function() {
        ...
    },
    init : function() {
        this.img = new Image();
        this.img.addEventListener('load', function() {
            //elementy belki
            this.canvasCnt = document.querySelector('.paint-canvas-cnt');

            //canvas
            this.canvasElem = this.createCanvas();
            this.canvasCnt.appendChild(this.canvasElem);
            this.ctx = this.canvasElem.getContext('2d');

            //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 = [].slice.call(document.querySelectorAll('.paint-buttons-cnt .button-mode'));

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

        }.bind(this));
        this.img.src = 'canvas-bg.png';
    }
}

paint.init();

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ę.


const paint = {
    createCanvas : function() {
        ...
    },
    init : function() {
        this.img = new Image();
        this.img.addEventListener('load', function() {
            ...

            //czy mozemy rysowac
            this.canDraw = false;
            this.mode = 'draw';

        }.bind(this));
        this.img.src = 'canvas-bg.png';
    }
}

paint.init();

Wstępne ustawienia dla canvas

Zanim to wszystko oskryptujemy, 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.


const paint = {
    createCanvas : function() {
        ...
    },

    setupInitialCtx : function() {
        //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;
    },

    init : function() {
        this.img = new Image();
        this.img.addEventListener('load', function() {
            ...

            //czy możemy rysować
            this.canDraw = false;

            this.setupInitialCtx();
        }.bind(this));
        this.img.src = 'canvas-bg.png';
    }
}

paint.init();

Podpięcie elementów

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


const paint = {
    createCanvas : function() {
        ...
    },

    setupInitialCtx : function() {
        ...
    },

    bindElements : function() {
        //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
        this.btnsMode.forEach(function(el) {
            el.addEventListener('click', function(e) {
                e.currentTarget.classList.add('active');
                this.mode = e.currentTarget.dataset.mode;

                this.btnsMode.forEach(function(el2) {

                    if (el2 !== e.currentTarget) {
                        el2.classList.remove('active');
                    }
                });
            }.bind(this));
        }, this);
    },

    init : function() {
        this.img = new Image();
        this.img.addEventListener('load', function() {
            ...

            this.setupInitialCtx();
            this.bindElements();
        }.bind(this));
        this.img.src = 'canvas-bg.png';
    }
}

paint.init();

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:


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

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

    createCanvas : function() {
        ...
    },

    setupInitialCtx : function() {
        ...
    },

    bindElements : function() {
        ...
    },

    init : function() {
        ...
    }
}

paint.init();

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:


const paint = {
    changeSize : function(e) {
        ...
    },

    changeColor : function(e) {
        ...
    },

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

    createCanvas : function() {
        ...
    },

    setupInitialCtx : function() {
        ...
    },

    bindElements : function() {
        ...
    },

    init : function() {
        this.avaibleMode = ['draw', 'line', 'rectangle'];
        ...
    }
}

paint.init();

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 spradzać, 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 przesówanie 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:


const paint = {
    changeSize : function(e) {
        ...
    },

    changeColor : function(e) {
        ...
    },

    enableMode : function(mode) {
        ...
    },

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

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

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

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

    createCanvas : function() {
        ...
    },

    setupInitialCtx : function() {
        ...
    },

    bindElements : function() {
        ...
        this.canvasCnt.addEventListener('mousemove', this.mouseMove.bind(this));
        this.canvasCnt.addEventListener('mouseup', this.mouseDisable.bind(this));
        this.canvasCnt.addEventListener('mousedown', this.mouseEnable.bind(this));
        ...
    },

    init : function() {
        ...
    }
}

paint.init();

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


const paint = {
    ...

    getMousePosition : function(e){
        const mouseX = e.pageX - this.getElementPos(this.canvasElem).left;
        const mouseY = e.pageY - this.getElementPos(this.canvasElem).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.


const paint = {
    ...

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

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

    getElementPos: function(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:


const paint = {
    ...

    init : function() {
        this.img = new Image();
        this.img.addEventListener('load', function() {
            //elementy belki
            this.canvasCnt = document.querySelector('.paint-canvas-cnt');

            //canvas
            this.canvasElem = this.createCanvas();
            this.canvasCnt.appendChild(this.canvasElem);
            this.ctx = this.canvasElem.getContext('2d');

            //canvas2
            this.canvasElem2 = this.createCanvas();
            this.canvasCnt.appendChild(this.canvasElem2);
            this.ctx2 = this.canvasElem2.getContext('2d');

            //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 = [].slice.call(document.querySelectorAll('.paint-buttons-cnt .button-mode'));

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

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

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


const paint = {
    ...

    setupInitialCtx : function() {
        //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:


const paint = {
    ...

    changeSize : function(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 : function(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:


const paint = {
    ...

    mouseEnable : function(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():


const paint = {
    ...

    mouseMove : function(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.canvasElem2.width, this.canvasElem2.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.


const paint = {
    ...

    mouseDisable : function(e) {
        this.canDraw = false;

        if (this.mode === 'line') {
            //klonujemy canvas2 na 1
            this.ctx.drawImage(this.canvasElem2, 0, 0);
            //czyścimy 2 canvas
            this.ctx2.clearRect(0, 0, this.canvasElem2.width, this.canvasElem2.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() odajemy kolejny wariant dla czyszczenia drugiego canvas:


const paint = {
    ...

    mouseMove : function(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.canvasElem2.width, this.canvasElem2.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.canvasElem2.width, this.canvasElem2.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 : function(e) {
        this.canDraw = false;

        if (this.mode === 'line' || this.mode === "rectangle") {
            //klonujemy canvas2 na 1
            this.ctx.drawImage(this.canvasElem2, 0, 0);
            //czyścimy 2 canvas
            this.ctx2.clearRect(0, 0, this.canvasElem2.width, this.canvasElem2.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ójkató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