Canvas
Wraz z HTML 5 doszło nam kilkanaście nowych elementów. Jednym z nich jest Canvas - czyli po naszemu płótno. Element ten bardzo przypomina obszar roboczy znany praktycznie z każdego programu graficznego. Różnica jest taka, że tam rysujemy za pomocą dostępnych narzędzi (np. piórka), a tutaj - za pomocą JavaScriptu.
<canvas width="400" height="200">
</canvas>Zanim zaczniemy
Kilka słów na początek. Element taki zachowuje się zupełnie jak img na stronie. Możemy na niego wgrywać za pomocą JavaScriptu obrazki, rysować po nim różne kształty, pisać po nim, odczytywać z niego informacje o pikselach itp. Dodatkowo możemy to wszystko robić dynamicznie, dzięki czemu idealnie nadaje się do tworzenia różnych animacji czy np. gier. A na końcu gdy klikniemy na niego prawym guzikiem, możemy wybrać opcję "Zapisz grafikę jako..." - zupełnie jak przy innych grafikach na stronie.
Wyobraź sobie, że robisz dynamiczną platformówkę. W takiej grze powinno się zawrzeć wiele poruszających się i zależnych od siebie elementów. Gdybyśmy to robili za pomocą CSS i diwów (dać się da), to trzeba by ogarnąć jakoś problem ze skalowaniem (RWD), a i w naszym HTML zapewne byśmy mieli niezły śmietnik, który trzeba by mieć jakoś pod kontrolą. Dzięki canvasowi unikamy takich nieprzyjemnych sytuacji.
Z drugiej strony używanie canvasa nie należy do najprzyjemniejszych rzeczy na świecie. Zrobienie nawet najmniejszej głupotki - ot narysowania kilku kwadratów - wiąże się z wyprodukowaniem dość pokaźnych ilości kodu. Do tego w klasycznych warunkach jeżeli chcemy dla elementu wprowadzić interakcję - np. najazd kursorem, przesunięcie itp. - w css użyjemy po prostu :hover, a w js np. mouseenter/mouseleave. W przypadku canvas trzeba by taką interakcję programowo wyliczać obliczając pozycję kursora, elementu itp. Ogólnie więc czeka nas tutaj dużo zabawy.
Z tego też powodu jeżeli chcemy tutaj tworzyć coś bardziej skomplikowanego niż proste rzeczy, większość ludzi sięga raczej po gotowe biblioteki, które udostępniają nam setki przydatnych funkcjonalności. Te bardziej popularne to pixijs czy p5.js. Ale i wiele popularnych bibliotek - np. charjs zbudowana jest właśnie w oparciu o canvas.
Jeżeli chodzi o tworzenie gier w canvasie, raczej bym nie pisał ich totalnie od podstaw. Z jednej strony jest to naprawdę porządny trening. I mówię tutaj - porządny!. Praktycznie żadna aplikacja w React, czy innym frameworku nie da wam takiego treningu jak napisanie dynamicznej gry w canvasie. To nie zrobienie pętli po tablicy. Mówimy tutaj o wyliczaniu kątów, obliczaniu fizyki itp. Dodatkowo JavaScript nie jest najwydajniejszym językiem na świecie i dość szybko będziemy musieli się zmierzyć nie tylko z problemami takimi jak obrót rysowanego elementu w kierunku kursora ale i coś bardziej zaawansowanego jak buforowanie klatek, optymalizację obiektów czy odpowiednie podejście do wykonywania obliczeń na wielu elementach na planszy.
Czy odradzam więc takie zabawy? Oczywiście, że nie. Ba - polecam bardzo. Dużo osób wkraczających we Frontend skupia się tylko na aplikacjach Javascript. A przecież można tutaj robić wiele, wiele innych rzeczy. Przejrzyj sobie filmy na tym kanale, a zobaczysz, ile zabawy może przynieść rozwiązywanie mniej pospolitych problemów.
W przypadku tworzenia gier warto sięgnąć też po gotowe silniki. Najpopularniejszymi są Game Maker, Construct czy Phaser. Ten pierwszy daje możliwość tworzenia gier w Canvasie (ale trzeba zapłacić), a te dwa ostanie działają głównie w oparciu o Canvas. Ale znowu - nawet jeżeli użyjemy gotowych rozwiązań, wcale nie oznacza to, że nie czeka nas rozwiązywanie złożonych problemów. Sam bardzo często przeglądam ten blog, gdzie autor dzieli sie jak tworzyć różne gry w Phaser. Kodu i ciekawych pomysłów tam co nie miara...
A co jeżeli nie umiem i nigdy nie będę umiał działać z Canvasem? Nic straconego. Wystarczy regularnie przeglądać strony takie jak np. codepen.io, gdzie autorzy co chwila wrzucają wiele fajnych eksperymentów właście w Canvas (1, 2, 3, 4, 5, 6, 7, 8 i wiele innych) i po prostu analizować. Temat jest bardzo rozległy. Co ciekawe nawet popularny ostatnio three.js też działa na canvasie.
Ściągawka z metod
Ściągawkę zawierającą zbiór metod canvas możesz ściągnąć tutaj
Odwołanie się do canvasu
No dobrze. Ale od czegoś trzeba zacząć. Małymi kroczkami, aż staniemy się mistrzami.
Aby zacząć rysowanie po naszym płótnie, musimy pobrać jego zawartość. Służy do tego funkcja getContext("2d"). Tryb 2d służy do rysowania 2d. Pozostałe możliwości to webgl, webgl2 i bitmaprender, ale tych nie będę tutaj omawiał.
Rysowanie ścieżek
Po pobraniu referencji do płótna zaczynamy nim manipulować. Możemy tutaj wgrywać grafiki (tym zajmiemy się później), ale też jak w każdym programie graficznym możemy po nim rysować kształty. Rysowanie takie przypomina rysowanie piórkiem np. w programie Adobe Photoshop czy Ilustartorze. Jak to wygląda w praktyce? Wybieramy piórko, rysujemy niewidzialną linię, a następnie otrzymany kształt obrysowujemy lub wypełniamy.

Podobnie jak w powyższej animacji rysowanie ścieżek po płótnie będziemy wykonywać w kilku krokach:
rozpoczynamy rysowanie nowej ścieżki za pomocą metody beginPath()
Używamy metod do rysowania ścieżki - np. lineTo(x, y), moveTo(x, y) itp.
obrysowujemy stroke() lub wypełniamy fill() naszą ścieżkę
Jeżeli chcemy zacząć rysować kolejną ścieżkę, poprzednią obrysowujemy/wypełniamy i zaczynamy rysować kolejny path:
W kolejnych przykładach będziemy generować sporo losowych liczb z zakresu, dlatego napiszmy pomocą funkcję, która nam takie zwróci:
Spróbujmy użyć powyższych funkcji w praktyce:
Jeżeli dodatkowo chcielibyśmy połączyć początkowy i końcowy punkt ścieżki, możemy skorzystać z funkcji closePath(), która zamyka całą ścieżkę
Rysowanie prostokątów
W powyższych przykładach rysowaliśmy niewidzialne ścieżki, a następnie albo je obrysowaliśmy, albo wypełnialiśmy kolorem. Jeżeli chcielibyśmy narysować prostokąt, możemy rysować każdy bok jako oddzielną kreskę:
Zamiast rozpoczynać path, rysować ręcznie każdy bok, i zamykać path, możemy też użyć gotowych funkcji, które wykonają powyższe te za nas.
Funkcje te to:
fillRect(x, y, width, height)
rysuje wypełniony prostokąt
strokeRect(x, y, width, height)
rysuje obrysowany prostokąt
clearRect(x, y, width, height)
czyści określony obszar
Bardzo ważną dla nas będzie funkcja clearRect(), którą bardzo często będziemy wykorzystywać do czyszczenia całego canvasu:
Tekst
Aby wypisać tekst, możemy skorzystać z dwóch funkcji:
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 możemy ustawić wygląd i pozycję pisanego tekstu za pomocą właściwości:
textAlign
wyrównanie tekstu w poziomie. Możliwe wartości to: start, end, left, right, center. Domyślną wartością jest start.
textBaseline
Określa pionowe wyrównanie tekstu względem danego punktu. Możliwe wartości to: top, hanging, middle, alphabetic, ideographic, bottom. Domyślną jest alphabetic.
Jeżeli byśmy chcieli policzyć szerokość ile zajmuje wypisany tekst, możemy skorzystać z funkcji measureText(), która zwraca dane o tekście pisanym z danym formatowaniem:
W momencie pisania tego tekstu za pomocą measureText możemy pobrać tylko szerokość danego tekstu, ale w przyszłości będziemy mieli więcej możliwości.
Rysowanie łuków w ścieżkach
Kolejnym kształtem który możemy dołączyć do naszej ścieżki jest łuk:
arc(x, y, radius, startAngle, endAngle, anticlockwise*)
Atrybuty x i y określają miejsce postawienia igły cyrkla. Parametr radius określa promień. Parametry startAngle i endAngle podajemy w radianach i określają one początkowy i końcowy kąt rysowanego łuku. Ostatni parametr określa czy rysować z kierunkiem wskazówek czy w odwrotnym.
Początkowy i końcowy kąt rysowanego łuku podane są w radianach, dlatego napiszmy funkcję, która przeliczy tradycyjne kąty na radiany:
Żeby narysować pełne kółko musimy narysować łuk o kącie 360 i go wypełnić:
Podobną w działaniu jest metoda ellipse(x, y, radiusX, radiusY, rotation, startAngle, endAngle, anticlockwise*), która służy do rysowania elips.
Funkcja ta ma podobne parametry do arc(), jedyną różnicą jest to, że podajemy tutaj 2 promienie zamiast pojedynczego a i dochodzi nam dodatkowy parametr rotation, który oznacza obrócenie elipsy.
Poza powyższymi dwoma mamy też funkcję arcTo(x1, y1, x2, y2, radius)), która rysuje łuk połączony z resztą ścieżki:
Kliknij na canvasie w jakieś miejsce by postawić punkt zakrzywienia. Prawym przyciskiem stawiasz drugi punkt. Dodatkowo pod canvasem input:range zmienia range rysowanego łuku.
Rysowanie zakrzywionych ścieżek
Aby narysować zakrzywione linie skorzystamy z funkcji:
quadraticCurveTo(cpx, cpy, x, y)
rysuje kwadratową ścieżkę do punktu x, y. Atrybuty cp1x i cp1y określają położenie punktu kontrolnego wyginającego ścieżkę
Kliknij na canvasie w jakieś miejsce by postawić punkt zakrzywienia:
bezierCurveTo(cp1x, cp1y, cp2x, cp2y, x, y)
rysuje ścieżkę Beziera do punktu x, y. Atrybuty cp1x, cp1y, cp2x, cp2y określają położenie punktów kontrolnych wyginających ścieżkę.
Klikaj na canvasie lewym przyciskiem by postawić pierwszy punkt. Prawym by postawić drugi punkt:
Kolory, przezroczystość
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ą.
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 to używanie po prostu kolorów z przezroczystością (rgba, hsla):
Wygląd linii
Wygląd linii ustawiamy wykorzystując metody:
lineWidth - określa grubość linii
lineCap - określa wygląd zakończenia rysowanej linii. Może przyjąć wartość butt, round lub square.
lineJoin - określa sposób łączenia 2 linii. Może przyjąć wartość round, bevel lub miter.
miterLimit - określa jak daleko kąt połączenia 2 linii metodą miter może wychodzić.
Dodatkowo możemy zdefiniować linię przerywaną za pomocą metody setLineDash(). Jako parametr przyjmuje ona tablicę 2 liczb. Pierwsza określa długość pojedynczej linii, a druga odstęp pomiędzy kolejnymi liczbami:
Gradienty
Gradienty w Canvasie działają nieco inaczej niż te znane z CSS, a bardziej przypominają te używane np. w programie Adobe Ilustrator. Tworząc gradient musimy określić jego początkową i końcową pozycję. Następnie rysujemy jakiś kształt, który jeżeli zostanie wypełniony, wybierze sobie odpowiedni kawałek z naszego gradientu.
Do tworzenia gradientów korzystamy z metod:
createLinearGradient(x1, y1, x2, y2)
tworzy gradient, który możemy wykorzystać do obrysowani albo wypełnienia (za pomocą powyżej opisanych fillStyle i strokeStyle). 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ń punktu początkowego okręgu, natomiast x2, y2, r2 - punktu końcowego okręgu
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. Traktuj to jak dodatkowe suwaki np. w tym generatorze
Jeżeli byśmy chcieli, by gradient zaczynał się wraz z figurą, musimy rozpocząć go wraz z pozycją figury.
Zapisywanie i odczytywanie stanu
Na zakończenie poznamy jeszcze 2 bardzo przydatne metody. Metoda save() służy do zapisania stanu canvasu - w tym właśnie ustawień odnośnie rysowania. Metoda restore() jak sama nazwa wskazuje służy do odczytania zapisanego wcześniej stanu. Metoda save i restore działają na zasadzie stosu. Save() odkłada coś na stos, a restore() pobiera ostatni odłożony zapisany stan.
Wyobraź sobie, że masz w programie graficznym belkę z narzędziami, wyborem kolorów, możliwymi transformacjami, dodatkowymi narzędziami, które mają swoje funkcje, zmieniają położenie czy rotację canvasu itp. Metoda save() będzie służyć tutaj do zapamiętania stanu wybranych w danym momencie opcji. Restore przywróci wcześniej zapisany stan.
W przypadku canvas zapisywane są następujące rzeczy:
transformacje
Obszar przycinania za pomocą clip()
Ustawienia właściwości: globalAlpha globalCompositeOperation strokeStyle textAlign, textBaseline lineCap, lineJoin, lineWidth, and miterLimit fillStyle font shadowBlur, shadowColor, shadowOffsetX, and shadowOffsetY
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. Żeby nie popsuć obecnych ustawień odkładamy jest na stos za pomocą metody save(). Ustawiamy jakieś nowe parametry, zmieniamy kolor. Po narysowaniu kształtów chcemy wrócić do początkowych ustawień więc odpalamy restore().
Cień
Aby do rysowanej figury dodać cień, możemy posłużyć się jedną z 4 metod:
shadowBlur
rozmycie cienia
shadowColor
kolor cienia
Przykład użycia poznanych wiadomości
Wycinanie - clip
Jeżeli kiedykolwiek używałeś maski w svg, maski w dowolnym programie graficznym czy nawet strony https://bennettfeely.com/clippy/, będziesz wiedział o czym mówimy.
Metoda clip() sprawia, że aktualnie rysowany path staje się właśnie taką maską, która przycina dany obrazek do jej krawędzi.
Gdy stworzymy maskę za pomocą clip() będzie ona oddziaływać na wszystko co po niej stworzymy.
Aby wyłączyć coś z działania tego narzędzia, musimy ponownie posłużyć się zapisem i przywróceniem stanu:
Kompozycja
Canvas udostępnia nam 2 właściwości, które określają sposób rysowania po canvasie.
globalCompositeOperation
Określa jak mają być nanoszone nowe kształty na płótno - sprawdź najlepiej na poniższym przykładzie
Działanie globalCompositeOperation można przyrównać do operacji boolowskich znanych z programów graficznych (np. pathfinder z AdobeIllustrator lub modyfikator Boolean w Blender3d).

Wartości dla globalCompositeOperation to:
destination-atop
Rysuje figurę oraz tą część canvasa, która zazębiała się z rysowaną figurą
destination-in
Pozostawia na canvasie tą część figur, które zazębiały się z rysowaną figurą
destination-out
Pozostawia na canvasie te części figur, które nie zazębiały się z rysowaną figurą.
destination-over
Rysuje figurę pod figurami z canvasu
lighter
W miejscach zazębiania się sumuje kolory rysowanej figury i canvasu.
source-atop
Pokazuje część canvasa która kolidowała z rysowaną figurą nad nią
source-in
Rysuje figurę tylko w miejscach gdzie zazębiała się z figurami narysowanymi na canvasie.
source-out
Pokazuje rysowaną figurę w miejscach, gdzie canvas był transparenty. W innych miejscach pokazuje przezroczystość.
source-over
Domyślna wartość. Pokazuje rysowaną figurę w miejscu rysowania. Źródło zostaje zachowane.
Transformacje
Dla zawartości canvas możemy używać kilku podstawowych funkcji transformacji.
Canvas jako powierzchnia to obszar, na który możemy nałożyć "macierz transformacji", która następnie go zmienia. Co to dla nas oznacza?
Jeżeli przykładowo nałożę na canvas efekt rotate(), rysowane następnie figury będą obrócone - mimo tego, że rysuję je normalnie.
Mamy kilka funkcji, które możemy tutaj wykorzystywać:
setTransform(a, b, c, d, e, f)
Ustawia matrix transformacji. Kolejne parametry to: a - pozioma skala, b - pionowe pochylenie (skew), c - poziome pochylenie, d - pionowa skala, e - poziome przemieszczenie, f - pionowe przemieszczenie
rotate()
obraca płótno, co oznacza, że rysowane elementy będą pojawiać się w innej pozycji. Płótno obracane jest względem punkty 0,0
translate(x, y)
przesuwa płótno (podobnie do odpowiednika z css)
scale(x, y)
skaluje płótno, co oznacza, że rysowane figury będą pojawiać się przeskalowane. Płótno skalowane jest względem punktu 0,0
Jak widzisz, transformacja rotate jest wykonywana względem punktu 0, 0. Niestety nie mamy tutaj znanej z css właściwości transform-origin, która w css zmienia taki punkt. Żeby obracać dany element względem konkretnego punktu, musimy skorzystać z dodatkowych obliczeń.
Kroki, które musimy wykonać są następujące:
Przesuwamy cały canvas (a więc i punkt transformacji) za pomocą translate() do punktu, w którym mamy obrócić dany element (czyli środka naszej przyszłej figury)
Obracamy płótno za pomocą rotate()
Rysujemy obiekt tak, by jego środek znajdował się w miejscu obrotu

W przypadku tekstów żeby narysować tekst z środkiem w punkcie obrotu możemy posłużyć się właściwościami wyrównującymi:
Podobnie do rotate() będziemy działać w przypadku scale, która także działa względem punktu 0,0:
Oczywiście skale i obracanie możemy łączyć razem:
Last updated