Rozdział 5. Rysowanie na kanwie w HTML5 (element canvas)

13 czerwca 2013
1 gwiadka2 gwiazdki3 gwiazdki4 gwiazdki5 gwiazdek

Do rzeczy

W języku HTML5 dostępny jest element canvas o następującej definicji: „zależna od rozdzielczości mapa bitowa stanowiąca kanwę rysunku, której można używać do renderowania na bieżąco wykresów, obrazów w grach i innych rodzajów grafiki”. Kanwa to prostokątny obszar na stronie, w którym można rysować dowolne obrazy za pomocą JavaScriptu.

Obsługa elementu canvas przez przeglądarki
IEFirefoxSafariChromeOperaiPhoneAndroid
7.0+*3.0+3.0+3.0+10.0+1.0+1.0+
* Internet Explorer 7 i 8 wymagają dodatku zewnętrznej biblioteki explorercanvas. Internet Explorer 9 standardowo obsługuje element <canvas>.

Jak wygląda kanwa? Tak naprawdę to w ogóle nie wygląda. Element canvas nie ma treści ani obramowania.

Niewidoczna kanwa

Kod źródłowy tego elementu jest następujący:

<canvas width="300" height="225"></canvas>

Dodamy przerywane obramowanie, aby było widać o czym jest mowa.

Kanwa z obramowaniem

Na jednej stronie może znajdować się wiele elementów canvas. Każdy z nich jest uwzględniony w drzewie DOM i ma swój własny stan. Jeśli każdej kanwie przypisze się identyfikator, to można się do nich odwoływać w taki sam sposób, jak do innych elementów.

Dodamy do przedstawionego przykładu atrybut id:

<canvas id="a" width="300" height="225"></canvas>

Teraz bez trudu można znaleźć ten element w drzewie DOM.

var a_canvas = document.getElementById("a");

Proste kształty

IEFirefoxSafariChromeOperaiPhoneAndroid
7.0+*3.0+3.0+3.0+10.0+1.0+1.0+
* Internet Explorer 7 i 8 wymagają dodatku zewnętrznej biblioteki explorercanvas. Internet Explorer 9 standardowo obsługuje kształty w elemencie canvas.

Każda kanwa jest na początku pusta. Nic ciekawego! Dlatego trzeba coś na niej narysować.

Kliknij, aby narysować coś na tej kanwie

Procedura obsługi zdarzenia onclick wywołuje poniższą funkcję:

function draw_b() {
  var b_canvas = document.getElementById("b");
  var b_context = b_canvas.getContext("2d");
  b_context.fillRect(50, 25, 150, 100);
}

Pierwszy wiersz tej funkcji nie zawiera niczego niezwykłego. Znajduje tylko element canvas w strukturze DOM.

Ale za nim znajduje się ten wyróżniony wiersz.

function draw_b() {
  var b_canvas = document.getElementById("b");
  var b_context = b_canvas.getContext("2d");
  b_context.fillRect(50, 25, 150, 100);
}

Mężczyzna rysujący przed lusterkiem Każda kanwa ma kontekst rysunku, w którym wszystko sie odbywa. Po znalezieniu elementu canvas w drzewie DOM (przy użyciu metody document.getElementById() albo dowolnej innej), wywołuje się jego metodę getContext(). Metodzie tej musimy przekazać łańcuch "2d".

P: Czy istnieje kanwa 3D?
O: Na razie nie. Twórcy przeglądarek eksperymentują z własnymi API trójwymiarowych kanw, ale żadne z nich nie doczekało się standardu. W specyfikacji HTML5 zaznaczono, że „w przyszłości prawdopodobnie zostanie zdefiniowany kontekst 3D”.

Mamy więc już element canvas i jego kontekst rysunku. W kontekście rysunku zdefiniowane są wszystkie metody i własności dotyczące rysowania. Istnieje cała grupa własności i metod do rysowania prostokątów:

  • Wartością własności fillStyle może być kolor w formacie CSS, wzór albo gradient. (Więcej o gradientach dowiesz sie wkrótce). Domyślnie własność fillStyle definiuje jednolity czarny kolor, ale można to zmienić. Każdy kontekst rysunkowy pamięta swoje własności przez cały okres otwarcia strony, chyba że się je jakoś zresetuje.
  • Wywołanie fillRect(x, y, width, height) powoduje narysowanie prostokąta wypełnionego bieżącym kolorem wypełnienia.
  • Własność strokeStyle jest podobna do fillStyle — jej wartością może być kolor w formacie CSS, wzór lub gradient.
  • Wywołanie strokeRect(x, y, width, height) powoduje narysowanie prostokąta z obrysem o bieżącym stylu. Metoda strokeRect nie wypełnia wnętrza figury, tylko rysuje jej krawędzie.
  • Metoda clearRect(x, y, width, height) kasuje ustawienia pikseli w określonym prostokącie.

Pytanie do profesora Kodeckiego

P: Czy można „zresetować” kanwę?
O: Tak. Ustawienie szerokości lub wysokości elementu <canvas> powoduje skasowanie jego zawartości oraz przywrócenie domyślnych wartości wszystkich własności jego kontekstu rysunkowego. Nie trzeba nawet zmieniać szerokości. Wystarczy ustawić ją jeszcze raz na taką samą wartość, np.:

var b_canvas = document.getElementById("b");
b_canvas.width = b_canvas.width;

Wrócimy do poprzedniego przykładu kodu…

Rysowanie prostokąta:

var b_canvas = document.getElementById("b");
var b_context = b_canvas.getContext("2d");
b_context.fillRect(50, 25, 150, 100);

Metoda fillRect() rysuje prostokąt i wypełnia go bieżącym stylem wypełnienia, które jeśli nie zostanie zmienione jest czarne. Wymiary prostokąta określają położenie jego lewego górnego rogu (50, 25), szerokość (150) oraz wysokość (100). Aby to lepiej zrozumieć, przyjrzymy się dokładniej układowi współrzędnych kanwy.

Współrzędne na kanwie

Kanwa jest dwuwymiarowym układem współrzędnych. Punkt o współrzędnych (0, 0) znajduje się w lewym górnym rogu kanwy. Na osi x wartości zwiększają się w prawo. Na osi y wartości zwiększają się w dół.

Schemat układu współrzędnych

Ten układ współrzędnych został narysowany przy użyciu elementu canvas. Rysunek ten zawiera:

  • zestaw poziomych linii w kolorze złamanej bieli,
  • zestaw pionowych linii w kolorze złamanej bieli,
  • dwie czarne poziome linie,
  • dwie małe skośne linie tworzące grot strzałki,
  • dwie czarne pionowe linie,
  • dwie małe skośne linie tworzące drugi grot strzałki,
  • literę x,
  • literę y,
  • tekst „(0, 0)” umiejscowiony w pobliżu lewego górnego rogu,
  • tekst „(500, 375)” umiejscowiony w pobliżu prawego dolnego rogu,
  • dwie kropki: w lewym górnym i prawym dolnym rogu.

Najpierw trzeba zdefiniować sam element canvas. W naszym elemencie zdefiniowaliśmy atrybuty width i height oraz id, dzięki któremu będziemy mogli go później odnaleźć.

<canvas id="c" width="500" height="375"></canvas>

Kolejną potrzebną nam rzeczą jest skrypt, który znajdzie element canvas w drzewie DOM i pozwoli nam uzyskać dostęp do jego kontekstu rysunkowego.

var c_canvas = document.getElementById("c");
var context = c_canvas.getContext("2d");

Teraz możemy rozpocząć rysowanie linii.

Ścieżki

IEFirefoxSafariChromeOperaiPhoneAndroid
7.0+*3.0+3.0+3.0+10.0+1.0+1.0+
* Internet Explorer 7 i 8 wymagają dodatku zewnętrznej biblioteki explorercanvas. Internet Explorer 9 standardowo obsługuje ścieżki elementu <canvas>.
Rysująca mysz

Wyobraź sobie, że rysujesz coś atramentem. Raczej nie zaczniesz od razu tak po prostu rysować, bo możesz popełnić błąd. Najpierw pewnie zrobisz szkic ołówkiem, a gdy osiągniesz zadowalający efekt poprawisz linie atramentem.

Kanwy mają ścieżki. Definiowanie ścieżki jest odpowiednikiem rysowania szkicu ołówkiem. Można narysować co się chce, ale dopóki nie poprawi się linii atramentem nie będzie tego widać na gotowym rysunku.

Aby narysować prostą linię za pomocą ołówka, należy użyć dwóch metod:

  1. moveTo(x, y) — przenosi ołówek w wyznaczone miejsce.
  2. lineTo(x, y) — rysuje linię do określonego punktu.

Im więcej wywołań metod moveTo() i lineTo(), tym większa ścieżka. To są metody „ołówkowe”, tzn. można je wywoływać do woli, ale dopóki nie użyje się jednej z metod „atramentowych” nie będzie widać żadnego efektu.

Najpierw narysujemy siatkę.

Rysowanie pionowych linii

for (var x = 0.5; x < 500; x += 10) {
  context.moveTo(x, 0);
  context.lineTo(x, 375);
}

Rysowanie poziomych linii

for (var y = 0.5; y < 375; y += 10) {
  context.moveTo(0, y);
  context.lineTo(500, y);
}

To były metody „ołówkowe”. Na kanwie nie widać jeszcze żadnego rysunku. Żeby stał się widoczny, musimy użyć metody „atramentowej”.

context.strokeStyle = "#eee";
context.stroke();

stroke() jest jedną z metod „atramentowych”. Pobiera skomplikowaną ścieżkę zdefiniowaną przy użyciu metod moveTo() i lineTo() i tworzy rysunek na kanwie. Metoda strokeStyle służy do ustawiania koloru linii. Oto wynik działania przedstawionego skryptu:

Pytanie do profesora Kodeckiego

P: Dlaczego zmienne x i y mają wartość początkową 0.5? Dlaczego nie 0?
O: Wyobraź sobie, że każdy piksel jest dużym kwadratem. Współrzędne całkowitoliczbowe (0, 1, 2…) są krawędziami tych kwadratów. Jeśli między współrzędnymi całkowitoliczbowymi narysujesz linię o szerokości jeden, najdzie ona na przeciwne strony kwadratu piksela i powstanie linia o szerokości dwóch pikseli. Aby narysować linię o szerokości tylko jednego piksela, należy przesunąć współrzędne o 0.5 prostopadle do linii.

Przykładowo, jeśli narysujesz linię od punktu (1, 0) do (1, 3), przeglądarka narysuje linię okrywającą 0,5 piksela ekranu po każdej stronie x=1. Na ekranie nie da się wyświetlić tylko połowy piksela, więc linia zostanie rozciągnięta na dwa piksele:

Linia z punktu (1,0) do (1,3) będzie miała szerokość dwóch pikseli

Ale jeśli spróbujesz narysować linię od punktu (1.5, 0) do (1.5, 3), przeglądarka narysuje linię pokrywającą po 0,5 piksela po każdej stronie x=1.5, co daje w wyniku linię o szerokości jednego piksela:

Piksele na kanwie (canvas)

Dziękuję Jasonowi Johnsonowi za te schematy.

Teraz narysujemy poziomą strzałkę. Wszystkie proste i krzywe na ścieżce są w tym samym kolorze (albo mają gradient, o czym zaraz będzie mowa). Jako że strzałka ma mieć inny kolor — czarny, nie biały — musimy utworzyć nową ścieżkę.

Nowa ścieżka

context.beginPath();
context.moveTo(0, 40);
context.lineTo(240, 40);
context.moveTo(260, 40);
context.lineTo(500, 40);
context.moveTo(495, 35);
context.lineTo(500, 40);
context.lineTo(495, 45);

Pionowa strzałka jest bardzo podobna. Ponieważ ma taki sam kolor, jak pozioma strzałka, nie musimy dla niej tworzyć nowej ścieżki. Obie strzałki będą należały do jednej ścieżki.

Nie nowa ścieżka

context.moveTo(60, 0);
context.lineTo(60, 153);
context.moveTo(60, 173);
context.lineTo(60, 375);
context.moveTo(65, 370);
context.lineTo(60, 375);
context.lineTo(55, 370);

Napisałem, że strzałki mają być czarne, ale własność strokeStyle ma nadal kolor złamanej bieli. (Ustawienia własności fillStyle i strokeStyle nie są kasowane po utworzeniu nowej ścieżki.) Na razie to nie przeszkadza, bo wykonaliśmy tylko kilka wywołań metod „ołówkowych”. Zanim jednak utrwalimy rysunek atramentem, musimy ustawić własność strokeStyle na czerń. W przeciwnym razie strzałki będą białawe i ledwie będzie je widać! Poniższe wiersze kodu zmieniają kolor na czarny i rysują linie na kanwie:

context.strokeStyle = "#000";
context.stroke();

Oto wynik działania przedstawionego skryptu:

Tekst

IEFirefoxSafariChromeOperaiPhoneAndroid
7.0+*3.0+3.0+3.0+10.0+1.0+1.0+
* Internet Explorer 7 i 8 wymagają dodatku zewnętrznej biblioteki explorercanvas. Internet Explorer 9 standardowo obsługuje tekst na kanwie.
† Mozilla Firefox 3.0 wymaga dodatku.

Oprócz linii na kanwie można też rysować tekst. W odróżnieniu od strony internetowej zawierającej kanwę, na kanwie nie ma czegoś takiego, jak model polowy. To znaczy, że nie można używać żadnej z technik CSS komponowania układów elementów: nie ma elementów pływających, marginesów, dopełnienia ani własności zawijania tekstu. (Może niektórzy pomyślą, że to bardzo dobrze!) Na kanwie ustawia się kilka własności czcionki, a następnie określa punkt, w którym ma zostać narysowany tekst.

W kontekście rysunku dostępne są następujące atrybuty czcionki:

  • font: pozwala ustawić wszystko to, co można zdefiniować przy użyciu własności CSS font. Można ustawić styl pisma, wariant czcionki, grubość tekstu, rozmiar czcionki, wysokość linii oraz rodzinę czcionek.
  • textAlign: służy do określania wyrównania tekstu. Ma podobne działanie do własności CSS text-align. Dostępne wartości to start, end, left, right oraz center.
  • textBaseline: umożliwia określanie miejsca rysowania tekstu w odniesieniu do punktu początkowego. Dostępne wartości to top, hanging, middle, alphabetic, ideographic oraz bottom.

Własność textBaseline jest problematyczna, ponieważ tekst sam w sobie sprawia trudności (zwykle litery łacińskie nie, ale na kanwie można narysować każdy znak Unicode, a Unicode jest skomplikowany). W specyfikacji HTML5 znajduje się objaśnienie różnych linii bazowych pisma:

Górna krawędź prostokąta em mniej więcej pokrywa się z górną krawędzią glifów fontu. Linia bazowa biegnie u nasady niektórych glifów, jak np. आ. Linia środkowa znajduje się pośrodku między górną a dolną krawędzią prostokąta em. Alfabetyczna linia bazowa biegnie u nasady takich znaków, jak Á, ÿ, f oraz Ω. Ideograficzna linia bazowa biegnie u podstawy takich glifów, jak 私 i 達. Natomiast linia dolna prostokąta em pokrywa się z grubsza z dolną końcówką glifów fontu. Górna i dolna krawędź otaczającego pola mogą znajdować się daleko od tych linii bazowych ze względu na to, że niektóre glify mogą wychodzić daleko poza prostokąt em.

Linie pisma

W przypadku prostych alfabetów, jak języka polskiego można bezpiecznie używać tylko ustawień top, middle oraz bottom.

Narysujmy jakiś tekst! Tekst rysowany na kanwie dziedziczy ustawienia rozmiaru i stylu po elemencie <canvas>, ale można je zmienić za pomocą własności font kontekstu rysunkowego.

Zmiana stylu pisma

context.font = "bold 12px sans-serif";
context.fillText("x", 248, 43);
context.fillText("y", 58, 165);

Metoda fillText() rysuje tekst.

context.font = "bold 12px sans-serif";
context.fillText("x", 248, 43);
context.fillText("y", 58, 165);

Pytanie do profesora Kodeckiego

P: Czy można ustawiać rozmiar tekstu na kanwie przy użyciu jednostek względnych?
O: Tak. Podobnie jak wszystkie inne elementy na stronie HTML, element <canvas> ma rozmiar pisma obliczony na podstawie reguł CSS zastosowanych do tej strony. Jeśli własność context.font zostanie ustawiona na wartość względną typu 1.5em albo 150%, przeglądarka pomnoży tę wartość przez obliczony rozmiar pisma elementu <canvas>.

Jeśli chodzi o tekst znajdujący się w lewym górnym rogu, to powiedzmy, że chcemy, aby jego górna krawędź znajdowała się w punkcie y=5. Ale ja jestem leniwy i nie chce mi się mierzyć wysokości tekstu, aby obliczyć jego linię bazową. Zamiast to robić mogę ustawić własność textBaseline na top i przekazać współrzędną lewego górnego rogu pola zawierającego tekst.

context.textBaseline = "top";
context.fillText("( 0 , 0 )", 8, 5);

Teraz zajmiemy się tekstem w prawym dolnym rogu. Powiedzmy, że chcemy, aby dolny prawy róg tekstu znajdował się w punkcie o współrzędnych (492,370) — tylko kilka pikseli od prawego dolnego rogu kanwy — ale nie chcemy mierzyć szerokości ani wysokości tekstu. Możemy ustawić własność textAlign na right i textBaseline na bottom, a następnie wywołać metodę fillText() ze współrzędnymi prawego dolnego rogu pola zawierającego tekstu.

context.textAlign = "right";
context.textBaseline = "bottom";
context.fillText("( 500 , 375 )", 492, 370);

Otrzymujemy taki wynik:

Ups! Zapomniałem o kropkach w rogach. Rysowaniem kół zajmiemy się później. Teraz trochę pooszukujemy i narysujemy prostokąty.

context.fillRect(0, 0, 3, 3);
context.fillRect(497, 372, 3, 3);

To wszystko! Oto finalny efekt naszej pracy:

Gradienty

IEFirefoxSafariChromeOperaiPhoneAndroid
gradienty liniowe7.0+*3.0+3.0+3.0+10.0+1.0+1.0+
gradienty promieniste9.0+3.0+3.0+3.0+10.0+1.0+1.0+
* Internet Explorer 7 i 8 wymagają dodatku zewnętrznej biblioteki explorercanvas. Internet Explorer 9 standardowo obsługuje gradienty w elemencie <canvas>.

W poprzednich częściach tego rozdziału nauczyłeś się rysować prostokąty wypełnione jednolitym kolorem oraz jednokolorowe linie. Ale to nie znaczy, że kolory zawsze muszą być jednolite. Zamiast nich można też tworzyć gradienty. Spójrzmy na przykład.

Kod HTML to zwykły element kanwy.

<canvas id="d" width="300" height="225"></canvas>

Najpierw musimy znaleźć interesujący nas element <canvas> i podpiąć się do jego kontekstu rysunkowego.

var d_canvas = document.getElementById("d");
var context = d_canvas.getContext("2d");

Mając kontekst rysunkowy możemy rozpocząć rysowanie gradientu. Gradient to płynne przejście między dwoma lub większą liczbą kolorów. Kontekst rysunku kanwy obsługuje dwa rodzaje gradientów:

  1. createLinearGradient(x0, y0, x1, y1): rysuje gradient wzdłuż linii łączącej punkty (x0, y0) i (x1, y1).
  2. createRadialGradient(x0, y0, r0, x1, y1, r1): rysuje gradient w kształcie stożka łączący dwa okręgi. Trzy pierwsze parametry definiują okrąg wyznaczający początek gradientu, ze środkiem w punkcie (x0, y0) i o promieniu r0. Trzy ostatnie parametry definiują okrąg wyznaczający koniec gradientu, ze środkiem w punkcie (x1, y1) i o promieniu r1.

Narysujemy gradient liniowy. Gradient może mieć dowolny rozmiar, a w tym przykładzie narysujemy gradient o szerokości 300 pikseli, czyli takiej samej, jak kanwa.

Utworzenie obiektu gradientu

var my_gradient = context.createLinearGradient(0, 0, 300, 0);

Jako że wartości y (drugi i czwarty parametr) mają wartość 0, ten gradient będzie się jednolicie rozkładał od lewej do prawej.

Mając obiekt gradientu można definiować jego kolory. Każdy gradient ma przynajmniej dwa stopnie kolorów. Stopnie te można umieszczać w dowolnych miejscach. Aby dodać stopień koloru, należy określić jego położenie na gradiencie. Położenie na gradiencie można definiować przy użyciu wartości z przedziału od 0 do 1.

Zdefiniujemy gradient przechodzący od czerni do bieli.

my_gradient.addColorStop(0, "black");
my_gradient.addColorStop(1, "white");

Samo zdefiniowanie gradientu nie powoduje narysowania czegokolwiek na kanwie. Jest to tylko obiekt zapisany gdzieś w pamięci. Aby narysować gradient, trzeba ustawić na niego własność fillStyle oraz narysować jakąś figurę, np. prostokąt albo linię.

Własność fillStyle jest gradientem

context.fillStyle = my_gradient;
context.fillRect(0, 0, 300, 225);

Otrzymujemy taki wynik:

Powiedzmy, że chcemy aby gradient biegł z góry na dół. Przy tworzeniu obiektu takiego gradientu należy wartości x (pierwszy i trzeci parametr) pozostawić niezmienne, a wartości y (drugi i czwarty parametr) zmienić w zakresie od 0 do wysokości kanwy.

Wartości x wynoszą 0, wartości y zmieniają się

var my_gradient = context.createLinearGradient(0, 0, 0, 225);
my_gradient.addColorStop(0, "black");
my_gradient.addColorStop(1, "white");
context.fillStyle = my_gradient;
context.fillRect(0, 0, 300, 225);

Otrzymujemy taki wynik:

Można też tworzyć gradienty biegnące po skosie.

Zmieniają się wartości x i y

var my_gradient = context.createLinearGradient(0, 0, 300, 225);
my_gradient.addColorStop(0, "black");
my_gradient.addColorStop(1, "white");
context.fillStyle = my_gradient;
context.fillRect(0, 0, 300, 225);

Otrzymujemy taki wynik:

Obrazy

IEFirefoxSafariChromeOperaiPhoneAndroid
7.0+*3.0+3.0+3.0+10.0+1.0+1.0+
* Internet Explorer 7 i 8 wymagają dodatku zewnętrznej biblioteki explorercanvas. Internet Explorer 9 standardowo obsługuje obrazy w elemencie <canvas>.

Oto rysunek kota:

Element img

Śpiący kot

A to ten sam kot, tylko narysowany na kanwie:

Element canvas

Kontekst rysunkowy kanwy ma metodę drawImage() służącą do rysowania obrazów na kanwie. Metoda ta pobiera trzy, pięć lub dziewięć argumentów.

  • Metoda drawImage(image, dx, dy) pobiera obraz i rysuje go na kanwie. Współrzędne (dx, dy) określają położenie lewego górnego rogu tego rysunku. Gdyby podano współrzędne (0, 0), to początek obrazu znajdowałby się w lewym górnym rogu kanwy.
  • Metoda drawImage(image, dx, dy, dw, dh) pobiera obraz, skaluje go do szerokości dw i wysokości dh a następnie rysuje go na kanwie zaczynając w punkcie o współrzędnych (dx, dy).
  • Metoda drawImage(image, sx, sy, sw, sh, dx, dy, dw, dh) pobiera obraz, wycina z niego prostokąt (sx, sy, sw, sh), skaluje go do rozmiarów (dw, dh), a następnie rysuje go na kanwie zaczynając w punkcie o współrzędnych (dx, dy).

W specyfikacji HTML5 znajduje się objaśnienie parametrów metody drawImage():

Prostokąt źródłowy to prostokąt [w obrębie obrazu źródłowego], którego rogi wyznaczają punkty (sx, sy), (sx+sw, sy), (sx+sw, sy+sh) oraz (sx, sy+sh).

Prostokąt docelowy to prostokąt [w obrębie kanwy], którego rogi wyznaczają punkty (dx, dy), (dx+dw, dy), (dx+dw, dy+dh) oraz (dx, dy+dh).

ykres przedstawiający parametry metody drawImage

Aby narysować obraz na kanwie, trzeba mieć jakiś obraz. Może to być istniejący już element <img>, ale można też utworzyć obiekt Image() przy użyciu JavaScript. W obu przypadkach rysowanie obrazu na kanwie można rozpocząć dopiero po jego całkowitym wczytaniu.

Istniejący element <img> można bezpiecznie narysować na kanwie podczas zdarzenia window.onload.

Użycie elementu img

<img id="cat" src="images/cat.png" alt="sleeping cat" width="177" height="113">
<canvas id="e" width="177" height="113"></canvas>
<script>
window.onload = function() {
  var canvas = document.getElementById("e");
  var context = canvas.getContext("2d");
  var cat = document.getElementById("cat");
  context.drawImage(cat, 0, 0);
};
</script>

Jeśli tworzony jest obiekt obrazu przy użyciu JavaScriptu, można bezpiecznie go narysować na kanwie podczas zdarzenia Image.onload.

Użycie obiektu Image()

<canvas id="e" width="177" height="113"></canvas>
<script>
  var canvas = document.getElementById("e");
  var context = canvas.getContext("2d");
  var cat = new Image();
  cat.src = "images/cat.png";
  cat.onload = function() {
    context.drawImage(cat, 0, 0);
  };
</script>

Trzeci i czwarty parametr metody drawImage() służą do skalowania obrazu. To jest ten sam obraz po zmniejszony o połowę i narysowany kilka razy w różnych miejscach na kanwie.

Poniżej znajduje się kod źródłowy tego skryptu:

cat.onload = function() {
  for (var x = 0, y = 0;
       x < 500 && y < 375;
       x += 50, y += 37) {
    context.drawImage(cat, x, y, 88, 56);
  }
};

W tym momencie nasuwa się ciekawe pytanie: po co w ogóle mielibyśmy rysować jakiekolwiek obrazy na kanwie? Jakie korzyści w porównaniu z użyciem elementu <img> i reguł CSS mamy ze stosowania tej bardziej skomplikowanej techniki? Nawet efekt „multikota” można by było uzyskać przy użyciu 10 nakładających się na siebie elementów <img>.

Najkrócej mówiąc powód jest taki sam, jak ten dla którego rysujemy tekst na kanwie. Układ współrzędnych kanwy zawiera tekst, linie i figury geometryczne. Tekst jest po prostu tylko częścią większego dzieła. Na bardziej złożonym schemacie można by było użyć metody drawImage() do narysowania ikon, sprite’ów i innych grafik.

Kanwa a przeglądarka Internet Explorer

Internet Explorer do wersji 8.0 włącznie nie obsługuje API kanwy. (Natomiast IE 9 obsługuje je już w pełni.) Starsze wersje Internet Explorera obsługują natomiast technologię Microsoftu o nazwie VML, za pomocą której można zrobić wiele tych samych rzeczy, co przy użyciu elementu <canvas>. Dlatego powstał skrypt excanvas.js.

Skrypt Explorercanvas (excanvas.js) to otwarta dostępna na licencji Apache biblioteka JavaScript implementująca API kanwy w Internet Explorerze. Aby z niej skorzystać, należy na początku strony umieścić poniższy element <script>.

<!DOCTYPE html>
<html>
<head>
  <meta charset="utf-8">
  <title>Dive Into HTML5</title>
  <!--[if lt IE 9]>
    <script src="excanvas.js"></script>
<![endif]-->
</head>
<body>
  ...
</body>
</html>

Części <!--[if lt IE 9]> i <![endif]--> to komentarze warunkowe. Internet Explorer interpretuje je jak instrukcję if: „jeśli bieżąca przeglądarka jest Internet Explorerem w wersji starszej od 9, to wykonaj ten blok kodu”. Dla wszystkich innych przeglądarek cały ten blok jest komentarzem HTML. W efekcie Internet Explorer 7 i 8 pobiorą skrypt excanvas.js i wykonają go, a inne przeglądarki zignorują ten kod (nie pobiorą skryptu i go nie wykonają). Dzięki temu w przeglądarkach obsługujących API kanwy strona będzie się szybciej wczytywała.

Dołączeniu skryptu excanvas.js w elemencie <head> strony to wszystko, co trzeba zrobić, aby namówić Internet Explorera do współpracy. Potem wystarczy dodać elementy <canvas> normalnie w kodzie HTML albo utworzyć je dynamicznie za pomocą JavaScriptu. Aby rysować figury geometryczne tekst i wzory na elemencie <canvas>, postępuj zgodnie ze wskazówkami opisanymi w tym rozdziale.

No… może niezupełnie. Jest kilka trudności:

  1. Gradienty mogą być tylko liniowe. Gradienty promieniste nie są obsługiwane.
  2. Wzory muszą być zwielokrotniane w obu kierunkach.
  3. Wycinki nie są obsługiwane.
  4. Niejednolite skalowanie powoduje niepoprawne skalowanie linii.
  5. Jest powolne. Chociaż to nikogo nie powinno dziwić, bo wszyscy wiedzą, że parser JavaScriptu w Internet Explorerze działa wolniej niż w innych przeglądarkach. Gdy zaczniesz rysować skomplikowane kształty przy użyciu biblioteki JavaScript zamieniającej instrukcje na polecenia całkiem innej technologii, coś w końcu musi się popsuć. Obniżki wydajności nie zauważysz w prostych rysunkach, jak kilka kresek i przekształcenie obrazu, ale od razu je dostrzeżesz, gdy zaczniesz tworzyć animacje i robić inne szalone rzeczy.

Z bibliotekę excanvas.js wiąże się jeszcze jeden problem, na który natknąłem się tworząc przykłady do tego rozdziału. ExplorerCanvas inicjuje własną atrapę interfejsu kanwy na każdej stronie HTML, do której dołączony jest skrypt excanvas.js. Ale to nie znaczy, że Internet Explorer może z niego od razu korzystać. W niektórych przypadkach może się zdarzyć, że ta atrapa interfejsu kanwy będzie prawie, ale jednak nie całkiem, gotowa do użytku. Najłatwiej rozpoznać to po tym, że Internet Explorer zgłasza błąd „Obiekt nie obsługuje tej właściwości lub metody”, gry próbujemy cokolwiek zrobić z elementem <canvas>, np. utworzyć uchwyt do jego kontekstu rysunkowego.

Najłatwiej problem ten jest rozwiązać czekając z operowaniem na kanwie do zdarzenia onload. To może trochę potrwać — jego wystąpienie opóźniają np. znajdujące się na stronie obrazy i filmy wideo — ale w ten sposób damy narzędziu ExplorerCanvas czas na wykonanie swojej pracy.

Kompletny realny przykład — gra Halma

Halma to bardzo stara gra planszowa. Istnieje wiele jej odmian. Ten przykład dotyczy wersji dla jednego gracza z planszą o wymiarach 9 × 9 pól. Na początku gry pionki są ustawione w kwadrat 3 × 3 w lewym dolnym rogu planszy. Celem gracza jest przeniesienie tego kwadratu do prawgo górnego rogu przy użyciu jak najmniejszej liczby ruchów.

W grze Halma dozwolone są dwa rodzaje ruchu:

  • przesunięcie pionka w dowolnym kierunku na sąsiednie puste pole. Puste pole to takie, na którym aktualnie nie znajduje się żaden pionek. Sąsiednie pole to pole znajdujące się na lewo, prawo, nad, pod lub po skosie względem aktualnego pola. (Nie ma możliwości wychodzenia poza krawędź planszy, tzn. jeśli pionek znajduje się przy lewej krawędzi, to nie można go przesunąć w lewo, a jeśli pionek znajduje się przy dolnej krawędzi, to nie można go przesunąć w dół).
  • Pionki mogą też przeskakiwać inne pionki i można to robić seriami. To znaczy, że można przeskoczyć jeden sąsiedni pionek, a po nim następny i liczy się to jako jeden ruch. Taki seryjny skok liczy się jako jeden ruch, bez względu na liczbę przeskoczonych w nim pionków. (Jako że celem gry jest przeniesienie pionków na drugą stronę planszy przy użyciu jak najmniejszej liczby ruchów, najlepszą strategią jest konstruowanie długich łańcuchów zajmujących co drugie pole pionków, aby potem je przeskoczyć innymi pionkami).

Oto plansza gry. Możesz w nią zagrać na osobnej stronie, jeśli chcesz przy okazji pobadać ją narzędziami dla webmasterów.

Plansza gry Halma

Jak to działa? Cieszę się, że pytasz. Nie będę tu pokazywał całego kodu, bo można go obejrzeć w pliku halma.js. Skoncentruję się tylko na części kodu dotyczącej rysowania na kanwie i reagowania na kliknięcia.

Gra jest inicjowana podczas ładowania strony poprzez ustawienie wymiarów elementu <canvas> i zapisanie referencji do jego kontekstu rysunkowego.

gCanvasElement.width = kPixelWidth;
gCanvasElement.height = kPixelHeight;
gDrawingContext = gCanvasElement.getContext("2d");

Potem robione jest coś, o czym jescze nie pisałem: do elementu <canvas> dodawana jest procedura nasłuchu zdarzeń kliknięcia myszą.

gCanvasElement.addEventListener("click", halmaOnClick, false);

Funkcja halmaOnClick() jest wywoływana, gdy użytkownik kliknie gdziekolwiek w obrębie kanwy. Jej argumentem jest obiekt MouseEvent zawierający informacje o miejscu, w którym nastąpiło kliknięcie.

function halmaOnClick(e) {
    var cell = getCursorPosition(e);

    // Reszta kodu to logika gry HTML5
    for (var i = 0; i < gNumPieces; i++) {
	if ((gPieces[i].row == cell.row) &&
	    (gPieces[i].column == cell.column)) {
	    clickOnPiece(i);
	    return;
	}
    }
    clickOnEmptyCell(cell);
}

Kolejną czynnością jest pobranie obiektu MouseEvent i obliczenie, który kwadrat na planszy został kliknięty. Jako że plansza zajmuje cały obszar kanwy, każde kliknięcie jest gdzieś na niej. Musimy tylko dowiedzieć się gdzie. Jest to trudne ze względu na to, że zdarzenia myszy w każdej przeglądarce są zaimplementowane nieco inaczej.

function getCursorPosition(e) {
    var x;
    var y;
    if (e.pageX != undefined && e.pageY != undefined) {
	x = e.pageX;
	y = e.pageY;
    }
    else {
	x = e.clientX + document.body.scrollLeft +
            document.documentElement.scrollLeft;
	y = e.clientY + document.body.scrollTop +
            document.documentElement.scrollTop;
    }

Teraz mamy współrzędne x i y względne w odniesieniu do dokumentu (czyli całej strony HTML). Na razie niewiele mamy z nich pożytku. Potrzebujemy współrzędnych względnych w odniesieniu do kanwy.

    x -= gCanvasElement.offsetLeft;
    y -= gCanvasElement.offsetTop;

Teraz mamy już współrzędne x i y względne w odniesieniu do kanwy. To znaczy, jeśli w tym momencie wartości x i y wynoszą 0, wiemy, że użytkownik kliknął piksel znajdujący się w lewym górnym rogu kanwy.

Dzięki tym informacjom możemy obliczyć, który kwadrat planszy został kliknięty i odpowiednio na to zareagować.

    var cell = new Cell(Math.floor(y/kPieceHeight),
                        Math.floor(x/kPieceWidth));
    return cell;
}

Uff! Zdarzenia myszy to rzecz dla prawdziwych twardzieli. Ale teraz już tego samego kodu możesz użyć we wszystkich swoich aplikacjach. Zapamiętaj: kliknięcie myszą → współrzędne dokumentu → współrzędne kanwy → logika aplikacji.

Teraz przyjrzymy się głównej procedurze rysującej. Jako że grafika jest w tym przypadku bardzo prosta, postanowiłem, że za każdym razem gdy coś się mzienia na planszy rysować ją od nowa w całości. Nie jest to konieczne. Kontekst rysunkowy kanwy zachowa wszystko, co zostały w nim narysowane, nawet gdy użytkownik przewinie stronę albo przejdzie na inną kartę w przeglądarce, a potem wróci do gry. Jeśli będziesz przy użyciu kanwy tworzyć aplikację o bardziej skomplikowanej grafice (np. grę arcade), możesz zoptymalizować jej działanie odświeżając tylko te obszary, w których coś się zmieniło. Ale to nie jest tematem tego podręcznika.

gDrawingContext.clearRect(0, 0, kPixelWidth, kPixelHeight);

Procedura rysująca planszę powinna wyglądać znajomo. Jest podobna do układu współrzędnych kanwy, który narysowaliśmy wcześniej.

gDrawingContext.beginPath();

/* pionowe linie */
for (var x = 0; x <= kPixelWidth; x += kPieceWidth) {
    gDrawingContext.moveTo(0.5 + x, 0);
    gDrawingContext.lineTo(0.5 + x, kPixelHeight);
}

/* poziome linie */
for (var y = 0; y <= kPixelHeight; y += kPieceHeight) {
    gDrawingContext.moveTo(0, 0.5 + y);
    gDrawingContext.lineTo(kPixelWidth, 0.5 +  y);
}

/* Rysowanie! */
gDrawingContext.strokeStyle = "#ccc";
gDrawingContext.stroke();

Najlepsza zabawa jest przy rysowaniu pionków. W tej grze pionki są okrągłe, a my jeszcze nie wiemy, jak rysować koła. Ponadto, gdy użytkownik kliknie pionek z zamiarem przesunięcia go na inne miejsce, tło tego pionka powinno się zabarwić. W poniższym kodzie argument p reprezentuje pionek, który ma własności row i column określające jego bieżące położenie na planszy. Przy użyciu kilku stałych gry zamieniamy współrzdne (column, row) na współrzędne (x, y) kanwy, potem rysujemy koło, a później (jeśli pionek jest kliknięty) wypełniamy te koło jednolitym kolorem.

function drawPiece(p, selected) {
    var column = p.column;
    var row = p.row;
    var x = (column * kPieceWidth) + (kPieceWidth/2);
    var y = (row * kPieceHeight) + (kPieceHeight/2);
    var radius = (kPieceWidth/2) - (kPieceWidth/10);

To koniec logiki gry HTML5. Mamy już współrzędne (x, y) na kanwie środka koła, które chcemy narysować. W API kanwy nie ma metody o nazwie circle(), ale jest metoda arc(). Służy ona do tworzenia łuków, a czym jest okrąg, jak nie łukiem z połączonymi końcami? Pamiętasz podstawy geometrii? Metoda arc() jako argumenty pobiera współrzędne środka (x, y), promień, kąt początkowy i końcowy (w radianach) oraz kierunkowskaz (false oznacza w prawo, true oznacza w lewo). Do wykonywania obliczeń w radianach można wykorzystać moduł Math języka JavaScript.

gDrawingContext.beginPath();
gDrawingContext.arc(x, y, radius, 0, Math.PI * 2, false);
gDrawingContext.closePath();

Chwileczkę! Nic jeszcze nie zostało narysowane. Metoda arc() , podobnie jak moveTo() i lineTo, jest metodą „ołówkową”. Aby narysować koło, musimy ustawić własność strokeStyle i wywołać metodę stroke().

gDrawingContext.strokeStyle = "#000";
gDrawingContext.stroke();

A co z sytuacją, gdy pionek jest kliknięty? Można wykorzystać tę samą ścieżkę, co do narysowania obrysu pionka i wypełnić tworzony przez nią okrąg kolorem.

if (selected) {
    gDrawingContext.fillStyle = "#000";
    gDrawingContext.fill();
}

I to by było na tyle. Reszta programu to logika gry — rozróżnianie dozwolonych i niedozwolonych ruchów, liczenie liczby ruchów, sprawdzanie czy gry dobiegła końca itp. Użyliśmy elementu <canvas>, na którym narysowaliśmy 9 okręgów i kilka prostych kresek oraz zdefiniowaliśmy jedną procedurę obsługi zdarzenia onclick, aby utworzyć ciekawą planszową grę logiczną. Hurrra!

Lektura uzupełniająca

Autor: Mark Pilgrim

Źródło: http://diveintohtml5.info/canvas.html

Tłumaczenie: Łukasz Piwko

Treść tej strony dostępna jest na zasadach licencji CC BY 3.0

Dyskusja

Twój adres email nie zostanie opublikowany. Pola, których wypełnienie jest wymagane, są oznaczone symbolem *