[MCSD 70-480] Rysowanie – canvas i SVG

Rysowanie na stronach internetowych jest często bardzo przydatne. Możemy chcieć wyświetlić wykresy, które będą renderowane na podstawie danych ze strony, albo tworzyć dodatkowe animacje, które mają uatrakcyjnić wygląd strony. Oczywistym jest, że dla większości problemów powstały już odpowiednie biblioteki, które robią większość rzeczy za nas. Warto jednak wiedzieć co może siedzieć w środku takiej biblioteki i w jaki sposób dzieje się cała magia.

Grafika na <canvas> i SVG

W HTMLu 5 mamy przynajmniej dwie możliwości na rysowanie na naszej stronie. Dzisiaj omówimy sobie znacznik <canvas> <svg>. Oczywiście zaczniemy od tego pierwszego.

Canvas właściwie jako część HTMLa tworzy nam jedynie kontener do pracy nad grafiką. Inicjalizujemy go w taki sposób:

<canvas id="drawingSurface" width="600" height="400">
  Your browser does not support HTML5.
</canvas>

Ten kod faktycznie przygotuje na kontener, ale będzie on właściwie niewidoczny, bo ma białe tło 🙂 Dlatego dodajmy sobie do niego obramowanie

canvas {
  border: 1px solid black;
}

Teraz już wyświetlając zobaczymy prostokąt o wielkości 600×400. I to tyle jeśli chodzi o HTMLa. W przypadku canvasu reszta magii dzieje się w JavaScriptcie. Dlatego na dobry początek potrzebujemy pobrać sobie kontekst naszego kontenera i wskazać czy będziemy rysować w dwóch czy trzech wymiarach. Dzisiaj zajmiemy się tylko dwoma wymiarami.

window.onload = function () {
  var drawingSurface = document.getElementById("drawingSurface");
  var ctxt = drawingSurface.getContext("2d");
}

Rysowanie linii

Mając kontekst możemy zacząć coś tworzyć. Zacznijmy od prostych linii. Żeby narysować linię, albo kilka linii potrzebujemy czterech metod:

Metoda Opis
 beginPath  Rozpoczyna, albo resetuje rysowanie nowej ścieżki
moveTo  Przesuwa kontekst o określoną liczbę pikseli
lineTo  Rysuje linię prostą z miejsca w którym jest kontekst do miejsca który wskażemy
stroke  Tworzy faktyczną linię na podstawie zdefiniowanej ścieżki

Metody całkiem proste. Założenie jest takie. Nasz kontekst (w domyśle można przyjąć, że kontekst to miejsce na kartce w którym trzymamy długopis) zawsze na początku znajduje się w lewym górnym rogu naszego kontenera. Kod:

ctxt.beginPath();
ctxt.moveTo(10, 10);
ctxt.lineTo(225, 350);
ctxt.lineTo(300, 10);
ctxt.lineTo(400, 350);
ctxt.stroke();

Mówi nam tak.

  • zaczynamy rysować
  • przesuń rękę o 10 pikseli w prawo i 10 w dół, ale nic nie rysuj
  • narysuj linię, która skończy się w punkcie oddalonym o 225 pikseli w prawo i 350 w dół od początku
  • itd.
  • ctxt.stroke(), czyli wyrenderuj to co chciałem żebyś zrobił.

I tak powstaje nam poniższy obrazek.

Proste right?

Rysowanie krzywych

No to ciut utrudnimy i zaokrąglimy. Dalej możemy zacząć tworzyć krzywe. Mamy do wyboru następujące:

Metoda Opis
 arc  Tworzy prosty łuk na podstawie punktów początkowych, końcowych i promienia
 quadradicCurveTo  Tworzy prostą krzywą kwadratową
 bezierCurveTo  Tworzy krzywą Beziera

Arc

Standardowy łuk tworzony po okręgu. Wskazujemy współrzędne środka okręgu, jego promień, punkty początku i końca (wyrażony w radianach) i na końcu czy rysowanie ma być zgodne czy przeciwne z ruchem wskazówek zegara.

Jak pierwszy raz zobaczyłem ostatni parametr to przez moment pomyślałem, że jest bez sensu, bo co za różnica w którą stronę będziemy rysować skoro wynik będzie taki sam. Potem jednak przypomniałem sobie, że mamy nasz kontekst, czyli miejsce w którym skończymy rysować łuk jest miejscem z którego możemy rysować kolejną rzecz na przykład linię prostą.

Parametr Opis
 X, Y  Współrzędne środka okręgu
 radius  Promień okręgu
 startAngle, endAngle  Parametry wskazujące na początek i koniec rysowanego łuku
 counterclockwise  Parametr określający czy łuk powinien być rysowany zgodnie czy przeciwnie do ruchu wskazówek zegara

Poniżej kilka przykładów prostych łuków. W tej chwili najważniejsza jest metoda arc oczywiście. Reszta jest dodana po to, żeby można się było połapać w tym, który łuk jest który na naszym canvasie. Wszystko omówię już za moment 🙂

ctxt.beginPath();
ctxt.arc(150,100,75,0,2 * Math.PI, false);
ctxt.lineWidth = 25;
ctxt.strokeStyle = '#0f0';
ctxt.stroke();
ctxt.beginPath();
ctxt.arc(450, 100, 75, 1.5 * Math.PI, 2 * Math.PI, false);
ctxt.lineWidth = 25;
ctxt.strokeStyle = 'blue';
ctxt.stroke();
ctxt.beginPath();
ctxt.arc(150, 300, 75, 1 * Math.PI, 1.5 * Math.PI, false);
ctxt.lineWidth = 25;
ctxt.strokeStyle = '#0ff';
ctxt.stroke();
ctxt.beginPath();
ctxt.arc(450, 300, 75, .5 * Math.PI, 1 * Math.PI, false);
ctxt.lineWidth = 25;
ctxt.strokeStyle = '#f00';
ctxt.stroke();

QuadraticCurveTo

Kolejna krzywa to krzywa kwadratowa. Sytuacja w tym przypadku jest całkiem ciekawa. Myślałem, że najłatwiej będzie po prostu jako parametry w jakiś sposób podać równanie kwadratowe na podstawie którego zostanie wyrysowana krzywa. Jednak w przypadku quadraticCurveTo podajemy punkt kontrolny w stosunku do którego krzywa będzie rozciągana. Szczerze? Nie do końca to czuję więc musicie sami się pobawić i zobaczyć o co dokładnie chodzi 🙂

Parametr Opis
 controlX, controlY  Parametry wskazujące na punkt (odległy o X i Y pikseli od lewego górnego rogu) w stosunku do którego linia powinna być rozciągnięta
 endX, endY  Końcowy punkt krzywej
ctxt.beginPath();
ctxt.moveTo(10,380);
ctxt.quadraticCurveTo(300,-250,580,380);
ctxt.lineWidth = 25;
ctxt.strokeStyle = '#f00';
ctxt.stroke();

BezierCurveTo

Krzywa Beziera jest ostatnią z krzywych które możemy stworzyć. Na początek cóż to takiego jest? Sam musiałem sprawdzić mimo, że gdzieś tam się kiedyś pojawiała na studiach czy w photoshopie. Otóż krzywa Beziera to tak zwana krzywa parametryczna, czyli każda współrzędna punktu krzywej jest pewną funkcją liczby rzeczywistej będącej wspomnianym parametrem. Aby określić krzywą na płaszczyźnie potrzebujemy dwie funkcje. I tutaj podobnie jak w poprzednim przykładzie nie mamy funkcji tylko punkty kontrolne w stosunku do których rozciągana jest krzywa. Z tym, że tutaj mamy dwa takie punkty więc sprawa jest bardziej skomplikowana niż w przypadku krzywej kwadratowej.

Parametr Opis
 controlX, controlY  Pierwszy punkt w stosunku do którego linia powinna być rozciągnięta
 Control2X, Control2Y  Drugi punkt w stosunku do którego linia powinna być rozciągnięta
 endX, endY  Współrzędne punktu końcowego
ctxt.beginPath();
ctxt.moveTo(125, 20);
ctxt.bezierCurveTo(0, 200, 300, 300, 50, 400);
ctxt.lineWidth = 5;
ctxt.strokeStyle = '#f00';
ctxt.stroke();

Metoda path

We wszystkich przykładach, metodach, liniach na początku używana była metoda beginPath(). Path, czyli ścieżka określa nam jakby jedno pociągnięcie kredki. W danej ścieżce możemy używać różnych metod do rysowania różnych kształtów i nadawania im różnych wypełnień, czy kolorów linii. W przypadku rysowania krzywych arc przy każdym łuku używałem nowej ścieżki. Było to spowodowane między innymi tym, że chciałem, aby każdy łuk miał inny kolor przez co musiałem „zmienić kredkę”, czyli ścieżkę 🙂 Ścieżkę możemy zakończyć na 3 sposoby. Albo wywołać kolejny raz beginPath(), co zacznie nową ścieżkę, albo stroke(), co oprócz zakończenia ścieżki wyrysuje ją nam na canvasie, albo closePath(), co chyba jest oczywiste.

ctxt.beginPath();
ctxt.arc(300, 200, 75, 1.75 * Math.PI, 1.25 * Math.PI, false);
ctxt.lineTo(150, 125);
ctxt.quadraticCurveTo(300, 0, 450, 125);
ctxt.lineTo(353, 144);
ctxt.strokeStyle = "blue";
ctxt.lineCap = "round";
ctxt.lineWidth = 10;
ctxt.stroke();

Prostokąty

Ostatnim kształtem, który może być użyteczny i często spotykany jest prostokąt. Sprawa jest banalnie prosta. Jako parametry metody rect() przekazujemy współrzędne lewego górnego wierzchołka, a następnie szerokość i wysokość prostokąta.

Parametr Opis
 x,y  Punkt wskazujący lewy górny wierzchołek prostokąta
 width  Szerokość prostokąta
 height  Wysokość prostokąta
ctxt.beginPath();
ctxt.rect(300, 200, 150, 75);
ctxt.stroke();

Wypełnienie

W poprzednich przykładach udawało mi się pokolorować linie w najróżniejszy sposób. Tak samo jak rysowanie samych kształtów tak i ich wypełnienie nie powinno być specjalnie problematyczne.

Aby zmienić kolor linii używamy właściwości strokeStyle na kontekście naszej ścieżki. Możemy również zaokrąglić końcówki naszych linii przez ustawienie lineCap na wartość „round. Grubość linii zmieniamy przez ustawienie lineWidth. Proste prawda?

Sprawa się trochę komplikuje w przypadku wypełnienia naszego zamkniętego kształtu. Możemy wypełnić po prostu jednolitym kolorem i to jest banalne. Używamy fillStyle i już. Popatrzcie na przykład poniżej.

ctxt.beginPath();
ctxt.arc(300, 200, 75, 1.75 * Math.PI, 1.25 * Math.PI, false);
ctxt.lineTo(150, 125);
ctxt.quadraticCurveTo(300, 0, 450, 125);
ctxt.lineTo(353, 144);
ctxt.strokeStyle = "blue";
ctxt.lineCap = "round";
ctxt.lineWidth = 10;
ctxt.fillStyle = "Green";
ctxt.fill();
ctxt.stroke();

Oprócz jednolitego koloru możemy stworzyć też gradient. Wtedy również używamy właściwości fillStyle, jednak przypisujemy jej obiekt CanvasGradient tworzony tak jak poniżej na przykładzie.

var ctxt = drawingSurface.getContext("2d");
ctxt.lineWidth = 3;
ctxt.rect(150, 150, 200, 125);
var gradient = ctxt.createLinearGradient(150, 150, 200, 125);
gradient.addColorStop(0, "Black");
gradient.addColorStop(0.5, "Gray");
gradient.addColorStop(1,"White");
ctxt.fillStyle = gradient;
ctxt.fill();
ctxt.stroke();

Żeby stworzyć ten obiekt najpierw wywołujemy metodę createLinearGradient() i jako parametry przekazujemy te same wartości co w przypadku prostokąta. Następnie za pomocą addColorStop dodajemy punkty zatrzymania koloru. Pomiędzy tymi punktami kolor będzie płynnie przechodził z jednego do drugiego.

 

Ostatnią możliwością wypełnienia jest wypełnienie obrazkiem. Znowu musimy ustawić fillStyle, jednak ustawiamy go po załadowaniu obrazka i poprzez createPattern w którym określamy, czy obrazek powinien się dopasować, rozciągnąć, czy być powtarzanym w naszym kształcie.

var ctxt = drawingSurface.getContext("2d");
ctxt.lineWidth = 3;
ctxt.rect(150, 150, 200, 125);
var img = new Image();
img.src = "texture.png";
img.onload = function () {
  var pat = ctxt.createPattern(img, "repeat");
  ctxt.fillStyle = pat;
  ctxt.fill();
  ctxt.stroke();
}

Obrazki

Ostatnią kwestią do poruszenia na canvasie są obrazki. Zamiast coś rysować od początku możemy wczytać sobie już gotowy obrazek na nasz canvas i potem rysować sobie na nim. Żeby to zrobić musimy załadować obrazek i po jego załadowaniu narysować go za pomocą metody drawImage() w odpowiednim miejscu naszego canvasa.

var drawingSurface = document.getElementById("drawingSurface");
var ctxt = drawingSurface.getContext("2d");
var img = new Image();
img.src = "orange.jpg";
img.onload = function () {
ctxt.drawImage(img, 0, 0);
ctxt.stroke();
}

SVG

Drugim po canvasie sposobie na rysowanie na stronie internetowej jest SVG. Scalable Vector Graphics (SVG) jest bardzo ciekawym bytem, ponieważ w związku z tym, że jest grafiką wektorową to nie traci na jakości w przypadku powiększania. Użytkownik może sobie taką grafikę dowolnie powiększać, albo zmniejszać a rezultat zawsze będzie tej samej jakości. To wiąże się jednak z pewnym minusem. Aby narysować kształt za pomocą SVG musimy użyć więcej zasobów naszego komputera. Także jeżeli grafiki na stronie jest duużo to jednak lepiej wspomagać się canvasem. Oba sposoby rysowania mają podobne możliwości, ale oczywiście w przypadku SVG różnicą jest przede wszystkim składnia.

Poniżej przedstawiam kod za pomocą, którego można stworzyć proste kształty widoczne na obrazku na samym dole.

<svg>
  <polygon points="10,15 30,35 10,85 100,85, 70,35,100,15" fill="purple"/>
  <polyline points="10,150 30,170 50,132 62,196 78,165 96,170" style="stroke:orange; fill:none; stroke-width:5;"/>
  <line x1="150" y1="100" x2="150" y2="150" style="stroke:blue;stroke-width:3"/>
  <ellipse cx="250" cy="150" rx="30" ry="55" fill="green"/>
  <text x="10" y="10" style="stroke: black;stroke-width:1;">
  Examples of SVG Shapes and Text</text>
</svg>

Related posts