Canvas

Canvas czyli płótno pozwala nam dynamicznie rysować po swojej powierzchni.

Ściągawkę zawierającą zbiór metod elementu canvas możesz ściągnąć tutaj

Aby utworzyć canvas, skorzystamy z kodu:


<canvas width="200" height="200" id="can">
    ...treść alternatywna...
</canvas>

Atrybuty width i height są opcjonalne. Jeżeli ich nie podamy, wówczas element przyjmie standardowe dla siebie rozmiary 300 x 150. Treść alternatywna będzie wyświetlana, gdy przeglądarka nie obsługuje tego typu elementów.

Odwołanie się do canvas

Aby zacząć rysowanie po naszym polu, musimy pobrać jego zawartość. Służy do tego metoda getContext('2d'). Tryb 2d służy do rysowania 2d. Pozostałe możliwości to webgl, webgl2 i bitmaprender.


const canvasElem = document.getElementById('canvas');
const ctx = canvasElem.getContext('2d');

//...rysujemy

Rysowanie prostokątów

Element canvas udostępnia nam metody:

fillRect(x, y, width, height) rysuje wypełniony prostokąt
strokeRect(x, y, width, height) rysuje obramowanie prostokąta
clearRext(x, y, width, height) czyści określony obszar i czyni go przezroczystym

const canvasElem = document.getElementById('canvas1');
const ctx = canvasElem.getContext('2d');

//rysujemy niebieski kwadrat
ctx.fillRect(25,25,100,100);
//wycinamy jego srodek
ctx.clearRect(45,45,60,60);

//rysujemy obramowanie drugiego kwadratu
ctx.strokeRect(50,50,50,50);

Tekst

Aby wypisać tekst, możemy skorzystać z dwóch metod:

  • fillText("tekst",x,y) - która wypisuje wypełniony tekst w pozycji x, y
  • strokeText("tekst",x,y) - która wypisuje obrysowany tekst w pozycji x, y

Dodatkowo mamy do dyspozycji kilka właściwości, które pozwalają nam zmieniać wygląd tekstu:

  • font = "wartość" - opis wyglądu czcionki taki sam jaki stosujemy w CSS
  • textAlign = "wartość" - wyrównanie tekstu w poziomie. Możliwe wartości to: start, end, left, right, center. Domyślną wartością jest start.
  • textBaseline = "wartość" - Określa pionowe wyrównanie tekstu względem linii. Możliwe wartości to: top, hanging, middle, alphabetic, ideographic, bottom. Domyślną jest alphabetic.

const canvasElem = document.getElementById('canvasTestText');
const ctx = canvasElem.getContext('2d');

ctx.font = "italic bold 30px Arial";
ctx.textBaseline = "middle";
ctx.fillText('Ala ma kota', 0, 30);

ctx.font = "italic bold 20px Arial";
ctx.textBaseline = "bottom";
ctx.strokeText('Ala ma kota', 30, 70);

ctx.font = "normal 10px Arial";
ctx.textBaseline = "top";
ctx.strokeText('Ala ma kota', 130, 80);

Ścieżki

Do rysowania większości rzeczy po canvasie korzystać będziemy ze ścieżek. Ich działanie jest identyczne jak zasada działania piórka w programach wektorowych. Rysowanie takie odbywa się w kilu krokach:

  • rozpoczynamy rysowanie niewidzialnej ścieżki - beginPath()
  • rysujemy poszczególne części ścieżki
  • obrysowujemy stroke() lub wypełniamy fill() naszą ścieżkę

const canvasElem = document.getElementById('canvas2');
const ctx = canvasElem.getContext('2d');

ctx.beginPath();
//...rysujemy scieżke
ctx.fill() //wypełniamy scieżke

Do przesuwania piórka bez wyznaczania ścieżki służy metoda moveTo(x,y), która przesuwa punkt rysowania do punku x, y.

Rysujemy linie w ścieżce

Do rysowania linii wykorzystujemy metodę lineTo(x,y), która rysuje ścieżkę z aktualnej pozycji do pozycji x, y.

Przykładowo narysujmy trójkąt:


ctx.beginPath();
ctx.moveTo(35,10); //rysowanie zaczynamy od punktów 35,10 - tam więc przesuwamy nasze piórko
ctx.lineTo(60,40);
ctx.lineTo(10,40);
ctx.lineTo(35,10);
ctx.stroke();

ctx.fillText('a',30,60);
ctx.fillText('c',110,60);
ctx.fillText('b',70,130);

Podczas rysowania drugiego trójkąta skorzystaliśmy z metody closePath(), która łączy ostatni punkt ścieżki z pierwszym, tym samym zamykając całą ścieżkę. Przy stosowaniu wypełnienia fill() możemy ten krok pominąć, gdyż wypełniane kształty są automatycznie zamykane.


const canvasElem = document.getElementById('canvas_3');
const data = [30,30,40,100,30,20,50,10,5,7,3,15,20,60,28,15,10,20,10,70];
const stepSize = parseInt(400 / data.length);
const ctx = canvasElem.getContext('2d');

ctx.beginPath();
//górne ramie
ctx.moveTo(0, 150-data[0]);
ctx.fillText(data[0], 0, 150-data[0]-10);
let bottomI = 0;

for (let i=1; i<data.length; i++) {
    ctx.lineTo(i*stepSize, 150-data[i]);
    ctx.fillText(data[i], i*stepSize, 150-data[i]-10);
    bottomI = i;
}

//zamykamy kształt od dołu
ctx.lineTo(bottomI*stepSize, 150);
ctx.lineTo(0, 150);
ctx.closePath();

//obrysowujemy
ctx.fill();

Rysowanie łuków w ścieżkach

Kolejnym kształtem który możemy dołączyć do naszej ścieżki jest łuk:

arc(x, y, r, start, koniec, kierunek rysowania [false lub true])

Atrybuty x i y określają miejsce postawienia igły cyrkla. Start i koniec określają graniczne kąty, w których będzie zawierał się nasz łuk. W przypadku canvas są one podawane w radianach, dlatego musimy je skonwertować do stopni za pomocą wzoru:

radians = (Math.PI/180)*kat

Ostatni parametr określa, czy łuk ma być rysowany zgonie z ruchem wskazówek czy nie.


function radianAngle(angle) {
    return radians = (Math.PI/180)*angle;
}

const canvasElem = document.getElementById('canvas_4');
const ctx = canvasElem.getContext('2d');
for (let i=0; i<4; i++) {
    ctx.beginPath();
    ctx.arc(75,75,40+(i*10), radianAngle(10), radianAngle(300),false);
    ctx.stroke();
}

Rysowanie krzywych w ścieżkach

Aby narysować krzywe skorzystamy z funkcji:

  • quadraticCurveTo(pk1x, pk1y, x, y) - rysuje kwadratową ścieżkę do punktu x, y. Atrybuty pk1x i pk1y określają położenie punktu kontrolnego wyginającego ścieżkę
  • bezierCurveTo(pk1x, pk1y, pk2x, pk2y, x, y) - rysuje ścieżkę beziera do punktu x, y. Atrybuty pk1x, pk1y, pk2x, pk2y określają położenie punktów kontrolnych.

krzywe

Tak samo jak poprzednio przyda się doświadczenie przy pracy z krzywymi w programach graficznych (piórkiem w Adobe). Jako, że pierwsza z nich nie jest obsługiwana przez Firefox 1.5, polecam skupienie się na drugiej.


const canvasElem = document.getElementById('canvas5');
const ctx = canvasElem.getContext('2d');

ctx.beginPath();

//rysujemy oczy
ctx.moveTo(60,20);
ctx.lineTo(60,50);
ctx.moveTo(90,20);
ctx.lineTo(90,50);

//brwi
ctx.moveTo(50,20);
ctx.lineTo(70,5);
ctx.moveTo(80,5);
ctx.lineTo(100,20);

//rysujemy uśmiech
ctx.moveTo(10,80);
ctx.bezierCurveTo(30,130,120,130,140,80);
ctx.moveTo(10,80);
ctx.bezierCurveTo(30,150,120,150,140,80);
ctx.stroke();

Rysiek jest zadowolony.

Wykorzystując krzywe, możemy w łatwy sposób napisać funkcję, która będzie tworzyła prostokąty z zaokrąglonymi rogami


function roundedRect(canvas, x ,y, width, height, radius){
    ctx.beginPath();
    ctx.moveTo(x,y+radius);
    ctx.lineTo(x,y+height-radius);
    ctx.quadraticCurveTo(x,y+height,x+radius,y+height);
    ctx.lineTo(x+width-radius,y+height);
    ctx.quadraticCurveTo(x+width,y+height,x+width,y+height-radius);
    ctx.lineTo(x+width,y+radius);
    ctx.quadraticCurveTo(x+width,y,x+width-radius,y);
    ctx.lineTo(x+radius,y);
    ctx.quadraticCurveTo(x,y,x,y+radius);
    ctx.stroke();
}

const canvasElem = document.getElementById('canvas_rounded');
const ctx = canvasElem.getContext('2d');
roundedRect(ctx, 10, 10, 130, 130, 20);

Rysowanie prostokątów w ścieżkach

Już rysowaliśmy prostokąty bezpośrednio na canvasie. To samo możemy zrobić dla ścieżki. Wystarczy skorzystać z metody rect(x, y, width, height). Kiedy ta metoda jest wykonywana, to metoda moveTo jest wywołana automatycznie z parametrami (0,0) (np. przechowuje punkty startowe do jego domyślnej lokalizacji).

Łącząc wszystkie poznane techniki możemy rysować już całkiem ciekawe rzeczy. Piękne dynamiczne wykresy to jedna z możliwości, lub...


const canvasElem = document.getElementById('canvas_6')
const ctx = canvasElem.getContext('2d');

//jajo tułów
ctx.beginPath();
ctx.moveTo(100,10);
ctx.bezierCurveTo(-10,10,-40,290,100,290);
ctx.moveTo(100,10);
ctx.bezierCurveTo(210,10,240,290,100,290);
ctx.stroke();
ctx.moveTo(10,120);
ctx.bezierCurveTo(20,140,100,160,190,120);
ctx.moveTo(14,220);
ctx.bezierCurveTo(20,240,100,260,186,220);

//nogi
ctx.moveTo(45,270);
ctx.lineTo(40,300);
ctx.moveTo(155,270);
ctx.lineTo(160,300);

//klamra
ctx.stroke();
roundedRect(ctx, 85, 225, 30, 30, 5);
roundedRect(ctx, 90, 230, 20, 20, 3);

//lewe oko
ctx.beginPath();
ctx.moveTo(100,110);
ctx.lineTo(10,80);
ctx.moveTo(100,120);
ctx.bezierCurveTo(100,190,10,110,10,80);
ctx.moveTo(100,115);
ctx.lineTo(10,85);
ctx.moveTo(100,115);
ctx.bezierCurveTo(100,180,10,100,15,85);
ctx.moveTo(90,130);
ctx.lineTo(80,125);

//prawe oko
ctx.moveTo(100,110);
ctx.lineTo(190,80);
ctx.moveTo(100,120);
ctx.bezierCurveTo(100,180,190,110,190,80);
ctx.moveTo(100,115);
ctx.lineTo(190,85);
ctx.moveTo(100,115);
ctx.bezierCurveTo(100,170,190,100,185,85);
ctx.moveTo(110,130);
ctx.lineTo(120,125);

//uśmiech
ctx.moveTo(130,150);
ctx.bezierCurveTo(130,160,140,160,140,150);

//ręce
ctx.moveTo(5,150);
ctx.lineTo(0,180);
ctx.moveTo(195,150);
ctx.lineTo(200,180);

//F
ctx.moveTo(90,40);
ctx.lineTo(120,40);
ctx.lineTo(120,50);
ctx.lineTo(100,50);
ctx.lineTo(100,60);
ctx.lineTo(110,60);
ctx.lineTo(110,70);
ctx.lineTo(100,70);
ctx.lineTo(100,80);
ctx.lineTo(90,80);
ctx.closePath();
ctx.stroke();

Kolory, przezroczystość i wygląd linii

Powyższe wiadomości mogły by nam w zasadzie wystarczyć. Istnieje jednak mały powód, dla którego nie wystarczą - nuda. Aby zwiększyć atrakcyjność naszych dzieł, dodamy im trochę koloru i stylu.

Do zmiany koloru obrysu lub wypełnienia służą własności strokeStyle i fillStyle. Aby zmienić kolor, przypisujemy im jego wartość podaną wartością CSS, składową RGB lub nazwą.


ctx.fillStyle = "#6DCF00";
ctx.fillStyle = "rgb(109,207,0)";
ctx.strokeStyle = "green";

const canvasElem = document.getElementById('canvasColor');
const ctx = canvasElem.getContext('2d');

ctx.strokeStyle = "green";
ctx.beginPath();
ctx.moveTo(60,10); //rysowanie zaczynamy od górnego kąta
ctx.lineTo(120,120);
ctx.lineTo(10,120);
ctx.lineTo(60,10);
ctx.stroke();

ctx.fillStyle = "rgb(201,224,54)";
ctx.beginPath();
ctx.moveTo(90,20);
ctx.lineTo(150,130);
ctx.lineTo(10,130);
ctx.closePath();
ctx.fill();

Aby ustawić przezroczystość rysowania, możemy wykorzystać dwa sposoby. Pierwszy z nich to użycie właściwości globalAlpha. Wartość tej własności zostanie zastosowana dla wszystkich następnie rysowanych kształtów na canvasie. Drugi sposób - o wiele przyjemniejszy - to podawanie kolorów w notacji CSS3:


ctx.strokeStyle = "rgba(255,0,0,0.5)"; //ostatni atrybut określa przezroczystość z przedziału 0.0 - 0.1
ctx.fillStyle = "rgba(255,0,0,0.3)";

Wartość przezroczystości zawiera się w przedziale 0.0 - 1.0.


const canvasElem = document.getElementById('canvas_kolor');
const ctx = canvasElem.getContext('2d');

ctx.strokeStyle = "green";
ctx.beginPath();
ctx.moveTo(60,10); //rysowanie zaczynamy od górnego kąta
ctx.lineTo(120,120);
ctx.lineTo(10,120);
ctx.lineTo(60,10);
ctx.stroke();

ctx.fillStyle = "rgba(201,224,54,0.4)";
ctx.beginPath();
ctx.moveTo(90,20);
ctx.lineTo(150,130);
ctx.lineTo(10,130);
ctx.closePath();
ctx.fill();

Tak samo jak w programach graficznych do wypełniania lub obrysowywania kształtów możemy używać gradientów. Zasada tworzenia gradientu jest podobna do tej z programów. Ustalamy położenie skrajnych punktów gradientu, a następnie określamy kolory gradientu.

Do tworzenia obiektów gradientów korzystamy z metod:

  • createLinearGradient(x1,y1,x2,y2) - tworzy obiekt gradientu, który możemy wykorzystać do obrysowani albo wypełnienia rysowanych figur. Gradient liniowy biegnie z punktu x1,y1 do punktu x2,y2
  • createRadialGradient(x1,y1,r1,x2,y2,r2) - ustawia gradient radialny. Atrybuty x1,y1,r1 - określają położenie i promień 1 okręgu, natomiast x2,y2,r2 - drugiego
  • addColorStop(pozycja, kolor) - dodaje nowy kolor do gradientu w pozycji z przedziału 0.0 - 1.0. Możesz dodawać takich punktów do woli.

const canvasElem = document.getElementById('canvasGradientTriangle');
const ctx = canvasElem.getContext('2d');
const gradient = ctx.createLinearGradient(0,0,150,150);

gradient.addColorStop(0,"#F5F8D6");
gradient.addColorStop(0.5,"#CBDE22");
gradient.addColorStop(1,"#51590E");
ctx.fillStyle = gradient;
ctx.beginPath();
ctx.moveTo(60,10); //rysowanie zaczynamy od górnego kąta
ctx.lineTo(120,120);
ctx.lineTo(10,120);
ctx.lineTo(60,10);
ctx.fill();

{
    const canvasElem = document.querySelector('#canvasGradient1');
    const ctx = canvasElem.getContext('2d');
    const grad = ctx.createLinearGradient(0,0,100,50);

    grad.addColorStop(0, '#00ABEB');
    grad.addColorStop(0.5, '#fff');
    grad.addColorStop(1, '#D14F2E');
    ctx.fillStyle = grad;
    ctx.fillRect(10,10,180,80);
}

{
    const canvasElem = document.querySelector('#canvasGradient2');
    const ctx = canvasElem.getContext('2d');
    const grad = ctx.createRadialGradient(50,50,100,150,50,100);

    grad.addColorStop(0, '#00ABEB');
    grad.addColorStop(0.5, '#fff');
    grad.addColorStop(1, '#B3DA4C');
    ctx.fillStyle = grad;
    ctx.fillRect(10,10,180,80);
}

Wygląd linii obrysowania ustawiamy wykorzystując metody:

  • lineWidth = wartość - określa grubość linii

  • lineCap = typ - określa wygląd zakończenia rysowanej linii. Może przyjąć wartość butt, round lub square.

  • lineJoin = typ - określa sposób łączenia 2 linii. Może przyjąć wartość round, bevel lub miter.

  • miterLimit = wartość - określa jak daleko kąt połączenia 2 linii metodą miter może wychodzić.

Zapisywanie i odczytywanie stylu

Na zakończenie poznamy jeszcze 2 bardzo przydatne metody. Metoda save() służy do zapisania aktualnie ustawionych właściwości rysowania (które poznaliśmy powyżej).
Metoda restore() jak sama nazwa wskazuje służy do odczytania zapisanych wcześniej właściwości.
Ale uwaga! Metoda save i restore działają na zasadzie stosu. Save kładzie coś na stos, a restore pobiera OSTATNI odłożony zapisany stan.

Jak to wygląda w praktyce? Przykładowo rysujemy sobie jakiś obrazek. Ustawiamy sobie kilka właściwości rysowania - np. grubość linii czy kolor. Zapisujemy je metodą save(). Dalej sobie rysujemy przy okazji kolejny raz zmieniając właściwości rysowania. Znowu zapisujemy. Jeżeli teraz wykonamy restore(), to pobierzemy ostatnie zapisane właściwości rysowania. Jako że nie mam talentu do tłumaczenia, posłużę się przykładem:


const canvasElem = document.querySelector('#canvas')
const ctx = canvasElem.getContext('2d');

ctx.fillRect(0,0,150,150);
ctx.save(); //Zapisuje domyślny stan

ctx.fillStyle = '#09F'; //Robimy zmiany w ustawieniach
ctx.fillRect(15,15,120,120);

ctx.save(); //Zapisuje aktualny stan
ctx.fillStyle = '#FFF'
ctx.globalAlpha = 0.5;
ctx.fillRect(30,30,90,90);

ctx.restore(); //Przywraca poprzedni stan (zapisany w linijce 9)
ctx.fillRect(45,45,60,60);

ctx.restore(); //Przywraca domyślny stan (zapisany w linijce 4)
ctx.fillRect(60,60,30,30);